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

handle errors

+54 -32
+30 -10
src/main.py
··· 9 9 profiles: dict[str, tuple[str, str]] = {} 10 10 11 11 PLC_DIRECTORY = "https://plc.directory" 12 + SCHEMA = "one.nauta" 12 13 13 14 14 15 @app.route("/") ··· 24 25 reload = request.args.get("reload") is not None 25 26 26 27 did = resolve_did_from_handle(handle, reload=reload) 28 + if did is None: 29 + return "did not found", 404 27 30 pds = resolve_pds_from_did(did, reload=reload) 31 + if pds is None: 32 + return "pds not found", 404 28 33 profile = load_profile(pds, did, reload=reload) 29 - links = load_links(pds, did, reload=reload) 34 + links = load_links(pds, did, reload=reload) or [] 30 35 return render_template("profile.html", profile=profile, links=links) 31 36 32 37 33 - def load_links(pds: str, did: str, reload: bool = False) -> list[dict[str, str]]: 38 + def load_links(pds: str, did: str, reload: bool = False) -> list[dict[str, str]] | None: 34 39 if did in links and not reload: 35 40 app.logger.debug(f"returning cached links for {did}") 36 41 return links[did] 37 42 38 - response = get_record(pds, did, "one.nauta.actor.links", "self") 43 + response = get_record(pds, did, f"{SCHEMA}.actor.links", "self") 44 + if response is None: 45 + return None 46 + 39 47 record = json.loads(response) 40 48 link = record["value"]["links"] 41 49 app.logger.debug(f"caching links for {did}") ··· 43 51 return link 44 52 45 53 46 - def load_profile(pds: str, did: str, reload: bool = False) -> tuple[str, str]: 54 + def load_profile(pds: str, did: str, reload: bool = False) -> tuple[str, str] | None: 47 55 if did in profiles and not reload: 48 56 app.logger.debug(f"returning cached profile for {did}") 49 57 return profiles[did] 50 58 51 - response = get_record(pds, did, "app.bsky.actor.profile", "self") 59 + response = get_record(pds, did, f"{SCHEMA}.actor.profile", "self") 60 + if response is None: 61 + response = get_record(pds, did, "app.bsky.actor.profile", "self") 62 + if response is None: 63 + return None 64 + 52 65 record = json.loads(response) 53 66 value: dict[str, str] = record["value"] 54 67 profile = (value["displayName"], value["description"]) ··· 57 70 return profile 58 71 59 72 60 - def resolve_pds_from_did(did: str, reload: bool = False) -> str: 73 + def resolve_pds_from_did(did: str, reload: bool = False) -> str | None: 61 74 if did in pdss and not reload: 62 75 app.logger.debug(f"returning cached pds for {did}") 63 76 return pdss[did] 64 77 65 78 response = http_get(f"{PLC_DIRECTORY}/{did}") 79 + if response is None: 80 + return None 66 81 parsed = json.loads(response) 67 82 pds = parsed["service"][0]["serviceEndpoint"] 68 83 pdss[did] = pds ··· 70 85 return pds 71 86 72 87 73 - def resolve_did_from_handle(handle: str, reload: bool = False) -> str: 88 + def resolve_did_from_handle(handle: str, reload: bool = False) -> str | None: 74 89 if handle in dids and not reload: 75 90 app.logger.debug(f"returning cached did for {handle}") 76 91 return dids[handle] 77 92 78 93 response = http_get(f"https://dns.google/resolve?name=_atproto.{handle}&type=TXT") 94 + if response is None: 95 + return None 79 96 parsed = json.loads(response) 80 97 answers = parsed["Answer"] 81 98 if len(answers) < 1: ··· 89 106 return did 90 107 91 108 92 - def get_record(pds: str, repo: str, collection: str, record: str) -> str: 109 + def get_record(pds: str, repo: str, collection: str, record: str) -> str | None: 93 110 response = http_get( 94 111 f"{pds}/xrpc/com.atproto.repo.getRecord?repo={repo}&collection={collection}&rkey={record}" 95 112 ) 96 113 return response 97 114 98 115 99 - def http_get(url: str) -> str: 100 - return http_request.urlopen(url).read() 116 + def http_get(url: str) -> str | None: 117 + try: 118 + return http_request.urlopen(url).read() 119 + except http_request.HTTPError: 120 + return None
+24 -22
src/templates/profile.html
··· 1 1 <!doctype html> 2 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> 3 + <head> 4 + <meta charset="utf-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1" /> 6 + <base target="_blank" /> 7 + <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}" /> 8 + </head> 9 + <body> 10 + <div class="wrapper"> 11 + <header> 12 + <h1>{{ profile.0 }}</h1> 13 + {% if profile.1 %} 14 + <span class="tagline">{{ profile.1 }}</span> 15 + {% endif %} 16 + </header> 17 + <ul> 18 + {% for link in links %} 19 + <li style="color: {{ link.color }}"> 20 + <a href="{{ link.url }}">{{ link.title }}</a> 21 + </li> 22 + {% endfor %} 23 + </ul> 24 + </div> 25 + <!-- .wrapper --> 26 + </body> 25 27 </html>