From e55474158aa0d64e453025a90b0ac017d456a501 Mon Sep 17 00:00:00 2001 From: Daniel Schwarz Date: Wed, 17 May 2023 20:23:46 -0400 Subject: [PATCH 1/4] Added toot timeline --account_tl console command Displays the timeline of any account (public timeline for users who are not you, public+private for you) Right now it does not display reblogs or replies, this could be configurable if we add more command line flags --- toot/api.py | 31 +++++++++++++++++++++++++++++ toot/commands.py | 51 ++++++++++++++---------------------------------- toot/console.py | 9 +++++++++ 3 files changed, 55 insertions(+), 36 deletions(-) diff --git a/toot/api.py b/toot/api.py index 8557c74..f0857a9 100644 --- a/toot/api.py +++ b/toot/api.py @@ -10,9 +10,33 @@ from toot import App, User, http, CLIENT_NAME, CLIENT_WEBSITE from toot.exceptions import AuthenticationError, ConsoleError from toot.utils import drop_empty_values, str_bool, str_bool_nullable + SCOPES = 'read write follow' +def find_account(app, user, account_name): + if not account_name: + raise ConsoleError("Empty account name given") + + normalized_name = account_name.lstrip("@").lower() + + # Strip @ from accounts on the local instance. The `acct` + # field in account object contains the qualified name for users of other + # instances, but only the username for users of the local instance. This is + # required in order to match the account name below. + if "@" in normalized_name: + [username, instance] = normalized_name.split("@", maxsplit=1) + if instance == app.instance: + normalized_name = username + + response = search(app, user, account_name, type="accounts", resolve=True) + for account in response["accounts"]: + if account["acct"].lower() == normalized_name: + return account + + raise ConsoleError("Account not found") + + def _account_action(app, user, account, action): url = f"/api/v1/accounts/{account}/{action}" return http.post(app, user, url).json() @@ -350,6 +374,13 @@ def conversation_timeline_generator(app, user, limit=20): return _conversation_timeline_generator(app, user, path, params) +def account_timeline_generator(app: App, user: User, account_name: str, replies=False, reblogs=False, limit=20): + account = find_account(app, user, account_name) + path = f"/api/v1/accounts/{account['id']}/statuses" + params = {"limit": limit, "exclude_replies": not replies, "exclude_reblogs": not reblogs} + return _timeline_generator(app, user, path, params) + + def timeline_list_generator(app, user, list_id, limit=20): path = f"/api/v1/timelines/list/{list_id}" return _timeline_generator(app, user, path, {'limit': limit}) diff --git a/toot/commands.py b/toot/commands.py index 0cc92d2..9df6a6f 100644 --- a/toot/commands.py +++ b/toot/commands.py @@ -16,8 +16,8 @@ from toot.utils import args_get_instance, delete_tmp_status_file, editor_input, def get_timeline_generator(app, user, args): # Make sure tag, list and public are not used simultaneously - if len([arg for arg in [args.tag, args.list, args.public] if arg]) > 1: - raise ConsoleError("Only one of --public, --tag, or --list can be used at one time.") + if len([arg for arg in [args.tag, args.list, args.public, args.account_tl] if arg]) > 1: + raise ConsoleError("Only one of --public, --tag, --account_tl, or --list can be used at one time.") if args.local and not (args.public or args.tag): raise ConsoleError("The --local option is only valid alongside --public or --tag.") @@ -35,6 +35,8 @@ def get_timeline_generator(app, user, args): return api.anon_tag_timeline_generator(args.instance, args.tag, limit=args.count) else: return api.tag_timeline_generator(app, user, args.tag, local=args.local, limit=args.count) + elif args.account_tl: + return api.account_timeline_generator(app, user, args.account, limit=args.count) elif args.list: return api.timeline_list_generator(app, user, args.list, limit=args.count) else: @@ -360,49 +362,26 @@ def _do_upload(app, user, file, description, thumbnail): return api.upload_media(app, user, file, description=description, thumbnail=thumbnail) -def find_account(app, user, account_name): - if not account_name: - raise ConsoleError("Empty account name given") - - normalized_name = account_name.lstrip("@").lower() - - # Strip @ from accounts on the local instance. The `acct` - # field in account object contains the qualified name for users of other - # instances, but only the username for users of the local instance. This is - # required in order to match the account name below. - if "@" in normalized_name: - [username, instance] = normalized_name.split("@", maxsplit=1) - if instance == app.instance: - normalized_name = username - - response = api.search(app, user, account_name, type="accounts", resolve=True) - for account in response["accounts"]: - if account["acct"].lower() == normalized_name: - return account - - raise ConsoleError("Account not found") - - def follow(app, user, args): - account = find_account(app, user, args.account) + account = api.find_account(app, user, args.account) api.follow(app, user, account['id']) print_out("✓ You are now following {}".format(args.account)) def unfollow(app, user, args): - account = find_account(app, user, args.account) + account = api.find_account(app, user, args.account) api.unfollow(app, user, account['id']) print_out("✓ You are no longer following {}".format(args.account)) def following(app, user, args): - account = find_account(app, user, args.account) + account = api.find_account(app, user, args.account) response = api.following(app, user, account['id']) print_acct_list(response) def followers(app, user, args): - account = find_account(app, user, args.account) + account = api.find_account(app, user, args.account) response = api.followers(app, user, account['id']) print_acct_list(response) @@ -452,7 +431,7 @@ def list_delete(app, user, args): def list_add(app, user, args): list_id = _get_list_id(app, user, args) - account = find_account(app, user, args.account) + account = api.find_account(app, user, args.account) try: api.add_accounts_to_list(app, user, list_id, [account['id']]) @@ -477,7 +456,7 @@ def list_add(app, user, args): def list_remove(app, user, args): list_id = _get_list_id(app, user, args) - account = find_account(app, user, args.account) + account = api.find_account(app, user, args.account) api.remove_accounts_from_list(app, user, list_id, [account['id']]) print_out(f"✓ Removed account \"{args.account}\"") @@ -490,25 +469,25 @@ def _get_list_id(app, user, args): def mute(app, user, args): - account = find_account(app, user, args.account) + account = api.find_account(app, user, args.account) api.mute(app, user, account['id']) print_out("✓ You have muted {}".format(args.account)) def unmute(app, user, args): - account = find_account(app, user, args.account) + account = api.find_account(app, user, args.account) api.unmute(app, user, account['id']) print_out("✓ {} is no longer muted".format(args.account)) def block(app, user, args): - account = find_account(app, user, args.account) + account = api.find_account(app, user, args.account) api.block(app, user, account['id']) print_out("✓ You are now blocking {}".format(args.account)) def unblock(app, user, args): - account = find_account(app, user, args.account) + account = api.find_account(app, user, args.account) api.unblock(app, user, account['id']) print_out("✓ {} is no longer blocked".format(args.account)) @@ -519,7 +498,7 @@ def whoami(app, user, args): def whois(app, user, args): - account = find_account(app, user, args.account) + account = api.find_account(app, user, args.account) print_account(account) diff --git a/toot/console.py b/toot/console.py index 5c84d97..f1eab39 100644 --- a/toot/console.py +++ b/toot/console.py @@ -238,6 +238,11 @@ common_timeline_args = [ "type": str, "help": "show hashtag timeline (does not require auth)", }), + (["-at", "--account_tl"], { + "action": "store_true", + "default": False, + "help": "show account timeline (requires account name)", + }), (["-l", "--local"], { "action": "store_true", "default": False, @@ -251,6 +256,10 @@ common_timeline_args = [ "type": str, "help": "show timeline for given list.", }), + (["account"], { + "nargs": "?", + "help": "account name, e.g. 'Gargron@mastodon.social'", + }), ] timeline_and_bookmark_args = [ From f7ba208d3ba6a59d878c128b68810ee8a6368678 Mon Sep 17 00:00:00 2001 From: Daniel Schwarz Date: Wed, 17 May 2023 20:23:46 -0400 Subject: [PATCH 2/4] Added personal timeline, which is your own timeline of posts Fixes issue #354 --- toot/tui/app.py | 15 ++++++++++++--- toot/tui/overlays.py | 5 +++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/toot/tui/app.py b/toot/tui/app.py index b49dee2..b9ba275 100644 --- a/toot/tui/app.py +++ b/toot/tui/app.py @@ -6,7 +6,6 @@ from concurrent.futures import ThreadPoolExecutor from toot import api, config, __version__ from toot.console import get_default_visibility from toot.exceptions import ApiError -from toot.commands import find_account from .compose import StatusComposer from .constants import PALETTE @@ -305,7 +304,7 @@ class TUI(urwid.Frame): def _load_accounts(): try: acct = f'@{self.user.username}@{self.user.instance}' - self.account = find_account(self.app, self.user, acct) + self.account = api.find_account(self.app, self.user, acct) return api.following(self.app, self.user, self.account["id"]) except ApiError: # not supported by all Mastodon servers so fail silently if necessary @@ -411,6 +410,8 @@ class TUI(urwid.Frame): lambda x, local: self.goto_notifications()) urwid.connect_signal(menu, "conversation_timeline", lambda x, local: self.goto_conversations()) + urwid.connect_signal(menu, "personal_timeline", + lambda x, local: self.goto_personal_timeline()) urwid.connect_signal(menu, "hashtag_timeline", lambda x, tag, local: self.goto_tag_timeline(tag, local=local)) urwid.connect_signal(menu, "list_timeline", @@ -418,7 +419,7 @@ class TUI(urwid.Frame): self.open_overlay(menu, title="Go to", options=dict( align="center", width=("relative", 60), - valign="middle", height=17 + len(user_timelines) + len(user_lists), + valign="middle", height=18 + len(user_timelines) + len(user_lists), )) def show_help(self): @@ -472,6 +473,14 @@ class TUI(urwid.Frame): ) promise.add_done_callback(lambda *args: self.close_overlay()) + def goto_personal_timeline(self): + account_name = f"{self.user.username}@{self.user.instance}" + + self.timeline_generator = api.account_timeline_generator( + self.app, self.user, account_name, reblogs=True, limit=40) + promise = self.async_load_timeline(is_initial=True, timeline_name=f"personal {account_name}") + promise.add_done_callback(lambda *args: self.close_overlay()) + def goto_list_timeline(self, list_item): self.timeline_generator = api.timeline_list_generator( self.app, self.user, list_item['id'], limit=40) diff --git a/toot/tui/overlays.py b/toot/tui/overlays.py index a9c2442..9582b92 100644 --- a/toot/tui/overlays.py +++ b/toot/tui/overlays.py @@ -102,6 +102,7 @@ class GotoMenu(urwid.ListBox): "bookmark_timeline", "notification_timeline", "conversation_timeline", + "personal_timeline", "list_timeline", ] @@ -126,6 +127,9 @@ class GotoMenu(urwid.ListBox): def _global_public(button): self._emit("public_timeline", False) + def _personal(button): + self._emit("personal_timeline", False) + def _bookmarks(button): self._emit("bookmark_timeline", False) @@ -156,6 +160,7 @@ class GotoMenu(urwid.ListBox): yield Button("Home timeline", on_press=_home) yield Button("Local public timeline", on_press=_local_public) yield Button("Global public timeline", on_press=_global_public) + yield Button("Personal timeline", on_press=_personal) yield Button("Bookmarks", on_press=_bookmarks) yield Button("Notifications", on_press=_notifications) yield Button("Conversations", on_press=_conversations) From 51d60679c35029a302ea59270d83477539af35ad Mon Sep 17 00:00:00 2001 From: Ivan Habunek Date: Thu, 22 Jun 2023 11:16:32 +0200 Subject: [PATCH 3/4] Simplify account timeline options --- toot/commands.py | 7 +++---- toot/console.py | 11 +++-------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/toot/commands.py b/toot/commands.py index 9df6a6f..0a76590 100644 --- a/toot/commands.py +++ b/toot/commands.py @@ -15,9 +15,8 @@ from toot.utils import args_get_instance, delete_tmp_status_file, editor_input, def get_timeline_generator(app, user, args): - # Make sure tag, list and public are not used simultaneously - if len([arg for arg in [args.tag, args.list, args.public, args.account_tl] if arg]) > 1: - raise ConsoleError("Only one of --public, --tag, --account_tl, or --list can be used at one time.") + if len([arg for arg in [args.tag, args.list, args.public, args.account] if arg]) > 1: + raise ConsoleError("Only one of --public, --tag, --account, or --list can be used at one time.") if args.local and not (args.public or args.tag): raise ConsoleError("The --local option is only valid alongside --public or --tag.") @@ -35,7 +34,7 @@ def get_timeline_generator(app, user, args): return api.anon_tag_timeline_generator(args.instance, args.tag, limit=args.count) else: return api.tag_timeline_generator(app, user, args.tag, local=args.local, limit=args.count) - elif args.account_tl: + elif args.account: return api.account_timeline_generator(app, user, args.account, limit=args.count) elif args.list: return api.timeline_list_generator(app, user, args.list, limit=args.count) diff --git a/toot/console.py b/toot/console.py index f1eab39..14433aa 100644 --- a/toot/console.py +++ b/toot/console.py @@ -238,10 +238,9 @@ common_timeline_args = [ "type": str, "help": "show hashtag timeline (does not require auth)", }), - (["-at", "--account_tl"], { - "action": "store_true", - "default": False, - "help": "show account timeline (requires account name)", + (["-a", "--account"], { + "type": str, + "help": "show timeline for the given account", }), (["-l", "--local"], { "action": "store_true", @@ -256,10 +255,6 @@ common_timeline_args = [ "type": str, "help": "show timeline for given list.", }), - (["account"], { - "nargs": "?", - "help": "account name, e.g. 'Gargron@mastodon.social'", - }), ] timeline_and_bookmark_args = [ From dc0d69f14b81735ed40f8f8aeedb6343709620d3 Mon Sep 17 00:00:00 2001 From: Ivan Habunek Date: Thu, 22 Jun 2023 11:21:04 +0200 Subject: [PATCH 4/4] Add changelog --- CHANGELOG.md | 6 ++++++ changelog.yaml | 6 ++++++ docs/changelog.md | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 809fb89..d5fad39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ Changelog +**0.37.0 (TBA)** + +* Add `timeline --account` option to show the account timeline (thanks Dan + Schwarz) +* TUI: Add personal timeline (thanks Dan Schwarz) + **0.36.0 (2023-03-09)** * Move docs from toot.readthedocs.io to toot.bezdomni.net diff --git a/changelog.yaml b/changelog.yaml index ee898d6..3d2e723 100644 --- a/changelog.yaml +++ b/changelog.yaml @@ -1,3 +1,9 @@ +0.37.0: + date: "TBA" + changes: + - "Add `timeline --account` option to show the account timeline (thanks Dan Schwarz)" + - "TUI: Add personal timeline (thanks Dan Schwarz)" + 0.36.0: date: 2023-03-09 changes: diff --git a/docs/changelog.md b/docs/changelog.md index 809fb89..d5fad39 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -3,6 +3,12 @@ Changelog +**0.37.0 (TBA)** + +* Add `timeline --account` option to show the account timeline (thanks Dan + Schwarz) +* TUI: Add personal timeline (thanks Dan Schwarz) + **0.36.0 (2023-03-09)** * Move docs from toot.readthedocs.io to toot.bezdomni.net