Retro Bulletin Board Systems on atproto. Web app and TUI. atbbs.xyz
python tui atproto bbs
at master 123 lines 3.5 kB view raw
1import webbrowser 2 3from textual.app import ComposeResult 4from textual.widget import Widget 5from textual.widgets import Markdown, Static 6 7from core.models import AtUri 8from core.util import format_datetime_local as format_datetime 9 10 11class AttachmentLink(Static, can_focus=True): 12 """A clickable attachment that opens in a browser.""" 13 14 DEFAULT_CSS = """ 15 AttachmentLink { 16 color: #8a8a8a; 17 margin-top: 1; 18 width: auto; 19 } 20 AttachmentLink:hover { 21 color: #a3a3a3; 22 } 23 AttachmentLink:focus { 24 color: #e5e5e5; 25 } 26 """ 27 28 def __init__(self, name: str, url: str, **kwargs) -> None: 29 super().__init__(f"[{name}]", markup=False, **kwargs) 30 self._url = url 31 32 def on_click(self) -> None: 33 webbrowser.open(self._url) 34 35 def key_enter(self) -> None: 36 webbrowser.open(self._url) 37 38 39class Post(Widget, can_focus=True): 40 """A post card showing author, date, optional title, body, and attachments.""" 41 42 DEFAULT_CSS = """ 43 Post { 44 height: auto; 45 padding: 1 2; 46 margin: 0 0 1 0; 47 border: solid #262626; 48 background: #1f1f1f; 49 } 50 Post:focus { 51 border: solid #525252; 52 } 53 Post .post-meta { 54 color: #8a8a8a; 55 margin-bottom: 1; 56 } 57 Post .post-title { 58 color: #e5e5e5; 59 text-style: bold; 60 margin-bottom: 1; 61 } 62 Post .post-body { 63 color: #a3a3a3; 64 } 65 Post .post-attachment { 66 color: #8a8a8a; 67 margin-top: 1; 68 } 69 Post .post-parent { 70 color: #8a8a8a; 71 border-left: solid #525252; 72 padding-left: 2; 73 margin-bottom: 1; 74 } 75 """ 76 77 def __init__( 78 self, 79 author: str, 80 date: str, 81 body: str, 82 title: str | None = None, 83 author_did: str | None = None, 84 author_pds: str | None = None, 85 record_uri: str | None = None, 86 collection: str | None = None, 87 attachments: list[dict] | None = None, 88 parent_preview: str | None = None, 89 **kwargs, 90 ) -> None: 91 super().__init__(**kwargs) 92 self._author = author 93 self._date = format_datetime(date) 94 self._title = title 95 self._body = body 96 self.author_did = author_did 97 self.author_pds = author_pds 98 self.record_uri = record_uri 99 self.collection = collection 100 self.attachments = attachments or [] 101 self._parent_preview = parent_preview 102 103 @property 104 def rkey(self) -> str | None: 105 if self.record_uri: 106 return AtUri.parse(self.record_uri).rkey 107 return None 108 109 def compose(self) -> ComposeResult: 110 yield Static(f"{self._author} {self._date}", classes="post-meta", markup=False) 111 if self._title: 112 yield Static(self._title, classes="post-title", markup=False) 113 if self._parent_preview: 114 yield Markdown(self._parent_preview, classes="post-parent") 115 yield Markdown(self._body, classes="post-body") 116 for attachment in self.attachments: 117 name = attachment.get("name", "file") 118 cid = attachment.get("file", {}).get("ref", {}).get("$link", "") 119 if cid and self.author_pds and self.author_did: 120 url = f"{self.author_pds}/xrpc/com.atproto.sync.getBlob?did={self.author_did}&cid={cid}" 121 yield AttachmentLink(name, url) 122 else: 123 yield Static(f"[{name}]", classes="post-attachment", markup=False)