chiark / gitweb /
Start of a Monochrome-like Mastodon client.
authorSimon Tatham <anakin@pobox.com>
Fri, 1 Dec 2023 09:00:43 +0000 (09:00 +0000)
committerSimon Tatham <anakin@pobox.com>
Fri, 1 Dec 2023 09:00:43 +0000 (09:00 +0000)
Currently fetches a public timeline, and formats Mono-style post
separator lines without any colour. Much yet to do!

.gitignore [new file with mode: 0644]
mastodonochrome [new file with mode: 0755]
text.py [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..a348e50
--- /dev/null
@@ -0,0 +1 @@
+/__pycache__/
diff --git a/mastodonochrome b/mastodonochrome
new file mode 100755 (executable)
index 0000000..c68528a
--- /dev/null
@@ -0,0 +1,87 @@
+#!/usr/bin/env python3
+
+'''
+Textual Mastodon client with a UI inspired by Monochrome BBS.
+'''
+
+import argparse
+import calendar
+import requests
+import string
+import time
+
+import text
+
+class HTTPError(Exception):
+    def __init__(self, response):
+        self.response = response
+
+    def __str__(self):
+        return (f"{self.response.request.method} request "
+                f"for {self.response.request.url} "
+                f"returned status {self.response.status_code}")
+
+class Client:
+    def __init__(self, instance):
+        self.base_url = instance + "/api/v1/"
+        self.log_response = lambda *args, **kws: None
+
+    def enable_debug(self, logfile):
+        logfh = open(logfile, "w")
+        pr = lambda *args, **kws: print(*args, file=logfh, **kws)
+
+        def log_response(rsp):
+            pr("Request: {rsp.request.method} {rsp.request.url}")
+            pr("  Response status: {rsp.status_code}")
+            pr("  Response headers:")
+            for k, v in rsp.headers.items():
+                pr(f"    {k}: {v}")
+
+        self.log_response = log_response
+
+    def get_public(self, path, **params):
+        rsp = requests.get(self.base_url + path, params=params)
+        self.log_response(rsp)
+        if rsp.status_code != 200:
+            raise HTTPError(rsp)
+        return rsp.json()
+
+class Post:
+    def __init__(self, data):
+        self.post_id = data['id']
+
+        date, suffix = data['created_at'].split(".", 1)
+        if suffix.lstrip(string.digits) != "Z":
+            raise ValueError(f"{self.post_id}: bad creation date {date!r}")
+        tm = time.strptime(date, "%Y-%m-%dT%H:%M:%S")
+        self.datestamp = calendar.timegm(tm)
+
+    def text(self):
+        yield text.SeparatorLine(self.datestamp)
+
+class MainUI(Client):
+    def run(self):
+        for item in self.get_public("timelines/public", limit=10):
+            p = Post(item)
+            for thing in p.text():
+                print(thing.render(79))
+
+def main():
+    parser = argparse.ArgumentParser(
+        description=__doc__,
+        formatter_class=argparse.RawDescriptionHelpFormatter)
+    parser.add_argument("--log", help="File to log debug information to.")
+    parser.set_defaults(action=MainUI)
+    args = parser.parse_args()
+
+    instance = "hachyderm.io" # FIXME
+    if "://" not in instance:
+        instance = "https://" + instance
+
+    thing = args.action(instance)
+    if args.log is not None:
+        thing.enable_debug(args.log)
+    thing.run()
+
+if __name__ == '__main__':
+    main()
diff --git a/text.py b/text.py
new file mode 100644 (file)
index 0000000..3fb55f4
--- /dev/null
+++ b/text.py
@@ -0,0 +1,14 @@
+# Represent colourised terminal text in a width-independent form.
+
+import time
+
+class SeparatorLine:
+    def __init__(self, timestamp):
+        self.timestamp = timestamp
+
+    def render(self, width):
+        date = time.strftime("%a %b %e %H:%M:%S %Y",
+                             time.localtime(self.timestamp))
+        # FIXME: colours
+        suffix = "[" + date + "]--"
+        return "-" * (width - len(suffix)) + suffix