|
|
|
@ -7,7 +7,8 @@ import sqlite3 |
|
|
|
import time |
|
|
|
import time |
|
|
|
from contextlib import contextmanager |
|
|
|
from contextlib import contextmanager |
|
|
|
from pathlib import Path |
|
|
|
from pathlib import Path |
|
|
|
from typing import Any, Generator, Iterable, TypedDict, cast |
|
|
|
from collections.abc import Generator, Iterable |
|
|
|
|
|
|
|
from typing import Any, TypedDict, cast |
|
|
|
|
|
|
|
|
|
|
|
SCHEMA_VERSION = 2 |
|
|
|
SCHEMA_VERSION = 2 |
|
|
|
|
|
|
|
|
|
|
|
@ -164,7 +165,7 @@ CREATE INDEX IF NOT EXISTS idx_feed_views_event ON feed_views(event_id); |
|
|
|
|
|
|
|
|
|
|
|
class Database: |
|
|
|
class Database: |
|
|
|
def __init__(self, path: Path) -> None: |
|
|
|
def __init__(self, path: Path) -> None: |
|
|
|
self.path = path |
|
|
|
self.path: Path = path |
|
|
|
path.parent.mkdir(parents=True, exist_ok=True) |
|
|
|
path.parent.mkdir(parents=True, exist_ok=True) |
|
|
|
self._conn: sqlite3.Connection | None = None |
|
|
|
self._conn: sqlite3.Connection | None = None |
|
|
|
|
|
|
|
|
|
|
|
@ -233,7 +234,12 @@ class Database: |
|
|
|
) -> None: |
|
|
|
) -> None: |
|
|
|
"""Insert or replace event; expand tags into tags table.""" |
|
|
|
"""Insert or replace event; expand tags into tags table.""" |
|
|
|
eid = ev["id"] |
|
|
|
eid = ev["id"] |
|
|
|
tags = ev.get("tags") or [] |
|
|
|
raw_tags = ev.get("tags") |
|
|
|
|
|
|
|
tags: list[list[str]] = ( |
|
|
|
|
|
|
|
cast(list[list[str]], raw_tags) |
|
|
|
|
|
|
|
if isinstance(raw_tags, list) |
|
|
|
|
|
|
|
else [] |
|
|
|
|
|
|
|
) |
|
|
|
tags_json = json.dumps(tags, ensure_ascii=False) |
|
|
|
tags_json = json.dumps(tags, ensure_ascii=False) |
|
|
|
raw = json.dumps(ev, ensure_ascii=False) |
|
|
|
raw = json.dumps(ev, ensure_ascii=False) |
|
|
|
with self.write_lock() as c: |
|
|
|
with self.write_lock() as c: |
|
|
|
@ -314,7 +320,7 @@ class Database: |
|
|
|
try: |
|
|
|
try: |
|
|
|
ev = json.loads(raw) |
|
|
|
ev = json.loads(raw) |
|
|
|
if isinstance(ev, dict): |
|
|
|
if isinstance(ev, dict): |
|
|
|
return ev |
|
|
|
return cast(dict[str, Any], ev) |
|
|
|
except json.JSONDecodeError: |
|
|
|
except json.JSONDecodeError: |
|
|
|
pass |
|
|
|
pass |
|
|
|
return { |
|
|
|
return { |
|
|
|
@ -324,7 +330,7 @@ class Database: |
|
|
|
"kind": row["kind"], |
|
|
|
"kind": row["kind"], |
|
|
|
"content": row["content"] or "", |
|
|
|
"content": row["content"] or "", |
|
|
|
"sig": row["sig"], |
|
|
|
"sig": row["sig"], |
|
|
|
"tags": json.loads(row["tags_json"] or "[]"), |
|
|
|
"tags": cast(list[list[str]], json.loads(row["tags_json"] or "[]")), |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
def get_event(self, event_id: str) -> StoredEventRow | None: |
|
|
|
def get_event(self, event_id: str) -> StoredEventRow | None: |
|
|
|
@ -411,7 +417,7 @@ class Database: |
|
|
|
"kind": row["kind"], |
|
|
|
"kind": row["kind"], |
|
|
|
"content": row["content"], |
|
|
|
"content": row["content"], |
|
|
|
"sig": row["sig"], |
|
|
|
"sig": row["sig"], |
|
|
|
"tags": json.loads(row["tags_json"] or "[]"), |
|
|
|
"tags": cast(list[list[str]], json.loads(row["tags_json"] or "[]")), |
|
|
|
"source_relay": row["source_relay"], |
|
|
|
"source_relay": row["source_relay"], |
|
|
|
} |
|
|
|
} |
|
|
|
) |
|
|
|
) |
|
|
|
@ -453,20 +459,25 @@ class Database: |
|
|
|
try: |
|
|
|
try: |
|
|
|
data = json.loads(content) |
|
|
|
data = json.loads(content) |
|
|
|
if isinstance(data, list): |
|
|
|
if isinstance(data, list): |
|
|
|
for x in data: |
|
|
|
for x in cast(list[object], data): |
|
|
|
if isinstance(x, str) and len(x) == 64: |
|
|
|
if isinstance(x, str) and len(x) == 64: |
|
|
|
out.add(x.lower()) |
|
|
|
out.add(x.lower()) |
|
|
|
elif isinstance(x, dict) and "pubkey" in x: |
|
|
|
elif isinstance(x, dict) and "pubkey" in x: |
|
|
|
pk = str(x["pubkey"]) |
|
|
|
xd = cast(dict[str, object], x) |
|
|
|
|
|
|
|
pk = str(xd.get("pubkey", "")) |
|
|
|
if len(pk) == 64: |
|
|
|
if len(pk) == 64: |
|
|
|
out.add(pk.lower()) |
|
|
|
out.add(pk.lower()) |
|
|
|
except json.JSONDecodeError: |
|
|
|
except json.JSONDecodeError: |
|
|
|
pass |
|
|
|
pass |
|
|
|
try: |
|
|
|
try: |
|
|
|
tags = json.loads(row["tags_json"] or "[]") |
|
|
|
tags_raw = json.loads(row["tags_json"] or "[]") |
|
|
|
for t in tags: |
|
|
|
if isinstance(tags_raw, list): |
|
|
|
if t and t[0] == "p" and len(t) > 1 and len(t[1]) == 64: |
|
|
|
for t_obj in cast(list[object], tags_raw): |
|
|
|
out.add(str(t[1]).lower()) |
|
|
|
if not isinstance(t_obj, list) or not t_obj: |
|
|
|
|
|
|
|
continue |
|
|
|
|
|
|
|
row = cast(list[object], t_obj) |
|
|
|
|
|
|
|
if str(row[0]) == "p" and len(row) > 1 and len(str(row[1])) == 64: |
|
|
|
|
|
|
|
out.add(str(row[1]).lower()) |
|
|
|
except json.JSONDecodeError: |
|
|
|
except json.JSONDecodeError: |
|
|
|
pass |
|
|
|
pass |
|
|
|
return out |
|
|
|
return out |
|
|
|
@ -512,7 +523,7 @@ class Database: |
|
|
|
"kind": r["kind"], |
|
|
|
"kind": r["kind"], |
|
|
|
"content": r["content"], |
|
|
|
"content": r["content"], |
|
|
|
"sig": r["sig"], |
|
|
|
"sig": r["sig"], |
|
|
|
"tags": json.loads(r["tags_json"] or "[]"), |
|
|
|
"tags": cast(list[list[str]], json.loads(r["tags_json"] or "[]")), |
|
|
|
} |
|
|
|
} |
|
|
|
for r in cur |
|
|
|
for r in cur |
|
|
|
] |
|
|
|
] |
|
|
|
@ -527,7 +538,7 @@ class Database: |
|
|
|
""", |
|
|
|
""", |
|
|
|
(q, q, q, limit), |
|
|
|
(q, q, q, limit), |
|
|
|
) |
|
|
|
) |
|
|
|
rows = [] |
|
|
|
rows: list[dict[str, Any]] = [] |
|
|
|
for row in cur: |
|
|
|
for row in cur: |
|
|
|
rows.append( |
|
|
|
rows.append( |
|
|
|
{ |
|
|
|
{ |
|
|
|
@ -537,7 +548,7 @@ class Database: |
|
|
|
"kind": row["kind"], |
|
|
|
"kind": row["kind"], |
|
|
|
"content": row["content"], |
|
|
|
"content": row["content"], |
|
|
|
"sig": row["sig"], |
|
|
|
"sig": row["sig"], |
|
|
|
"tags": json.loads(row["tags_json"] or "[]"), |
|
|
|
"tags": cast(list[list[str]], json.loads(row["tags_json"] or "[]")), |
|
|
|
} |
|
|
|
} |
|
|
|
) |
|
|
|
) |
|
|
|
return rows |
|
|
|
return rows |
|
|
|
@ -558,7 +569,7 @@ class Database: |
|
|
|
"kind": r["kind"], |
|
|
|
"kind": r["kind"], |
|
|
|
"content": r["content"], |
|
|
|
"content": r["content"], |
|
|
|
"sig": r["sig"], |
|
|
|
"sig": r["sig"], |
|
|
|
"tags": json.loads(r["tags_json"] or "[]"), |
|
|
|
"tags": cast(list[list[str]], json.loads(r["tags_json"] or "[]")), |
|
|
|
} |
|
|
|
} |
|
|
|
for r in cur |
|
|
|
for r in cur |
|
|
|
] |
|
|
|
] |
|
|
|
@ -599,7 +610,7 @@ class Database: |
|
|
|
|
|
|
|
|
|
|
|
def get_latest_kind0_profiles(self, pubkeys: Iterable[str]) -> dict[str, Kind0ProfileSummary]: |
|
|
|
def get_latest_kind0_profiles(self, pubkeys: Iterable[str]) -> dict[str, Kind0ProfileSummary]: |
|
|
|
"""Most recent kind-0 ``content`` per pubkey (lowercase hex keys).""" |
|
|
|
"""Most recent kind-0 ``content`` per pubkey (lowercase hex keys).""" |
|
|
|
pks = [p.lower() for p in pubkeys if isinstance(p, str) and len(p) == 64] |
|
|
|
pks = [p.lower() for p in pubkeys if len(p) == 64] |
|
|
|
if not pks: |
|
|
|
if not pks: |
|
|
|
return {} |
|
|
|
return {} |
|
|
|
placeholders = ",".join("?" * len(pks)) |
|
|
|
placeholders = ",".join("?" * len(pks)) |
|
|
|
|