mirror of
https://github.com/ihabunek/toot.git
synced 2024-11-03 04:17:21 -05:00
Merge pull request #357
This commit is contained in:
commit
70c9eec55e
@ -3,6 +3,12 @@ Changelog
|
|||||||
|
|
||||||
<!-- Do not edit. This file is automatically generated from changelog.yaml.-->
|
<!-- Do not edit. This file is automatically generated from changelog.yaml.-->
|
||||||
|
|
||||||
|
**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)**
|
**0.36.0 (2023-03-09)**
|
||||||
|
|
||||||
* Move docs from toot.readthedocs.io to toot.bezdomni.net
|
* Move docs from toot.readthedocs.io to toot.bezdomni.net
|
||||||
|
@ -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:
|
0.36.0:
|
||||||
date: 2023-03-09
|
date: 2023-03-09
|
||||||
changes:
|
changes:
|
||||||
|
@ -3,6 +3,12 @@ Changelog
|
|||||||
|
|
||||||
<!-- Do not edit. This file is automatically generated from changelog.yaml.-->
|
<!-- Do not edit. This file is automatically generated from changelog.yaml.-->
|
||||||
|
|
||||||
|
**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)**
|
**0.36.0 (2023-03-09)**
|
||||||
|
|
||||||
* Move docs from toot.readthedocs.io to toot.bezdomni.net
|
* Move docs from toot.readthedocs.io to toot.bezdomni.net
|
||||||
|
31
toot/api.py
31
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.exceptions import AuthenticationError, ConsoleError
|
||||||
from toot.utils import drop_empty_values, str_bool, str_bool_nullable
|
from toot.utils import drop_empty_values, str_bool, str_bool_nullable
|
||||||
|
|
||||||
|
|
||||||
SCOPES = 'read write follow'
|
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 @<instance_name> 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):
|
def _account_action(app, user, account, action):
|
||||||
url = f"/api/v1/accounts/{account}/{action}"
|
url = f"/api/v1/accounts/{account}/{action}"
|
||||||
return http.post(app, user, url).json()
|
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)
|
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):
|
def timeline_list_generator(app, user, list_id, limit=20):
|
||||||
path = f"/api/v1/timelines/list/{list_id}"
|
path = f"/api/v1/timelines/list/{list_id}"
|
||||||
return _timeline_generator(app, user, path, {'limit': limit})
|
return _timeline_generator(app, user, path, {'limit': limit})
|
||||||
|
@ -15,9 +15,8 @@ from toot.utils import args_get_instance, delete_tmp_status_file, editor_input,
|
|||||||
|
|
||||||
|
|
||||||
def get_timeline_generator(app, user, args):
|
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] if arg]) > 1:
|
||||||
if len([arg for arg in [args.tag, args.list, args.public] if arg]) > 1:
|
raise ConsoleError("Only one of --public, --tag, --account, or --list can be used at one time.")
|
||||||
raise ConsoleError("Only one of --public, --tag, or --list can be used at one time.")
|
|
||||||
|
|
||||||
if args.local and not (args.public or args.tag):
|
if args.local and not (args.public or args.tag):
|
||||||
raise ConsoleError("The --local option is only valid alongside --public or --tag.")
|
raise ConsoleError("The --local option is only valid alongside --public or --tag.")
|
||||||
@ -35,6 +34,8 @@ def get_timeline_generator(app, user, args):
|
|||||||
return api.anon_tag_timeline_generator(args.instance, args.tag, limit=args.count)
|
return api.anon_tag_timeline_generator(args.instance, args.tag, limit=args.count)
|
||||||
else:
|
else:
|
||||||
return api.tag_timeline_generator(app, user, args.tag, local=args.local, limit=args.count)
|
return api.tag_timeline_generator(app, user, args.tag, local=args.local, limit=args.count)
|
||||||
|
elif args.account:
|
||||||
|
return api.account_timeline_generator(app, user, args.account, limit=args.count)
|
||||||
elif args.list:
|
elif args.list:
|
||||||
return api.timeline_list_generator(app, user, args.list, limit=args.count)
|
return api.timeline_list_generator(app, user, args.list, limit=args.count)
|
||||||
else:
|
else:
|
||||||
@ -360,49 +361,26 @@ def _do_upload(app, user, file, description, thumbnail):
|
|||||||
return api.upload_media(app, user, file, description=description, thumbnail=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 @<instance_name> 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):
|
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'])
|
api.follow(app, user, account['id'])
|
||||||
print_out("<green>✓ You are now following {}</green>".format(args.account))
|
print_out("<green>✓ You are now following {}</green>".format(args.account))
|
||||||
|
|
||||||
|
|
||||||
def unfollow(app, user, args):
|
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'])
|
api.unfollow(app, user, account['id'])
|
||||||
print_out("<green>✓ You are no longer following {}</green>".format(args.account))
|
print_out("<green>✓ You are no longer following {}</green>".format(args.account))
|
||||||
|
|
||||||
|
|
||||||
def following(app, user, args):
|
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'])
|
response = api.following(app, user, account['id'])
|
||||||
print_acct_list(response)
|
print_acct_list(response)
|
||||||
|
|
||||||
|
|
||||||
def followers(app, user, args):
|
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'])
|
response = api.followers(app, user, account['id'])
|
||||||
print_acct_list(response)
|
print_acct_list(response)
|
||||||
|
|
||||||
@ -452,7 +430,7 @@ def list_delete(app, user, args):
|
|||||||
|
|
||||||
def list_add(app, user, args):
|
def list_add(app, user, args):
|
||||||
list_id = _get_list_id(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:
|
try:
|
||||||
api.add_accounts_to_list(app, user, list_id, [account['id']])
|
api.add_accounts_to_list(app, user, list_id, [account['id']])
|
||||||
@ -477,7 +455,7 @@ def list_add(app, user, args):
|
|||||||
|
|
||||||
def list_remove(app, user, args):
|
def list_remove(app, user, args):
|
||||||
list_id = _get_list_id(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']])
|
api.remove_accounts_from_list(app, user, list_id, [account['id']])
|
||||||
print_out(f"<green>✓ Removed account \"{args.account}\"</green>")
|
print_out(f"<green>✓ Removed account \"{args.account}\"</green>")
|
||||||
|
|
||||||
@ -490,25 +468,25 @@ def _get_list_id(app, user, args):
|
|||||||
|
|
||||||
|
|
||||||
def mute(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'])
|
api.mute(app, user, account['id'])
|
||||||
print_out("<green>✓ You have muted {}</green>".format(args.account))
|
print_out("<green>✓ You have muted {}</green>".format(args.account))
|
||||||
|
|
||||||
|
|
||||||
def unmute(app, user, args):
|
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'])
|
api.unmute(app, user, account['id'])
|
||||||
print_out("<green>✓ {} is no longer muted</green>".format(args.account))
|
print_out("<green>✓ {} is no longer muted</green>".format(args.account))
|
||||||
|
|
||||||
|
|
||||||
def block(app, user, args):
|
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'])
|
api.block(app, user, account['id'])
|
||||||
print_out("<green>✓ You are now blocking {}</green>".format(args.account))
|
print_out("<green>✓ You are now blocking {}</green>".format(args.account))
|
||||||
|
|
||||||
|
|
||||||
def unblock(app, user, args):
|
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'])
|
api.unblock(app, user, account['id'])
|
||||||
print_out("<green>✓ {} is no longer blocked</green>".format(args.account))
|
print_out("<green>✓ {} is no longer blocked</green>".format(args.account))
|
||||||
|
|
||||||
@ -519,7 +497,7 @@ def whoami(app, user, args):
|
|||||||
|
|
||||||
|
|
||||||
def whois(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)
|
print_account(account)
|
||||||
|
|
||||||
|
|
||||||
|
@ -238,6 +238,10 @@ common_timeline_args = [
|
|||||||
"type": str,
|
"type": str,
|
||||||
"help": "show hashtag timeline (does not require auth)",
|
"help": "show hashtag timeline (does not require auth)",
|
||||||
}),
|
}),
|
||||||
|
(["-a", "--account"], {
|
||||||
|
"type": str,
|
||||||
|
"help": "show timeline for the given account",
|
||||||
|
}),
|
||||||
(["-l", "--local"], {
|
(["-l", "--local"], {
|
||||||
"action": "store_true",
|
"action": "store_true",
|
||||||
"default": False,
|
"default": False,
|
||||||
|
@ -6,7 +6,6 @@ from concurrent.futures import ThreadPoolExecutor
|
|||||||
from toot import api, config, __version__
|
from toot import api, config, __version__
|
||||||
from toot.console import get_default_visibility
|
from toot.console import get_default_visibility
|
||||||
from toot.exceptions import ApiError
|
from toot.exceptions import ApiError
|
||||||
from toot.commands import find_account
|
|
||||||
|
|
||||||
from .compose import StatusComposer
|
from .compose import StatusComposer
|
||||||
from .constants import PALETTE
|
from .constants import PALETTE
|
||||||
@ -305,7 +304,7 @@ class TUI(urwid.Frame):
|
|||||||
def _load_accounts():
|
def _load_accounts():
|
||||||
try:
|
try:
|
||||||
acct = f'@{self.user.username}@{self.user.instance}'
|
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"])
|
return api.following(self.app, self.user, self.account["id"])
|
||||||
except ApiError:
|
except ApiError:
|
||||||
# not supported by all Mastodon servers so fail silently if necessary
|
# 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())
|
lambda x, local: self.goto_notifications())
|
||||||
urwid.connect_signal(menu, "conversation_timeline",
|
urwid.connect_signal(menu, "conversation_timeline",
|
||||||
lambda x, local: self.goto_conversations())
|
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",
|
urwid.connect_signal(menu, "hashtag_timeline",
|
||||||
lambda x, tag, local: self.goto_tag_timeline(tag, local=local))
|
lambda x, tag, local: self.goto_tag_timeline(tag, local=local))
|
||||||
urwid.connect_signal(menu, "list_timeline",
|
urwid.connect_signal(menu, "list_timeline",
|
||||||
@ -418,7 +419,7 @@ class TUI(urwid.Frame):
|
|||||||
|
|
||||||
self.open_overlay(menu, title="Go to", options=dict(
|
self.open_overlay(menu, title="Go to", options=dict(
|
||||||
align="center", width=("relative", 60),
|
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):
|
def show_help(self):
|
||||||
@ -472,6 +473,14 @@ class TUI(urwid.Frame):
|
|||||||
)
|
)
|
||||||
promise.add_done_callback(lambda *args: self.close_overlay())
|
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):
|
def goto_list_timeline(self, list_item):
|
||||||
self.timeline_generator = api.timeline_list_generator(
|
self.timeline_generator = api.timeline_list_generator(
|
||||||
self.app, self.user, list_item['id'], limit=40)
|
self.app, self.user, list_item['id'], limit=40)
|
||||||
|
@ -102,6 +102,7 @@ class GotoMenu(urwid.ListBox):
|
|||||||
"bookmark_timeline",
|
"bookmark_timeline",
|
||||||
"notification_timeline",
|
"notification_timeline",
|
||||||
"conversation_timeline",
|
"conversation_timeline",
|
||||||
|
"personal_timeline",
|
||||||
"list_timeline",
|
"list_timeline",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -126,6 +127,9 @@ class GotoMenu(urwid.ListBox):
|
|||||||
def _global_public(button):
|
def _global_public(button):
|
||||||
self._emit("public_timeline", False)
|
self._emit("public_timeline", False)
|
||||||
|
|
||||||
|
def _personal(button):
|
||||||
|
self._emit("personal_timeline", False)
|
||||||
|
|
||||||
def _bookmarks(button):
|
def _bookmarks(button):
|
||||||
self._emit("bookmark_timeline", False)
|
self._emit("bookmark_timeline", False)
|
||||||
|
|
||||||
@ -156,6 +160,7 @@ class GotoMenu(urwid.ListBox):
|
|||||||
yield Button("Home timeline", on_press=_home)
|
yield Button("Home timeline", on_press=_home)
|
||||||
yield Button("Local public timeline", on_press=_local_public)
|
yield Button("Local public timeline", on_press=_local_public)
|
||||||
yield Button("Global public timeline", on_press=_global_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("Bookmarks", on_press=_bookmarks)
|
||||||
yield Button("Notifications", on_press=_notifications)
|
yield Button("Notifications", on_press=_notifications)
|
||||||
yield Button("Conversations", on_press=_conversations)
|
yield Button("Conversations", on_press=_conversations)
|
||||||
|
Loading…
Reference in New Issue
Block a user