tangled
alpha
login
or
join now
ligo.at
/
core
6
fork
atom
decentralized and customizable links page on top of atproto
ligo.at
atproto
link-in-bio
python
uv
6
fork
atom
overview
issues
2
pulls
pipelines
move kv around
nauta.one
5 months ago
76766e36
27cb851a
+62
-33
7 changed files
expand all
collapse all
unified
split
src
atproto
__init__.py
kv.py
db.py
main.py
oauth.py
schema.sql
templates
login.html
+20
-8
src/atproto/__init__.py
···
3
from typing import Any
4
import httpx
5
6
-
from ..db import KV
7
8
from .validator import is_valid_authserver_meta
9
from ..security import is_safe_url
···
26
return regex_match(DID_REGEX, did) is not None
27
28
29
-
def resolve_identity(query: str, didkv: KV) -> tuple[str, str, dict[str, Any]] | None:
0
0
0
30
"""Resolves an identity to a DID, handle and DID document, verifies handles bi directionally."""
31
32
if is_valid_handle(query):
33
-
handle = query
34
did = resolve_did_from_handle(handle, didkv)
35
if not did:
36
return None
···
62
handles: list[str] = []
63
for aka in doc.get("alsoKnownAs", []):
64
if aka.startswith("at://"):
65
-
handle = aka[5:]
66
if is_valid_handle(handle):
67
handles.append(handle)
68
return handles
···
77
return None
78
79
80
-
def resolve_did_from_handle(handle: str, kv: KV, reload: bool = False) -> str | None:
0
0
0
0
81
"""Returns the DID for a given handle"""
82
83
if not is_valid_handle(handle):
···
114
return None
115
116
117
-
def resolve_pds_from_did(did: DID, kv: KV, reload: bool = False) -> PdsUrl | None:
0
0
0
0
118
pds = kv.get(did)
119
if pds is not None and not reload:
120
print(f"returning cached pds for {did}")
···
150
151
def resolve_authserver_from_pds(
152
pds_url: PdsUrl,
153
-
kv: KV,
154
reload: bool = False,
155
) -> AuthserverUrl | None:
156
"""Returns the authserver URL for the PDS."""
···
189
repo: str,
190
collection: str,
191
record: str,
0
192
) -> dict[str, Any] | None:
193
"""Retrieve record from PDS. Verifies type is the same as collection name."""
194
response = httpx.get(
···
198
return None
199
parsed = response.json()
200
value: dict[str, Any] = parsed["value"]
201
-
if value["$type"] != collection:
202
return None
203
del value["$type"]
204
return value
···
3
from typing import Any
4
import httpx
5
6
+
from .kv import KV, nokv
7
8
from .validator import is_valid_authserver_meta
9
from ..security import is_safe_url
···
26
return regex_match(DID_REGEX, did) is not None
27
28
29
+
def resolve_identity(
30
+
query: str,
31
+
didkv: KV = nokv,
32
+
) -> tuple[str, str, dict[str, Any]] | None:
33
"""Resolves an identity to a DID, handle and DID document, verifies handles bi directionally."""
34
35
if is_valid_handle(query):
36
+
handle = query.lower()
37
did = resolve_did_from_handle(handle, didkv)
38
if not did:
39
return None
···
65
handles: list[str] = []
66
for aka in doc.get("alsoKnownAs", []):
67
if aka.startswith("at://"):
68
+
handle = aka[5:].lower()
69
if is_valid_handle(handle):
70
handles.append(handle)
71
return handles
···
80
return None
81
82
83
+
def resolve_did_from_handle(
84
+
handle: str,
85
+
kv: KV = nokv,
86
+
reload: bool = False,
87
+
) -> str | None:
88
"""Returns the DID for a given handle"""
89
90
if not is_valid_handle(handle):
···
121
return None
122
123
124
+
def resolve_pds_from_did(
125
+
did: DID,
126
+
kv: KV = nokv,
127
+
reload: bool = False,
128
+
) -> PdsUrl | None:
129
pds = kv.get(did)
130
if pds is not None and not reload:
131
print(f"returning cached pds for {did}")
···
161
162
def resolve_authserver_from_pds(
163
pds_url: PdsUrl,
164
+
kv: KV = nokv,
165
reload: bool = False,
166
) -> AuthserverUrl | None:
167
"""Returns the authserver URL for the PDS."""
···
200
repo: str,
201
collection: str,
202
record: str,
203
+
type: str | None = None,
204
) -> dict[str, Any] | None:
205
"""Retrieve record from PDS. Verifies type is the same as collection name."""
206
response = httpx.get(
···
210
return None
211
parsed = response.json()
212
value: dict[str, Any] = parsed["value"]
213
+
if value["$type"] != (type or collection):
214
return None
215
del value["$type"]
216
return value
+25
src/atproto/kv.py
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
from abc import ABC, abstractmethod
2
+
from typing import override
3
+
4
+
5
+
class KV(ABC):
6
+
@abstractmethod
7
+
def get(self, key: str) -> str | None:
8
+
pass
9
+
10
+
@abstractmethod
11
+
def set(self, key: str, value: str):
12
+
pass
13
+
14
+
15
+
class NoKV(KV):
16
+
@override
17
+
def get(self, key: str) -> str | None:
18
+
return None
19
+
20
+
@override
21
+
def set(self, key: str, value: str):
22
+
pass
23
+
24
+
25
+
nokv = NoKV()
+4
-13
src/db.py
···
1
-
from abc import ABC, abstractmethod
2
from typing import override
3
from flask import Flask, g
4
5
import sqlite3
6
from sqlite3 import Connection
7
0
8
9
-
class KV(ABC):
10
-
@abstractmethod
11
-
def get(self, key: str) -> str | None:
12
-
pass
13
14
-
@abstractmethod
15
-
def set(self, key: str, value: str):
16
-
pass
17
-
18
-
19
-
class Keyval(KV):
20
db: Connection
21
prefix: str
22
23
-
def __init__(self, app: Flask, prefix: str):
24
-
self.db = get_db(app)
25
self.prefix = prefix
26
27
@override
···
0
1
from typing import override
2
from flask import Flask, g
3
4
import sqlite3
5
from sqlite3 import Connection
6
7
+
from .atproto.kv import KV as BaseKV
8
0
0
0
0
9
10
+
class KV(BaseKV):
0
0
0
0
0
11
db: Connection
12
prefix: str
13
14
+
def __init__(self, app: Connection | Flask, prefix: str):
15
+
self.db = app if isinstance(app, Connection) else get_db(app)
16
self.prefix = prefix
17
18
@override
+5
-5
src/main.py
···
10
resolve_pds_from_did,
11
)
12
from .atproto.oauth import pds_authed_req
13
-
from .db import Keyval, close_db_connection, init_db
14
from .oauth import get_auth_session, oauth, save_auth_session
15
from .types import OAuthSession
16
···
52
@app.get("/@<string:handle>")
53
def page_profile_with_handle(handle: str):
54
reload = request.args.get("reload") is not None
55
-
kv = Keyval(app, "did_from_handle")
56
did = resolve_did_from_handle(handle, kv, reload=reload)
57
if did is None:
58
return "did not found", 404
···
60
61
62
def page_profile(did: str, reload: bool = False):
63
-
kv = Keyval(app, "pds_from_did")
64
pds = resolve_pds_from_did(did, kv, reload=reload)
65
if pds is None:
66
return "pds not found", 404
···
193
194
195
def load_links(pds: str, did: str, reload: bool = False) -> list[dict[str, str]] | None:
196
-
kv = Keyval(app, "links_from_did")
197
links = kv.get(did)
198
199
if links is not None and not reload:
···
215
did: str,
216
reload: bool = False,
217
) -> tuple[tuple[str, str] | None, bool]:
218
-
kv = Keyval(app, "profile_from_did")
219
profile = kv.get(did)
220
221
if profile is not None and not reload:
···
10
resolve_pds_from_did,
11
)
12
from .atproto.oauth import pds_authed_req
13
+
from .db import KV, close_db_connection, init_db
14
from .oauth import get_auth_session, oauth, save_auth_session
15
from .types import OAuthSession
16
···
52
@app.get("/@<string:handle>")
53
def page_profile_with_handle(handle: str):
54
reload = request.args.get("reload") is not None
55
+
kv = KV(app, "did_from_handle")
56
did = resolve_did_from_handle(handle, kv, reload=reload)
57
if did is None:
58
return "did not found", 404
···
60
61
62
def page_profile(did: str, reload: bool = False):
63
+
kv = KV(app, "pds_from_did")
64
pds = resolve_pds_from_did(did, kv, reload=reload)
65
if pds is None:
66
return "pds not found", 404
···
193
194
195
def load_links(pds: str, did: str, reload: bool = False) -> list[dict[str, str]] | None:
196
+
kv = KV(app, "links_from_did")
197
links = kv.get(did)
198
199
if links is not None and not reload:
···
215
did: str,
216
reload: bool = False,
217
) -> tuple[tuple[str, str] | None, bool]:
218
+
kv = KV(app, "profile_from_did")
219
profile = kv.get(did)
220
221
if profile is not None and not reload:
+7
-5
src/oauth.py
···
6
7
import json
8
9
-
from .db import Keyval
10
11
from .atproto import (
12
is_valid_did,
···
30
if not username:
31
return redirect(url_for("page_login"), 303)
32
33
-
pdskv = Keyval(current_app, "authserver_from_pds")
0
34
35
if is_valid_handle(username) or is_valid_did(username):
36
login_hint = username
37
-
kv = Keyval(current_app, "did_from_handle")
38
identity = resolve_identity(username, didkv=kv)
39
if identity is None:
40
return "couldnt resolve identity", 500
···
139
140
row = auth_request
141
142
-
didkv = Keyval(current_app, "did_from_handle")
143
-
authserverkv = Keyval(current_app, "authserver_from_pds")
0
144
145
if row.did:
146
# If we started with an account identifier, this is simple
···
6
7
import json
8
9
+
from .db import KV, get_db
10
11
from .atproto import (
12
is_valid_did,
···
30
if not username:
31
return redirect(url_for("page_login"), 303)
32
33
+
db = get_db(current_app)
34
+
pdskv = KV(db, "authserver_from_pds")
35
36
if is_valid_handle(username) or is_valid_did(username):
37
login_hint = username
38
+
kv = KV(db, "did_from_handle")
39
identity = resolve_identity(username, didkv=kv)
40
if identity is None:
41
return "couldnt resolve identity", 500
···
140
141
row = auth_request
142
143
+
db = get_db(current_app)
144
+
didkv = KV(db, "did_from_handle")
145
+
authserverkv = KV(db, "authserver_from_pds")
146
147
if row.did:
148
# If we started with an account identifier, this is simple
-1
src/schema.sql
···
1
-
drop table if exists keyval;
2
create table if not exists keyval (
3
prefix text not null,
4
key text not null,
···
0
1
create table if not exists keyval (
2
prefix text not null,
3
key text not null,
+1
-1
src/templates/login.html
···
19
<form action="{{ url_for('auth_login') }}" method="post">
20
<label>
21
<span>Handle</span>
22
-
<input type="text" name="username" placeholder="username.example.com" required />
23
</label>
24
<span class="caption">
25
Use your AT Protocol handle to log in.
···
19
<form action="{{ url_for('auth_login') }}" method="post">
20
<label>
21
<span>Handle</span>
22
+
<input type="text" name="username" placeholder="username.example.com" autocapitalize="off" autocomplete="off" spellcheck="false" required />
23
</label>
24
<span class="caption">
25
Use your AT Protocol handle to log in.