decentralized and customizable links page on top of atproto ligo.at
atproto link-in-bio python uv
9
fork

Configure Feed

Select the types of activity you want to include in your feed.

improve logging & separate session auth

+102 -85
-6
src/atproto/__init__.py
··· 84 84 85 85 did = kv.get(handle) 86 86 if did is not None and not reload: 87 - print(f"returning cached did for {handle}") 88 87 return did 89 88 90 89 resolver = DNSResolver() ··· 98 97 if value.startswith("did="): 99 98 did = value[4:] 100 99 if is_valid_did(did): 101 - print(f"caching did {did} for {handle}") 102 100 kv.set(handle, value=did) 103 101 return did 104 102 ··· 122 120 ) -> PdsUrl | None: 123 121 pds = kv.get(did) 124 122 if pds is not None and not reload: 125 - print(f"returning cached pds for {did}") 126 123 return pds 127 124 128 125 doc = await resolve_doc_from_did(client, did) ··· 131 128 pds = doc["service"][0]["serviceEndpoint"] 132 129 if pds is None: 133 130 return None 134 - print(f"caching pds {pds} for {did}") 135 131 kv.set(did, value=pds) 136 132 return pds 137 133 ··· 165 161 166 162 authserver_url = kv.get(pds_url) 167 163 if authserver_url is not None and not reload: 168 - print(f"returning cached authserver for PDS {pds_url}") 169 164 return authserver_url 170 165 171 166 assert is_safe_url(pds_url) ··· 175 170 return None 176 171 parsed: dict[str, list[str]] = await response.json() 177 172 authserver_url = parsed["authorization_servers"][0] 178 - print(f"caching authserver {authserver_url} for PDS {pds_url}") 179 173 kv.set(pds_url, value=authserver_url) 180 174 return authserver_url 181 175
+7 -2
src/atproto/kv.py
··· 1 1 from abc import ABC, abstractmethod 2 + from logging import Logger 2 3 from typing import override 3 4 4 5 ··· 12 13 pass 13 14 14 15 15 - class NoKV(KV): 16 + class _NoKV(KV): 17 + logger: Logger = Logger(__name__) 18 + 16 19 @override 17 20 def get(self, key: str) -> str | None: 21 + self.logger.debug(f"NoKV get({key})") 18 22 return None 19 23 20 24 @override 21 25 def set(self, key: str, value: str): 26 + self.logger.debug(f"NoKV set({key}, {value})") 22 27 pass 23 28 24 29 25 - nokv = NoKV() 30 + nokv = _NoKV()
+4 -5
src/atproto/oauth.py
··· 7 7 from authlib.oauth2.rfc7636 import create_s256_code_challenge 8 8 9 9 from . import fetch_authserver_meta 10 - 11 - from ..types import OAuthAuthRequest, OAuthSession 12 - 10 + from .types import OAuthAuthRequest, OAuthSession 13 11 from ..security import is_safe_url, hardened_http 14 12 15 13 ··· 219 217 token_url, data=params, headers={"DPoP": dpop_proof} 220 218 ) 221 219 220 + respjson = await resp.json() 222 221 if resp.status not in [200, 201]: 223 - print(f"Token Refresh Error: {resp.json()}") 222 + print(f"Token Refresh Error: {respjson}") 224 223 225 224 resp.raise_for_status() 226 - token_body = await resp.json() 225 + token_body = respjson 227 226 tokens = OAuthTokens(**token_body) 228 227 229 228 return tokens, dpop_authserver_nonce
+57
src/auth.py
··· 1 + from flask import current_app 2 + from flask.sessions import SessionMixin 3 + from typing import NamedTuple, TypeVar 4 + 5 + from .atproto.types import OAuthAuthRequest, OAuthSession 6 + 7 + 8 + def save_auth_request(session: SessionMixin, request: OAuthAuthRequest): 9 + return _set_into_session(session, "oauth_auth_request", request) 10 + 11 + 12 + def save_auth_session(session: SessionMixin, auth_session: OAuthSession): 13 + return _set_into_session(session, "oauth_auth_session", auth_session) 14 + 15 + 16 + def delete_auth_request(session: SessionMixin): 17 + return _delete_from_session(session, "oauth_auth_request") 18 + 19 + 20 + def delete_auth_session(session: SessionMixin): 21 + return _delete_from_session(session, "oauth_auth_session") 22 + 23 + 24 + def get_auth_request(session: SessionMixin) -> OAuthAuthRequest | None: 25 + return _get_from_session(session, "oauth_auth_request", OAuthAuthRequest) 26 + 27 + 28 + def get_auth_session(session: SessionMixin) -> OAuthSession | None: 29 + return _get_from_session(session, "oauth_auth_session", OAuthSession) 30 + 31 + 32 + def _set_into_session(session: SessionMixin, key: str, value: NamedTuple): 33 + session[key] = value._asdict() 34 + 35 + 36 + def _delete_from_session(session: SessionMixin, key: str): 37 + del session[key] 38 + 39 + 40 + OAuthClass = TypeVar("OAuthClass") 41 + 42 + 43 + def _get_from_session( 44 + session: SessionMixin, 45 + key: str, 46 + Type: type[OAuthClass], 47 + ) -> OAuthClass | None: 48 + if key not in session: 49 + return None 50 + 51 + try: 52 + return Type(**session[key]) 53 + except TypeError as exception: 54 + current_app.logger.debug(f"unable to load {key}") 55 + current_app.logger.debug(exception) 56 + del session[key] 57 + return None
+11 -4
src/db.py
··· 1 + from flask import Flask, g 2 + from logging import Logger 1 3 from typing import override 2 - from flask import Flask, g 3 4 4 5 import sqlite3 5 6 from sqlite3 import Connection ··· 9 10 10 11 class KV(BaseKV): 11 12 db: Connection 13 + logger: Logger 12 14 prefix: str 13 15 14 - def __init__(self, app: Connection | Flask, prefix: str): 16 + def __init__(self, app: Connection | Flask, logger: Logger, prefix: str): 15 17 self.db = app if isinstance(app, Connection) else get_db(app) 18 + self.logger = logger 16 19 self.prefix = prefix 17 20 18 21 @override 19 22 def get(self, key: str) -> str | None: 20 23 cursor = self.db.cursor() 21 - row = cursor.execute( 24 + row: dict[str, str] | None = cursor.execute( 22 25 "select value from keyval where prefix = ? and key = ?", 23 26 (self.prefix, key), 24 27 ).fetchone() 25 - return None if row is None else row["value"] 28 + if row is not None: 29 + self.logger.debug(f"returning cached {self.prefix}({key})") 30 + return row["value"] 31 + return None 26 32 27 33 @override 28 34 def set(self, key: str, value: str): 35 + self.logger.debug(f"caching {self.prefix}({key}): {value}") 29 36 cursor = self.db.cursor() 30 37 _ = cursor.execute( 31 38 "insert or replace into keyval (prefix, key, value) values (?, ?, ?)",
+8 -11
src/main.py
··· 13 13 resolve_pds_from_did, 14 14 ) 15 15 from .atproto.oauth import pds_authed_req 16 + from .atproto.types import OAuthSession 17 + from .auth import get_auth_session, save_auth_session 16 18 from .db import KV, close_db_connection, get_db, init_db 17 - from .oauth import get_auth_session, oauth, save_auth_session 18 - from .types import OAuthSession 19 + from .oauth import oauth 19 20 20 21 app = Flask(__name__) 21 22 _ = app.config.from_prefixed_env() ··· 49 50 reload = request.args.get("reload") is not None 50 51 51 52 db = get_db(app) 52 - didkv = KV(db, "did_from_handle") 53 - pdskv = KV(db, "pds_from_did") 53 + didkv = KV(db, app.logger, "did_from_handle") 54 + pdskv = KV(db, app.logger, "pds_from_did") 54 55 55 56 if atid.startswith("@"): 56 57 handle = atid[1:].lower() ··· 208 209 did: str, 209 210 reload: bool = False, 210 211 ) -> list[dict[str, str]] | None: 211 - kv = KV(app, "links_from_did") 212 + kv = KV(app, app.logger, "links_from_did") 212 213 recordstr = kv.get(did) 213 214 214 215 if recordstr is not None and not reload: 215 - app.logger.debug(f"returning cached links for {did}") 216 216 return json.loads(recordstr)["links"] 217 217 218 218 record = await get_record(client, pds, did, f"{SCHEMA}.actor.links", "self") 219 219 if record is None: 220 220 return None 221 221 222 - app.logger.debug(f"caching links for {did}") 223 222 kv.set(did, value=json.dumps(record)) 224 223 return record["links"] 225 224 ··· 231 230 fallback_with_bluesky: bool = True, 232 231 reload: bool = False, 233 232 ) -> tuple[dict[str, str] | None, bool]: 234 - kv = KV(app, "profile_from_did") 233 + kv = KV(app, app.logger, "profile_from_did") 235 234 recordstr = kv.get(did) 236 235 237 236 if recordstr is not None and not reload: 238 - app.logger.debug(f"returning cached profile for {did}") 239 237 return json.loads(recordstr), False 240 238 241 239 from_bluesky = False ··· 246 244 if record is None: 247 245 return None, False 248 246 249 - app.logger.debug(f"caching profile for {did}") 250 247 kv.set(did, value=json.dumps(record)) 251 248 return record, from_bluesky 252 249 ··· 284 281 285 282 286 283 def _is_did_blocked(did: str) -> bool: 287 - kv = KV(app, "blockeddids") 284 + kv = KV(app, app.logger, "blockeddids") 288 285 return kv.get(did) is not None
+15 -57
src/oauth.py
··· 1 1 from aiohttp.client import ClientSession 2 2 from authlib.jose import JsonWebKey, Key 3 3 from flask import Blueprint, current_app, jsonify, redirect, request, session, url_for 4 - from flask.sessions import SessionMixin 5 - from typing import NamedTuple 6 4 from urllib.parse import urlencode 7 5 8 6 import json 9 7 8 + from .auth import ( 9 + delete_auth_request, 10 + get_auth_request, 11 + save_auth_request, 12 + save_auth_session, 13 + ) 10 14 from .db import KV, get_db 11 - 12 15 from .atproto import ( 13 16 is_valid_did, 14 17 is_valid_handle, ··· 18 21 resolve_identity, 19 22 ) 20 23 from .atproto.oauth import initial_token_request, send_par_auth_request 24 + from .atproto.types import OAuthAuthRequest, OAuthSession 21 25 from .security import is_safe_url 22 - from .types import OAuthAuthRequest, OAuthSession 23 26 24 27 oauth = Blueprint("oauth", __name__, url_prefix="/oauth") 25 28 ··· 32 35 return redirect(url_for("page_login"), 303) 33 36 34 37 db = get_db(current_app) 35 - pdskv = KV(db, "authserver_from_pds") 38 + pdskv = KV(db, current_app.logger, "authserver_from_pds") 36 39 37 40 client = ClientSession() 38 41 39 42 if is_valid_handle(username) or is_valid_did(username): 40 43 login_hint = username 41 - kv = KV(db, "did_from_handle") 44 + kv = KV(db, current_app.logger, "did_from_handle") 42 45 identity = await resolve_identity(client, username, didkv=kv) 43 46 if identity is None: 44 47 return "couldnt resolve identity", 500 ··· 79 82 callback_endpoint = url_for("oauth.oauth_callback") 80 83 redirect_uri = f"https://{host}{callback_endpoint}" 81 84 82 - current_app.logger.debug(client_id) 83 - current_app.logger.debug(redirect_uri) 85 + current_app.logger.debug(f"client_id {client_id}") 86 + current_app.logger.debug(f"redirect_uri {redirect_uri}") 84 87 85 88 CLIENT_SECRET_JWK = JsonWebKey.import_key(current_app.config["CLIENT_SECRET_JWK"]) 86 89 ··· 96 99 ) 97 100 98 101 if resp.status == 400: 99 - current_app.logger.debug("PAR request returned error 400") 100 - current_app.logger.debug(resp.text) 102 + current_app.logger.warning("PAR request returned error 400") 103 + current_app.logger.warning(await resp.text()) 101 104 return redirect(url_for("page_login"), 303) 102 105 _ = resp.raise_for_status() 103 106 ··· 155 158 row = auth_request 156 159 157 160 db = get_db(current_app) 158 - didkv = KV(db, "did_from_handle") 159 - authserverkv = KV(db, "authserver_from_pds") 161 + didkv = KV(db, current_app.logger, "did_from_handle") 162 + authserverkv = KV(db, current_app.logger, "authserver_from_pds") 160 163 161 164 if row.did: 162 165 # If we started with an account identifier, this is simple ··· 234 237 CLIENT_SECRET_JWK = JsonWebKey.import_key(current_app.config["CLIENT_SECRET_JWK"]) 235 238 CLIENT_PUB_JWK = json.loads(CLIENT_SECRET_JWK.as_json(is_private=False)) 236 239 return jsonify({"keys": [CLIENT_PUB_JWK]}) 237 - 238 - 239 - # Session storage 240 - 241 - 242 - def save_auth_request(session: SessionMixin, request: OAuthAuthRequest): 243 - return _set_into_session(session, "oauth_auth_request", request) 244 - 245 - 246 - def save_auth_session(session: SessionMixin, auth_session: OAuthSession): 247 - return _set_into_session(session, "oauth_auth_session", auth_session) 248 - 249 - 250 - def delete_auth_request(session: SessionMixin): 251 - return _delete_from_session(session, "oauth_auth_request") 252 - 253 - 254 - def delete_auth_session(session: SessionMixin): 255 - return _delete_from_session(session, "oauth_auth_session") 256 - 257 - 258 - def get_auth_request(session: SessionMixin) -> OAuthAuthRequest | None: 259 - try: 260 - return OAuthAuthRequest(**session["oauth_auth_request"]) 261 - except (KeyError, TypeError) as exception: 262 - current_app.logger.debug("unable to load oauth_auth_request") 263 - current_app.logger.debug(exception) 264 - return None 265 - 266 - 267 - def get_auth_session(session: SessionMixin) -> OAuthSession | None: 268 - try: 269 - return OAuthSession(**session["oauth_auth_session"]) 270 - except (KeyError, TypeError) as exception: 271 - current_app.logger.debug("unable to load oauth_auth_session") 272 - current_app.logger.debug(exception) 273 - return None 274 - 275 - 276 - def _set_into_session(session: SessionMixin, key: str, value: NamedTuple): 277 - session[key] = value._asdict() 278 - 279 - 280 - def _delete_from_session(session: SessionMixin, key: str): 281 - del session[key]
src/types.py src/atproto/types.py