From b75939fc4c27c7076d5d2864c0166912ad4ff77a Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Fri, 1 Dec 2023 09:00:43 +0000 Subject: [PATCH] Start of a Monochrome-like Mastodon client. Currently fetches a public timeline, and formats Mono-style post separator lines without any colour. Much yet to do! --- .gitignore | 1 + mastodonochrome | 87 +++++++++++++++++++++++++++++++++++++++++++++++++ text.py | 14 ++++++++ 3 files changed, 102 insertions(+) create mode 100644 .gitignore create mode 100755 mastodonochrome create mode 100644 text.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a348e50 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/__pycache__/ diff --git a/mastodonochrome b/mastodonochrome new file mode 100755 index 0000000..c68528a --- /dev/null +++ b/mastodonochrome @@ -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 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 -- 2.30.2