From 7e5632f3a502184bdb3fbc9acee57fef01c78305 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Sat, 2 Dec 2023 18:59:50 +0000 Subject: [PATCH] First cut at interleaving home timeline with mentions. It took so many tries to get that merge algorithm working that I managed to get '429 Too many requests' from the server. I hope that's sorted out tomorrow. Perhaps I should call it a night for now. --- client.py | 7 +++++- mastodonochrome | 67 +++++++++++++++++++++++++++++++------------------ 2 files changed, 49 insertions(+), 25 deletions(-) diff --git a/client.py b/client.py index 3e55434..92569ef 100644 --- a/client.py +++ b/client.py @@ -88,7 +88,12 @@ class Client: params.setdefault('limit', 32) while True: links = {} - data = self.method(requests.get, path, base, params, links) + try: + data = self.method(requests.get, path, base, params, links) + except HTTPError as e: + if e.response.status_code == 429: + # Blocked for too many requests, oops + break yield from data if 'next' not in links: break diff --git a/mastodonochrome b/mastodonochrome index 823681d..4e50a51 100755 --- a/mastodonochrome +++ b/mastodonochrome @@ -5,6 +5,7 @@ Textual Mastodon client with a UI inspired by Monochrome BBS. ''' import argparse +import itertools import sys import unittest @@ -12,26 +13,46 @@ import client import cursesclient import login -class TimelineUI(client.Client): - def run(self): - for item in reversed(self.get("timelines/home", limit=20)): - p = client.Status(item) - for thing in p.text(): - for line in thing.render(80): - print(line.ecma48()) +class CombinedUI(client.Client): + def combined_feed(self): + feeds = [ + ((item['created_at'], item) + for item in self.get_incremental("timelines/home")), + ((item['created_at'], item['status']) + for item in self.get_incremental("notifications") + if item['type'] == 'mention'), + ] -class MentionsUI(client.Client): - def run(self): - g = self.get_incremental("notifications") - things = [] - for item in g: - if item['type'] != 'mention': - continue - p = client.Status(item['status']) - things.append(p) - if len(things) >= 40: + items = [] + + nexts = [None for _ in feeds] + + while True: + next_item = None + + for i in range(len(feeds)): + if feeds[i] is not None and nexts[i] is None: + try: + nexts[i] = next(feeds[i]) + except StopIteration: + feeds[i] = None + if nexts[i] is not None: + if next_item is None: + next_item = nexts[i] + elif next_item[0] < nexts[i][0]: + next_item = nexts[i] + if next_item is None: break - for p in reversed(things): + yield next_item[1] + for i in range(len(feeds)): + if (nexts[i] is not None and + nexts[i][1]['id'] == next_item[1]['id']): + nexts[i] = None + + def run(self): + items = list(itertools.islice(self.combined_feed(), 0, 100)) + for item in reversed(items): + p = client.Status(item) for thing in p.text(): for line in thing.render(80): print(line.ecma48()) @@ -51,12 +72,10 @@ def main(): parser.add_argument("--log", help="File to log debug information to.") parser.add_argument("--test", nargs=argparse.REMAINDER, help="Run unit tests.") - parser.add_argument("--timeline", action="store_const", dest="action", - const=TimelineUI, help="Temporary mode to fetch " - "the user's timeline and print it on the terminal.") - parser.add_argument("--mentions", action="store_const", dest="action", - const=MentionsUI, help="Temporary mode to fetch " - "the user's mentions and print them on the terminal.") + parser.add_argument("--combined", action="store_const", dest="action", + const=CombinedUI, help="Temporary mode to fetch " + "the user's timeline and mentions, interleave them, " + "and print the result on the terminal.") parser.add_argument("--login", action="store_const", dest="action", const=login.LoginUI, help="Log in to a user account.") parser.set_defaults(action=cursesclient.CursesUI) -- 2.30.2