Retro Bulletin Board Systems on atproto. Web app and TUI. atbbs.xyz
python tui atproto bbs
at master 170 lines 3.2 kB view raw
1from dataclasses import dataclass 2 3 4class AtUri: 5 """Parsed AT URI with did, collection, and rkey fields.""" 6 7 __slots__ = ("did", "collection", "rkey") 8 9 def __init__(self, did: str, collection: str, rkey: str): 10 self.did = did 11 self.collection = collection 12 self.rkey = rkey 13 14 @classmethod 15 def parse(cls, uri: str) -> "AtUri": 16 parts = uri.split("/") 17 return cls(parts[2], parts[3], parts[4]) 18 19 def __str__(self) -> str: 20 return f"at://{self.did}/{self.collection}/{self.rkey}" 21 22 def __eq__(self, other): 23 if isinstance(other, AtUri): 24 return str(self) == str(other) 25 if isinstance(other, str): 26 return str(self) == other 27 return NotImplemented 28 29 def __hash__(self): 30 return hash(str(self)) 31 32 33# errors 34 35 36class BBSNotFoundError(Exception): 37 """Handle could not be resolved.""" 38 39 40class NoBBSError(Exception): 41 """Handle resolved but has no site record.""" 42 43 44class NetworkError(Exception): 45 """Slingshot or Constellation is unreachable.""" 46 47 48class AuthError(Exception): 49 """Session expired and token refresh failed.""" 50 51 52# microcosm response types 53 54 55@dataclass 56class MiniDoc: 57 did: str 58 handle: str 59 pds: str | None = None 60 signing_key: str | None = None 61 62 63@dataclass 64class BacklinkRef: 65 """A single backlink from Constellation (record locator, no content).""" 66 67 did: str 68 collection: str 69 rkey: str 70 71 @property 72 def uri(self) -> str: 73 return f"at://{self.did}/{self.collection}/{self.rkey}" 74 75 76@dataclass 77class BacklinksResponse: 78 """Full Constellation getBacklinks response.""" 79 80 total: int 81 records: list[BacklinkRef] 82 cursor: str | None = None 83 84 85@dataclass 86class Record: 87 """A single record from Slingshot getRecord.""" 88 89 uri: str 90 cid: str 91 value: dict 92 93 94# lexicons 95 96 97@dataclass 98class Board: 99 """xyz.atboards.board — a subforum defined by the sysop.""" 100 101 slug: str 102 name: str 103 description: str 104 created_at: str 105 updated_at: str | None = None 106 107 108@dataclass 109class News: 110 """xyz.atboards.news — a sysop announcement.""" 111 112 tid: str 113 site_uri: str 114 title: str 115 body: str 116 created_at: str 117 118 119@dataclass 120class Site: 121 """xyz.atboards.site/self — the BBS front door.""" 122 123 name: str 124 description: str 125 intro: str 126 boards: list[Board] 127 banned_dids: set[str] 128 hidden_posts: set[str] 129 created_at: str 130 updated_at: str | None = None 131 132 def is_banned(self, did: str) -> bool: 133 return did in self.banned_dids 134 135 136@dataclass 137class Thread: 138 """xyz.atboards.thread — a user's thread on a board.""" 139 140 uri: str 141 board_uri: str 142 title: str 143 body: str 144 created_at: str 145 author: MiniDoc 146 updated_at: str | None = None 147 attachments: list[dict] | None = None 148 149 150@dataclass 151class Reply: 152 """xyz.atboards.reply — a user's reply to a thread.""" 153 154 uri: str 155 subject_uri: str 156 body: str 157 created_at: str 158 author: MiniDoc 159 updated_at: str | None = None 160 attachments: list[dict] | None = None 161 quote: str | None = None 162 163 164@dataclass 165class BBS: 166 """Fully resolved BBS: resolve_bbs(handle).""" 167 168 identity: MiniDoc 169 site: Site 170 news: list[News]