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.

first commit

@nauta.one af8c0b1e

+192
+1
.gitignore
··· 1 + .venv
+86
src/main.py
··· 1 + from flask import Flask, render_template 2 + from urllib import request 3 + import json 4 + 5 + app = Flask(__name__) 6 + 7 + PLC_DIRECTORY = "https://plc.directory" 8 + 9 + 10 + @app.route("/") 11 + def hello_world(): 12 + return "<3" 13 + 14 + 15 + @app.route("/<string:handle>") 16 + def page_profile(handle: str): 17 + if handle == "favicon.ico": 18 + return "not found", 404 19 + 20 + did = resolve_did_from_handle(handle) 21 + pds = resolve_pds_from_did(did) 22 + profile = load_profile(pds, did) 23 + links = load_links(pds, did) 24 + return render_template("profile.html", profile=profile, links=links) 25 + 26 + 27 + def load_links(pds: str, did: str) -> list[dict[str, str]]: 28 + response = get_record(pds, did, "one.nauta.actor.links", "self") 29 + record = json.loads(response) 30 + return record["value"]["links"] 31 + 32 + 33 + def load_profile(pds: str, did: str) -> tuple[str, str]: 34 + response = get_record(pds, did, "app.bsky.actor.profile", "self") 35 + record = json.loads(response) 36 + value: dict[str, str] = record["value"] 37 + return (value["displayName"], value["description"]) 38 + 39 + 40 + pdss: dict[str, str] = {} 41 + 42 + 43 + def resolve_pds_from_did(did: str, reload: bool = False) -> str: 44 + if did in pdss and not reload: 45 + app.logger.debug(f"returning cached pds for {did}") 46 + return pdss[did] 47 + 48 + response = http_get(f"{PLC_DIRECTORY}/{did}") 49 + parsed = json.loads(response) 50 + pds = parsed["service"][0]["serviceEndpoint"] 51 + pdss[did] = pds 52 + app.logger.debug(f"caching pds {pds} for {did}") 53 + return pds 54 + 55 + 56 + dids: dict[str, str] = {} 57 + 58 + 59 + def resolve_did_from_handle(handle: str, reload: bool = False) -> str: 60 + if handle in dids and not reload: 61 + app.logger.debug(f"returning cached did for {handle}") 62 + return dids[handle] 63 + 64 + response = http_get(f"https://dns.google/resolve?name=_atproto.{handle}&type=TXT") 65 + parsed = json.loads(response) 66 + answers = parsed["Answer"] 67 + if len(answers) < 1: 68 + return handle 69 + data: str = answers[0]["data"] 70 + if not data.startswith("did="): 71 + return handle 72 + did = data[4:] 73 + dids[handle] = did 74 + app.logger.debug(f"caching did {did} for {handle}") 75 + return did 76 + 77 + 78 + def get_record(pds: str, repo: str, collection: str, record: str) -> str: 79 + response = http_get( 80 + f"{pds}/xrpc/com.atproto.repo.getRecord?repo={repo}&collection={collection}&rkey={record}" 81 + ) 82 + return response 83 + 84 + 85 + def http_get(url: str) -> str: 86 + return request.urlopen(url).read()
+80
src/static/style.css
··· 1 + body { 2 + background: #fff; 3 + color: #333; 4 + font-size: 20px; 5 + font-family: monospace; 6 + font-weight: 450; 7 + margin: 1rem; 8 + -webkit-font-smoothing: antialiased; 9 + -moz-osx-font-smoothing: grayscale; 10 + } 11 + 12 + @media (prefers-color-scheme: dark) { 13 + body { 14 + background: #111; 15 + color: #fff; 16 + } 17 + } 18 + 19 + .wrapper { 20 + margin: auto; 21 + max-width: 25rem; 22 + } 23 + 24 + header { 25 + margin: 2.5em 0; 26 + text-align: center; 27 + } 28 + 29 + header h1 { 30 + margin: 0; 31 + font-size: 1.5em; 32 + font-weight: inherit; 33 + 34 + & a { 35 + color: inherit; 36 + text-decoration: none; 37 + } 38 + } 39 + 40 + header .tagline { 41 + font-style: italic; 42 + } 43 + 44 + ul { 45 + list-style: none; 46 + padding: 0; 47 + } 48 + 49 + li { 50 + background: currentColor; 51 + transition: transform 0.1s; 52 + text-align: center; 53 + box-shadow: -3px 3px 0 rgba(from currentColor r g b / 0.6); 54 + } 55 + 56 + li .detail { 57 + display: block; 58 + font-size: 0.75em; 59 + opacity: 0.6; 60 + transition: opacity 0.3s; 61 + } 62 + 63 + li:hover { 64 + transform: scale(1.05); 65 + 66 + & .detail { 67 + opacity: 1; 68 + } 69 + } 70 + 71 + li + li { 72 + margin-top: 1rem; 73 + } 74 + 75 + li a { 76 + color: white; 77 + display: block; 78 + padding: 1rem; 79 + text-decoration: none; 80 + }
+25
src/templates/profile.html
··· 1 + <!doctype html> 2 + <html> 3 + <head> 4 + <link 5 + rel="stylesheet" 6 + href="{{ url_for('static', filename='style.css') }}" 7 + /> 8 + </head> 9 + <body> 10 + <div class="wrapper"> 11 + <header> 12 + <h1>{{ profile.0 }}</h1> 13 + <span class="tagline">{{ profile.1 }}</span> 14 + </header> 15 + <ul> 16 + {% for link in links %} 17 + <li style="color: {{ link.color }}"> 18 + <a href="{{ link.url }}">{{ link.title }}</a> 19 + </li> 20 + {% endfor %} 21 + </ul> 22 + </div> 23 + <!-- .wrapper --> 24 + </body> 25 + </html>