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

store auth_server list in config db

+72 -39
+9 -5
src/atproto/__init__.py
··· 2 from os import getenv 3 from re import match as regex_match 4 from typing import Any, TypeGuard 5 6 from aiodns import DNSResolver 7 from aiodns import error as dns_error ··· 96 didkv: KV[Handle, DID], 97 pdskv: KV[DID, PdsUrl], 98 ) -> tuple[DID, Handle, PdsUrl] | None: 99 - base = "https://slingshot.microcosm.blue" 100 - url = f"{base}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier={query}" 101 response = await client.get(url) 102 if not response.ok: 103 return None ··· 241 return authserver_url 242 243 assert is_safe_url(pds_url) 244 - endpoint = f"{pds_url}/.well-known/oauth-protected-resource" 245 response = await client.get(endpoint) 246 if response.status != 200: 247 return None ··· 258 """Returns metadata from the authserver""" 259 260 assert is_safe_url(authserver_url) 261 - endpoint = f"{authserver_url}/.well-known/oauth-authorization-server" 262 response = await client.get(endpoint) 263 if not response.ok: 264 return None ··· 278 """Retrieve record from PDS. Verifies type is the same as collection name.""" 279 280 params = {"repo": repo, "collection": collection, "rkey": record} 281 - response = await client.get(f"{pds}/xrpc/com.atproto.repo.getRecord", params=params) 282 if not response.ok: 283 return None 284 parsed = await response.json()
··· 2 from os import getenv 3 from re import match as regex_match 4 from typing import Any, TypeGuard 5 + from urllib.parse import urljoin 6 7 from aiodns import DNSResolver 8 from aiodns import error as dns_error ··· 97 didkv: KV[Handle, DID], 98 pdskv: KV[DID, PdsUrl], 99 ) -> tuple[DID, Handle, PdsUrl] | None: 100 + url = urljoin( 101 + "https://slingshot.microcosm.blue", 102 + f"/xrpc/com.bad-example.identity.resolveMiniDoc?identifier={query}", 103 + ) 104 response = await client.get(url) 105 if not response.ok: 106 return None ··· 244 return authserver_url 245 246 assert is_safe_url(pds_url) 247 + endpoint = urljoin(pds_url, "/.well-known/oauth-protected-resource") 248 response = await client.get(endpoint) 249 if response.status != 200: 250 return None ··· 261 """Returns metadata from the authserver""" 262 263 assert is_safe_url(authserver_url) 264 + endpoint = urljoin(authserver_url, "/.well-known/oauth-authorization-server") 265 response = await client.get(endpoint) 266 if not response.ok: 267 return None ··· 281 """Retrieve record from PDS. Verifies type is the same as collection name.""" 282 283 params = {"repo": repo, "collection": collection, "rkey": record} 284 + url = urljoin(pds, "/xrpc/com.atproto.repo.getRecord") 285 + response = await client.get(url, params=params) 286 if not response.ok: 287 return None 288 parsed = await response.json()
+26
src/config.py
···
··· 1 + from sqlite3 import Connection 2 + from typing import NamedTuple 3 + 4 + from flask import Flask 5 + 6 + from src.db import get_db 7 + 8 + 9 + class AuthServer(NamedTuple): 10 + name: str 11 + url: str 12 + 13 + 14 + class Config: 15 + db: Connection 16 + 17 + def __init__(self, app: Flask): 18 + self.db = get_db(app, name="config") 19 + 20 + def auth_servers(self) -> list[AuthServer]: 21 + raw = ( 22 + self.db.cursor() 23 + .execute("select name, url from pdss order by relevance desc") 24 + .fetchall() 25 + ) 26 + return [AuthServer(*r) for r in raw]
+7
src/config.sql
···
··· 1 + create table if not exists pdss ( 2 + name text not null unique, 3 + url text not null unique, 4 + relevance integer not null 5 + ) strict; 6 + 7 + create index if not exists pdss_by_relevance on pdss(relevance desc);
+18 -12
src/db.py
··· 1 import sqlite3 2 from logging import Logger 3 from sqlite3 import Connection 4 - from typing import Generic, cast, override 5 6 from flask import Flask, g 7 ··· 15 prefix: str 16 17 def __init__(self, app: Connection | Flask, logger: Logger, prefix: str): 18 - self.db = app if isinstance(app, Connection) else get_db(app) 19 self.logger = logger 20 self.prefix = prefix 21 ··· 42 self.db.commit() 43 44 45 - def get_db(app: Flask) -> sqlite3.Connection: 46 - db: sqlite3.Connection | None = g.get("db", None) 47 if db is None: 48 - db_path: str = app.config.get("DATABASE_URL", "ligoat.db") 49 - db = g.db = sqlite3.connect(db_path, check_same_thread=False) 50 # return rows as dict-like objects 51 db.row_factory = sqlite3.Row 52 return db 53 54 55 def close_db_connection(_exception: BaseException | None): 56 - db: sqlite3.Connection | None = g.get("db", None) 57 - if db is not None: 58 - db.close() 59 60 61 - def init_db(app: Flask): 62 with app.app_context(): 63 - db = get_db(app) 64 - with app.open_resource("schema.sql", mode="r") as schema: 65 _ = db.cursor().executescript(schema.read()) 66 db.commit()
··· 1 import sqlite3 2 from logging import Logger 3 from sqlite3 import Connection 4 + from typing import Generic, Literal, cast, override 5 6 from flask import Flask, g 7 ··· 15 prefix: str 16 17 def __init__(self, app: Connection | Flask, logger: Logger, prefix: str): 18 + self.db = app if isinstance(app, Connection) else get_db(app, name="keyval") 19 self.logger = logger 20 self.prefix = prefix 21 ··· 42 self.db.commit() 43 44 45 + type DatabaseName = Literal["config"] | Literal["keyval"] 46 + 47 + 48 + def get_db(app: Flask, name: DatabaseName) -> sqlite3.Connection: 49 + global_key = f"{name}_db" 50 + db: sqlite3.Connection | None = g.get(global_key, None) 51 if db is None: 52 + db_path: str = app.config[f"{name.upper()}_DB_URL"] 53 + db = sqlite3.connect(db_path, check_same_thread=False) 54 + setattr(g, global_key, db) 55 # return rows as dict-like objects 56 db.row_factory = sqlite3.Row 57 return db 58 59 60 def close_db_connection(_exception: BaseException | None): 61 + for name in ["keyval", "config"]: 62 + db: sqlite3.Connection | None = g.pop(f"{name}_db", None) 63 + if db is not None: 64 + db.close() 65 66 67 + def init_db(app: Flask, name: DatabaseName) -> None: 68 with app.app_context(): 69 + db = get_db(app, name) 70 + with app.open_resource(f"{name}.sql", mode="r") as schema: 71 _ = db.cursor().executescript(schema.read()) 72 db.commit()
+8 -20
src/main.py
··· 20 refresh_auth_session, 21 save_auth_session, 22 ) 23 from src.db import KV, close_db_connection, get_db, init_db 24 from src.oauth import oauth 25 ··· 28 app.register_blueprint(oauth) 29 htmx = HTMX() 30 htmx.init_app(app) 31 - init_db(app) 32 33 34 @app.before_request ··· 58 async def page_profile(atid: str): 59 reload = request.args.get("reload") is not None 60 61 - db = get_db(app) 62 didkv = KV[Handle, DID](db, app.logger, "did_from_handle") 63 pdskv = KV[DID, PdsUrl](db, app.logger, "pds_from_did") 64 ··· 103 ) 104 105 106 - class AuthServer(NamedTuple): 107 - name: str 108 - url: str 109 - 110 - 111 - auth_servers: list[AuthServer] = [ 112 - AuthServer("Bluesky", "https://bsky.social"), 113 - AuthServer("Blacksky", "https://blacksky.app"), 114 - AuthServer("Northsky", "https://northsky.social"), 115 - AuthServer("tangled.org", "https://tngl.sh"), 116 - AuthServer("Witchraft Systems", "https://pds.witchcraft.systems"), 117 - AuthServer("selfhosted.social", "https://selfhosted.social"), 118 - ] 119 - 120 - if app.debug: 121 - auth_servers.append(AuthServer("pds.rip", "https://pds.rip")) 122 - 123 - 124 @app.get("/login") 125 async def page_login(): 126 if await get_user() is not None: 127 return redirect("/editor") 128 return render_template("login.html", auth_servers=auth_servers) 129 130
··· 20 refresh_auth_session, 21 save_auth_session, 22 ) 23 + from src.config import AuthServer, Config 24 from src.db import KV, close_db_connection, get_db, init_db 25 from src.oauth import oauth 26 ··· 29 app.register_blueprint(oauth) 30 htmx = HTMX() 31 htmx.init_app(app) 32 + init_db(app, name="config") 33 + init_db(app, name="keyval") 34 35 36 @app.before_request ··· 60 async def page_profile(atid: str): 61 reload = request.args.get("reload") is not None 62 63 + db = get_db(app, name="keyval") 64 didkv = KV[Handle, DID](db, app.logger, "did_from_handle") 65 pdskv = KV[DID, PdsUrl](db, app.logger, "pds_from_did") 66 ··· 105 ) 106 107 108 @app.get("/login") 109 async def page_login(): 110 if await get_user() is not None: 111 return redirect("/editor") 112 + config = Config(app) 113 + auth_servers = config.auth_servers() 114 + if app.debug: 115 + auth_servers.append(AuthServer("pds.rip", "https://pds.rip")) 116 return render_template("login.html", auth_servers=auth_servers) 117 118
+2 -2
src/oauth.py
··· 43 if not username: 44 return redirect(url_for("page_login"), 303) 45 46 - db = get_db(current_app) 47 didkv = KV[Handle, DID](db, current_app.logger, "did_from_handle") 48 pdskv = KV[DID, PdsUrl](db, current_app.logger, "pds_from_did") 49 authserverkv = KV[PdsUrl, AuthserverUrl]( ··· 177 178 row = auth_request 179 180 - db = get_db(current_app) 181 didkv = KV(db, current_app.logger, "did_from_handle") 182 authserverkv = KV(db, current_app.logger, "authserver_from_pds") 183
··· 43 if not username: 44 return redirect(url_for("page_login"), 303) 45 46 + db = get_db(current_app, name="keyval") 47 didkv = KV[Handle, DID](db, current_app.logger, "did_from_handle") 48 pdskv = KV[DID, PdsUrl](db, current_app.logger, "pds_from_did") 49 authserverkv = KV[PdsUrl, AuthserverUrl]( ··· 177 178 row = auth_request 179 180 + db = get_db(current_app, name="keyval") 181 didkv = KV(db, current_app.logger, "did_from_handle") 182 authserverkv = KV(db, current_app.logger, "authserver_from_pds") 183
src/schema.sql src/keyval.sql
+2
src/templates/login.html
··· 28 <input type="submit" value="continue" /> 29 </form> 30 31 <div class="authservers"> 32 <span class="faded caption">If you're unsure you can log in with...</span> 33 <form action="{{ url_for('auth_login') }}" method="post"> ··· 36 {% endfor %} 37 </form> 38 </div> 39 40 <footer> 41 <p>
··· 28 <input type="submit" value="continue" /> 29 </form> 30 31 + {% if auth_servers %} 32 <div class="authservers"> 33 <span class="faded caption">If you're unsure you can log in with...</span> 34 <form action="{{ url_for('auth_login') }}" method="post"> ··· 37 {% endfor %} 38 </form> 39 </div> 40 + {% endif %} 41 42 <footer> 43 <p>