mirror of
https://github.com/ihabunek/toot
synced 2024-12-22 23:08:17 +01:00
Delete old command implementations
This commit is contained in:
parent
4dfab69f3b
commit
452b98d2ad
669
toot/commands.py
669
toot/commands.py
@ -1,669 +0,0 @@
|
||||
from itertools import chain
|
||||
import json
|
||||
import sys
|
||||
import platform
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from time import sleep, time
|
||||
|
||||
from toot import api, config, __version__
|
||||
from toot.auth import login_interactive, login_browser_interactive, create_app_interactive
|
||||
from toot.entities import Account, Instance, Notification, Status, from_dict
|
||||
from toot.exceptions import ApiError, ConsoleError
|
||||
from toot.output import (print_lists, print_out, print_instance, print_account, print_acct_list,
|
||||
print_search_results, print_status, print_table, print_timeline, print_notifications,
|
||||
print_tag_list, print_list_accounts, print_user_list)
|
||||
from toot.utils import args_get_instance, delete_tmp_status_file, editor_input, multiline_input, EOF_KEY
|
||||
from toot.utils.datetime import parse_datetime
|
||||
|
||||
|
||||
def get_timeline_generator(app, user, args):
|
||||
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.")
|
||||
|
||||
if args.instance and not (args.public or args.tag):
|
||||
raise ConsoleError("The --instance option is only valid alongside --public or --tag.")
|
||||
|
||||
if args.public:
|
||||
if args.instance:
|
||||
return api.anon_public_timeline_generator(args.instance, local=args.local, limit=args.count)
|
||||
else:
|
||||
return api.public_timeline_generator(app, user, local=args.local, limit=args.count)
|
||||
elif args.tag:
|
||||
if args.instance:
|
||||
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:
|
||||
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:
|
||||
return api.home_timeline_generator(app, user, limit=args.count)
|
||||
|
||||
|
||||
def timeline(app, user, args, generator=None):
|
||||
if not generator:
|
||||
generator = get_timeline_generator(app, user, args)
|
||||
|
||||
while True:
|
||||
try:
|
||||
items = next(generator)
|
||||
except StopIteration:
|
||||
print_out("That's all folks.")
|
||||
return
|
||||
|
||||
if args.reverse:
|
||||
items = reversed(items)
|
||||
|
||||
statuses = [from_dict(Status, item) for item in items]
|
||||
print_timeline(statuses)
|
||||
|
||||
if args.once or not sys.stdout.isatty():
|
||||
break
|
||||
|
||||
char = input("\nContinue? [Y/n] ")
|
||||
if char.lower() == "n":
|
||||
break
|
||||
|
||||
|
||||
def status(app, user, args):
|
||||
response = api.fetch_status(app, user, args.status_id)
|
||||
if args.json:
|
||||
print(response.text)
|
||||
else:
|
||||
status = from_dict(Status, response.json())
|
||||
print_status(status)
|
||||
|
||||
|
||||
def thread(app, user, args):
|
||||
context_response = api.context(app, user, args.status_id)
|
||||
|
||||
if args.json:
|
||||
print(context_response.text)
|
||||
else:
|
||||
toot = api.fetch_status(app, user, args.status_id).json()
|
||||
context = context_response.json()
|
||||
|
||||
statuses = chain(context["ancestors"], [toot], context["descendants"])
|
||||
print_timeline(from_dict(Status, s) for s in statuses)
|
||||
|
||||
|
||||
def post(app, user, args):
|
||||
if args.editor and not sys.stdin.isatty():
|
||||
raise ConsoleError("Cannot run editor if not in tty.")
|
||||
|
||||
if args.media and len(args.media) > 4:
|
||||
raise ConsoleError("Cannot attach more than 4 files.")
|
||||
|
||||
media_ids = _upload_media(app, user, args)
|
||||
status_text = _get_status_text(args.text, args.editor, args.media)
|
||||
scheduled_at = _get_scheduled_at(args.scheduled_at, args.scheduled_in)
|
||||
|
||||
if not status_text and not media_ids:
|
||||
raise ConsoleError("You must specify either text or media to post.")
|
||||
|
||||
response = api.post_status(
|
||||
app, user, status_text,
|
||||
visibility=args.visibility,
|
||||
media_ids=media_ids,
|
||||
sensitive=args.sensitive,
|
||||
spoiler_text=args.spoiler_text,
|
||||
in_reply_to_id=args.reply_to,
|
||||
language=args.language,
|
||||
scheduled_at=scheduled_at,
|
||||
content_type=args.content_type,
|
||||
poll_options=args.poll_option,
|
||||
poll_expires_in=args.poll_expires_in,
|
||||
poll_multiple=args.poll_multiple,
|
||||
poll_hide_totals=args.poll_hide_totals,
|
||||
)
|
||||
|
||||
if args.json:
|
||||
print(response.text)
|
||||
else:
|
||||
status = response.json()
|
||||
if "scheduled_at" in status:
|
||||
scheduled_at = parse_datetime(status["scheduled_at"])
|
||||
scheduled_at = datetime.strftime(scheduled_at, "%Y-%m-%d %H:%M:%S%z")
|
||||
print_out(f"Toot scheduled for: <green>{scheduled_at}</green>")
|
||||
else:
|
||||
print_out(f"Toot posted: <green>{status['url']}")
|
||||
|
||||
delete_tmp_status_file()
|
||||
|
||||
|
||||
def _get_status_text(text, editor, media):
|
||||
isatty = sys.stdin.isatty()
|
||||
|
||||
if not text and not isatty:
|
||||
text = sys.stdin.read().rstrip()
|
||||
|
||||
if isatty:
|
||||
if editor:
|
||||
text = editor_input(editor, text)
|
||||
elif not text and not media:
|
||||
print_out("Write or paste your toot. Press <yellow>{}</yellow> to post it.".format(EOF_KEY))
|
||||
text = multiline_input()
|
||||
|
||||
return text
|
||||
|
||||
|
||||
def _get_scheduled_at(scheduled_at, scheduled_in):
|
||||
if scheduled_at:
|
||||
return scheduled_at
|
||||
|
||||
if scheduled_in:
|
||||
scheduled_at = datetime.now(timezone.utc) + timedelta(seconds=scheduled_in)
|
||||
return scheduled_at.replace(microsecond=0).isoformat()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _upload_media(app, user, args):
|
||||
# Match media to corresponding description and thumbnail
|
||||
media = args.media or []
|
||||
descriptions = args.description or []
|
||||
thumbnails = args.thumbnail or []
|
||||
uploaded_media = []
|
||||
|
||||
for idx, file in enumerate(media):
|
||||
description = descriptions[idx].strip() if idx < len(descriptions) else None
|
||||
thumbnail = thumbnails[idx] if idx < len(thumbnails) else None
|
||||
result = _do_upload(app, user, file, description, thumbnail)
|
||||
uploaded_media.append(result)
|
||||
|
||||
_wait_until_all_processed(app, user, uploaded_media)
|
||||
|
||||
return [m["id"] for m in uploaded_media]
|
||||
|
||||
|
||||
def _wait_until_all_processed(app, user, uploaded_media):
|
||||
"""
|
||||
Media is uploaded asynchronously, and cannot be attached until the server
|
||||
has finished processing it. This function waits for that to happen.
|
||||
|
||||
Once media is processed, it will have the URL populated.
|
||||
"""
|
||||
if all(m["url"] for m in uploaded_media):
|
||||
return
|
||||
|
||||
# Timeout after waiting 1 minute
|
||||
start_time = time()
|
||||
timeout = 60
|
||||
|
||||
print_out("<dim>Waiting for media to finish processing...</dim>")
|
||||
for media in uploaded_media:
|
||||
_wait_until_processed(app, user, media, start_time, timeout)
|
||||
|
||||
|
||||
def _wait_until_processed(app, user, media, start_time, timeout):
|
||||
if media["url"]:
|
||||
return
|
||||
|
||||
media = api.get_media(app, user, media["id"])
|
||||
while not media["url"]:
|
||||
sleep(1)
|
||||
if time() > start_time + timeout:
|
||||
raise ConsoleError(f"Media not processed by server after {timeout} seconds. Aborting.")
|
||||
media = api.get_media(app, user, media["id"])
|
||||
|
||||
|
||||
def delete(app, user, args):
|
||||
response = api.delete_status(app, user, args.status_id)
|
||||
if args.json:
|
||||
print(response.text)
|
||||
else:
|
||||
print_out("<green>✓ Status deleted</green>")
|
||||
|
||||
|
||||
def favourite(app, user, args):
|
||||
response = api.favourite(app, user, args.status_id)
|
||||
if args.json:
|
||||
print(response.text)
|
||||
else:
|
||||
print_out("<green>✓ Status favourited</green>")
|
||||
|
||||
|
||||
def unfavourite(app, user, args):
|
||||
response = api.unfavourite(app, user, args.status_id)
|
||||
if args.json:
|
||||
print(response.text)
|
||||
else:
|
||||
print_out("<green>✓ Status unfavourited</green>")
|
||||
|
||||
|
||||
def reblog(app, user, args):
|
||||
response = api.reblog(app, user, args.status_id, visibility=args.visibility)
|
||||
if args.json:
|
||||
print(response.text)
|
||||
else:
|
||||
print_out("<green>✓ Status reblogged</green>")
|
||||
|
||||
|
||||
def unreblog(app, user, args):
|
||||
response = api.unreblog(app, user, args.status_id)
|
||||
if args.json:
|
||||
print(response.text)
|
||||
else:
|
||||
print_out("<green>✓ Status unreblogged</green>")
|
||||
|
||||
|
||||
def pin(app, user, args):
|
||||
response = api.pin(app, user, args.status_id)
|
||||
if args.json:
|
||||
print(response.text)
|
||||
else:
|
||||
print_out("<green>✓ Status pinned</green>")
|
||||
|
||||
|
||||
def unpin(app, user, args):
|
||||
response = api.unpin(app, user, args.status_id)
|
||||
if args.json:
|
||||
print(response.text)
|
||||
else:
|
||||
print_out("<green>✓ Status unpinned</green>")
|
||||
|
||||
|
||||
def bookmark(app, user, args):
|
||||
response = api.bookmark(app, user, args.status_id)
|
||||
if args.json:
|
||||
print(response.text)
|
||||
else:
|
||||
print_out("<green>✓ Status bookmarked</green>")
|
||||
|
||||
|
||||
def unbookmark(app, user, args):
|
||||
response = api.unbookmark(app, user, args.status_id)
|
||||
if args.json:
|
||||
print(response.text)
|
||||
else:
|
||||
print_out("<green>✓ Status unbookmarked</green>")
|
||||
|
||||
|
||||
def bookmarks(app, user, args):
|
||||
timeline(app, user, args, api.bookmark_timeline_generator(app, user, limit=args.count))
|
||||
|
||||
|
||||
def reblogged_by(app, user, args):
|
||||
response = api.reblogged_by(app, user, args.status_id)
|
||||
|
||||
if args.json:
|
||||
print(response.text)
|
||||
else:
|
||||
headers = ["Account", "Display name"]
|
||||
rows = [[a["acct"], a["display_name"]] for a in response.json()]
|
||||
print_table(headers, rows)
|
||||
|
||||
|
||||
def auth(app, user, args):
|
||||
config_data = config.load_config()
|
||||
|
||||
if not config_data["users"]:
|
||||
print_out("You are not logged in to any accounts")
|
||||
return
|
||||
|
||||
active_user = config_data["active_user"]
|
||||
|
||||
print_out("Authenticated accounts:")
|
||||
for uid, u in config_data["users"].items():
|
||||
active_label = "ACTIVE" if active_user == uid else ""
|
||||
print_out("* <green>{}</green> <yellow>{}</yellow>".format(uid, active_label))
|
||||
|
||||
path = config.get_config_file_path()
|
||||
print_out("\nAuth tokens are stored in: <blue>{}</blue>".format(path))
|
||||
|
||||
|
||||
def env(app, user, args):
|
||||
print_out(f"toot {__version__}")
|
||||
print_out(f"Python {sys.version}")
|
||||
print_out(platform.platform())
|
||||
|
||||
|
||||
def update_account(app, user, args):
|
||||
options = [
|
||||
args.avatar,
|
||||
args.bot,
|
||||
args.discoverable,
|
||||
args.display_name,
|
||||
args.header,
|
||||
args.language,
|
||||
args.locked,
|
||||
args.note,
|
||||
args.privacy,
|
||||
args.sensitive,
|
||||
]
|
||||
|
||||
if all(option is None for option in options):
|
||||
raise ConsoleError("Please specify at least one option to update the account")
|
||||
|
||||
response = api.update_account(
|
||||
app,
|
||||
user,
|
||||
avatar=args.avatar,
|
||||
bot=args.bot,
|
||||
discoverable=args.discoverable,
|
||||
display_name=args.display_name,
|
||||
header=args.header,
|
||||
language=args.language,
|
||||
locked=args.locked,
|
||||
note=args.note,
|
||||
privacy=args.privacy,
|
||||
sensitive=args.sensitive,
|
||||
)
|
||||
|
||||
if args.json:
|
||||
print(response.text)
|
||||
else:
|
||||
print_out("<green>✓ Account updated</green>")
|
||||
|
||||
|
||||
def login_cli(app, user, args):
|
||||
base_url = args_get_instance(args.instance, args.scheme)
|
||||
app = create_app_interactive(base_url)
|
||||
login_interactive(app, args.email)
|
||||
|
||||
print_out()
|
||||
print_out("<green>✓ Successfully logged in.</green>")
|
||||
|
||||
|
||||
def login(app, user, args):
|
||||
base_url = args_get_instance(args.instance, args.scheme)
|
||||
app = create_app_interactive(base_url)
|
||||
login_browser_interactive(app)
|
||||
|
||||
print_out()
|
||||
print_out("<green>✓ Successfully logged in.</green>")
|
||||
|
||||
|
||||
def logout(app, user, args):
|
||||
user = config.load_user(args.account, throw=True)
|
||||
config.delete_user(user)
|
||||
print_out("<green>✓ User {} logged out</green>".format(config.user_id(user)))
|
||||
|
||||
|
||||
def activate(app, user, args):
|
||||
if not args.account:
|
||||
print_out("Specify one of the following user accounts to activate:\n")
|
||||
print_user_list(config.get_user_list())
|
||||
return
|
||||
|
||||
user = config.load_user(args.account, throw=True)
|
||||
config.activate_user(user)
|
||||
print_out("<green>✓ User {} active</green>".format(config.user_id(user)))
|
||||
|
||||
|
||||
def upload(app, user, args):
|
||||
response = _do_upload(app, user, args.file, args.description, None)
|
||||
|
||||
msg = "Successfully uploaded media ID <yellow>{}</yellow>, type '<yellow>{}</yellow>'"
|
||||
|
||||
print_out()
|
||||
print_out(msg.format(response['id'], response['type']))
|
||||
print_out("URL: <green>{}</green>".format(response['url']))
|
||||
print_out("Preview URL: <green>{}</green>".format(response['preview_url']))
|
||||
|
||||
|
||||
def search(app, user, args):
|
||||
response = api.search(app, user, args.query, args.resolve)
|
||||
if args.json:
|
||||
print(response.text)
|
||||
else:
|
||||
print_search_results(response.json())
|
||||
|
||||
|
||||
def _do_upload(app, user, file, description, thumbnail):
|
||||
print_out("Uploading media: <green>{}</green>".format(file.name))
|
||||
return api.upload_media(app, user, file, description=description, thumbnail=thumbnail)
|
||||
|
||||
|
||||
def follow(app, user, args):
|
||||
account = api.find_account(app, user, args.account)
|
||||
response = api.follow(app, user, account["id"])
|
||||
if args.json:
|
||||
print(response.text)
|
||||
else:
|
||||
print_out(f"<green>✓ You are now following {args.account}</green>")
|
||||
|
||||
|
||||
def unfollow(app, user, args):
|
||||
account = api.find_account(app, user, args.account)
|
||||
response = api.unfollow(app, user, account["id"])
|
||||
if args.json:
|
||||
print(response.text)
|
||||
else:
|
||||
print_out(f"<green>✓ You are no longer following {args.account}</green>")
|
||||
|
||||
|
||||
def following(app, user, args):
|
||||
account = args.account or user.username
|
||||
account = api.find_account(app, user, account)
|
||||
accounts = api.following(app, user, account["id"])
|
||||
if args.json:
|
||||
print(json.dumps(accounts))
|
||||
else:
|
||||
print_acct_list(accounts)
|
||||
|
||||
|
||||
def followers(app, user, args):
|
||||
account = args.account or user.username
|
||||
account = api.find_account(app, user, account)
|
||||
accounts = api.followers(app, user, account["id"])
|
||||
if args.json:
|
||||
print(json.dumps(accounts))
|
||||
else:
|
||||
print_acct_list(accounts)
|
||||
|
||||
|
||||
def tags_follow(app, user, args):
|
||||
tn = args.tag_name if not args.tag_name.startswith("#") else args.tag_name[1:]
|
||||
api.follow_tag(app, user, tn)
|
||||
print_out("<green>✓ You are now following #{}</green>".format(tn))
|
||||
|
||||
|
||||
def tags_unfollow(app, user, args):
|
||||
tn = args.tag_name if not args.tag_name.startswith("#") else args.tag_name[1:]
|
||||
api.unfollow_tag(app, user, tn)
|
||||
print_out("<green>✓ You are no longer following #{}</green>".format(tn))
|
||||
|
||||
|
||||
def tags_followed(app, user, args):
|
||||
response = api.followed_tags(app, user)
|
||||
print_tag_list(response)
|
||||
|
||||
|
||||
def lists(app, user, args):
|
||||
lists = api.get_lists(app, user)
|
||||
|
||||
if lists:
|
||||
print_lists(lists)
|
||||
else:
|
||||
print_out("You have no lists defined.")
|
||||
|
||||
|
||||
def list_accounts(app, user, args):
|
||||
list_id = _get_list_id(app, user, args)
|
||||
response = api.get_list_accounts(app, user, list_id)
|
||||
print_list_accounts(response)
|
||||
|
||||
|
||||
def list_create(app, user, args):
|
||||
api.create_list(app, user, title=args.title, replies_policy=args.replies_policy)
|
||||
print_out(f"<green>✓ List \"{args.title}\" created.</green>")
|
||||
|
||||
|
||||
def list_delete(app, user, args):
|
||||
list_id = _get_list_id(app, user, args)
|
||||
api.delete_list(app, user, list_id)
|
||||
print_out(f"<green>✓ List \"{args.title if args.title else args.id}\"</green> <red>deleted.</red>")
|
||||
|
||||
|
||||
def list_add(app, user, args):
|
||||
list_id = _get_list_id(app, user, args)
|
||||
account = api.find_account(app, user, args.account)
|
||||
|
||||
try:
|
||||
api.add_accounts_to_list(app, user, list_id, [account['id']])
|
||||
except Exception as ex:
|
||||
# if we failed to add the account, try to give a
|
||||
# more specific error message than "record not found"
|
||||
my_accounts = api.followers(app, user, account['id'])
|
||||
found = False
|
||||
if my_accounts:
|
||||
for my_account in my_accounts:
|
||||
if my_account['id'] == account['id']:
|
||||
found = True
|
||||
break
|
||||
if found is False:
|
||||
print_out(f"<red>You must follow @{account['acct']} before adding this account to a list.</red>")
|
||||
else:
|
||||
print_out(f"<red>{ex}</red>")
|
||||
return
|
||||
|
||||
print_out(f"<green>✓ Added account \"{args.account}\"</green>")
|
||||
|
||||
|
||||
def list_remove(app, user, args):
|
||||
list_id = _get_list_id(app, user, args)
|
||||
account = api.find_account(app, user, args.account)
|
||||
api.remove_accounts_from_list(app, user, list_id, [account['id']])
|
||||
print_out(f"<green>✓ Removed account \"{args.account}\"</green>")
|
||||
|
||||
|
||||
def _get_list_id(app, user, args):
|
||||
list_id = args.id or api.find_list_id(app, user, args.title)
|
||||
if not list_id:
|
||||
raise ConsoleError("List not found")
|
||||
return list_id
|
||||
|
||||
|
||||
def mute(app, user, args):
|
||||
account = api.find_account(app, user, args.account)
|
||||
response = api.mute(app, user, account['id'])
|
||||
if args.json:
|
||||
print(response.text)
|
||||
else:
|
||||
print_out("<green>✓ You have muted {}</green>".format(args.account))
|
||||
|
||||
|
||||
def unmute(app, user, args):
|
||||
account = api.find_account(app, user, args.account)
|
||||
response = api.unmute(app, user, account['id'])
|
||||
if args.json:
|
||||
print(response.text)
|
||||
else:
|
||||
print_out("<green>✓ {} is no longer muted</green>".format(args.account))
|
||||
|
||||
|
||||
def muted(app, user, args):
|
||||
response = api.muted(app, user)
|
||||
if args.json:
|
||||
print(json.dumps(response))
|
||||
else:
|
||||
if len(response) > 0:
|
||||
print("Muted accounts:")
|
||||
print_acct_list(response)
|
||||
else:
|
||||
print("No accounts muted")
|
||||
|
||||
|
||||
def block(app, user, args):
|
||||
account = api.find_account(app, user, args.account)
|
||||
response = api.block(app, user, account['id'])
|
||||
if args.json:
|
||||
print(response.text)
|
||||
else:
|
||||
print_out("<green>✓ You are now blocking {}</green>".format(args.account))
|
||||
|
||||
|
||||
def unblock(app, user, args):
|
||||
account = api.find_account(app, user, args.account)
|
||||
response = api.unblock(app, user, account['id'])
|
||||
if args.json:
|
||||
print(response.text)
|
||||
else:
|
||||
print_out("<green>✓ {} is no longer blocked</green>".format(args.account))
|
||||
|
||||
|
||||
def blocked(app, user, args):
|
||||
response = api.blocked(app, user)
|
||||
if args.json:
|
||||
print(json.dumps(response))
|
||||
else:
|
||||
if len(response) > 0:
|
||||
print("Blocked accounts:")
|
||||
print_acct_list(response)
|
||||
else:
|
||||
print("No accounts blocked")
|
||||
|
||||
|
||||
def whoami(app, user, args):
|
||||
response = api.verify_credentials(app, user)
|
||||
if args.json:
|
||||
print(response.text)
|
||||
else:
|
||||
account = from_dict(Account, response.json())
|
||||
print_account(account)
|
||||
|
||||
|
||||
def whois(app, user, args):
|
||||
account = api.find_account(app, user, args.account)
|
||||
# Here it's not possible to avoid parsing json since it's needed to find the account.
|
||||
if args.json:
|
||||
print(json.dumps(account))
|
||||
else:
|
||||
account = from_dict(Account, account)
|
||||
print_account(account)
|
||||
|
||||
|
||||
def instance(app, user, args):
|
||||
default = app.base_url if app else None
|
||||
base_url = args_get_instance(args.instance, args.scheme, default)
|
||||
|
||||
if not base_url:
|
||||
raise ConsoleError("Please specify an instance.")
|
||||
|
||||
try:
|
||||
response = api.get_instance(base_url)
|
||||
except ApiError:
|
||||
raise ConsoleError(
|
||||
f"Instance not found at {base_url}.\n"
|
||||
"The given domain probably does not host a Mastodon instance."
|
||||
)
|
||||
|
||||
if args.json:
|
||||
print(response.text)
|
||||
else:
|
||||
instance = from_dict(Instance, response.json())
|
||||
print_instance(instance)
|
||||
|
||||
|
||||
def notifications(app, user, args):
|
||||
if args.clear:
|
||||
api.clear_notifications(app, user)
|
||||
print_out("<green>Cleared notifications</green>")
|
||||
return
|
||||
|
||||
exclude = []
|
||||
if args.mentions:
|
||||
# Filter everything except mentions
|
||||
# https://docs.joinmastodon.org/methods/notifications/
|
||||
exclude = ["follow", "favourite", "reblog", "poll", "follow_request"]
|
||||
notifications = api.get_notifications(app, user, exclude_types=exclude)
|
||||
if not notifications:
|
||||
print_out("<yellow>No notification</yellow>")
|
||||
return
|
||||
|
||||
if args.reverse:
|
||||
notifications = reversed(notifications)
|
||||
|
||||
notifications = [from_dict(Notification, n) for n in notifications]
|
||||
print_notifications(notifications)
|
||||
|
||||
|
||||
def tui(app, user, args):
|
||||
from .tui.app import TUI
|
||||
TUI.create(app, user, args).run()
|
966
toot/console.py
966
toot/console.py
@ -1,966 +0,0 @@
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
from argparse import ArgumentParser, FileType, ArgumentTypeError, Action
|
||||
from collections import namedtuple
|
||||
from itertools import chain
|
||||
from toot import config, commands, CLIENT_NAME, CLIENT_WEBSITE, __version__, settings
|
||||
from toot.exceptions import ApiError, ConsoleError
|
||||
from toot.output import print_out, print_err
|
||||
from toot.settings import get_setting
|
||||
|
||||
VISIBILITY_CHOICES = ["public", "unlisted", "private", "direct"]
|
||||
VISIBILITY_CHOICES_STR = ", ".join(f"'{v}'" for v in VISIBILITY_CHOICES)
|
||||
|
||||
PRIVACY_CHOICES = ["public", "unlisted", "private"]
|
||||
PRIVACY_CHOICES_STR = ", ".join(f"'{v}'" for v in PRIVACY_CHOICES)
|
||||
|
||||
|
||||
class BooleanOptionalAction(Action):
|
||||
"""
|
||||
Backported from argparse. This action is available since Python 3.9.
|
||||
https://github.com/python/cpython/blob/3.11/Lib/argparse.py
|
||||
"""
|
||||
def __init__(self,
|
||||
option_strings,
|
||||
dest,
|
||||
default=None,
|
||||
type=None,
|
||||
choices=None,
|
||||
required=False,
|
||||
help=None,
|
||||
metavar=None):
|
||||
|
||||
_option_strings = []
|
||||
for option_string in option_strings:
|
||||
_option_strings.append(option_string)
|
||||
|
||||
if option_string.startswith('--'):
|
||||
option_string = '--no-' + option_string[2:]
|
||||
_option_strings.append(option_string)
|
||||
|
||||
super().__init__(
|
||||
option_strings=_option_strings,
|
||||
dest=dest,
|
||||
nargs=0,
|
||||
default=default,
|
||||
type=type,
|
||||
choices=choices,
|
||||
required=required,
|
||||
help=help,
|
||||
metavar=metavar)
|
||||
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
if option_string in self.option_strings:
|
||||
setattr(namespace, self.dest, not option_string.startswith('--no-'))
|
||||
|
||||
def format_usage(self):
|
||||
return ' | '.join(self.option_strings)
|
||||
|
||||
|
||||
def get_default_visibility():
|
||||
return os.getenv("TOOT_POST_VISIBILITY", "public")
|
||||
|
||||
|
||||
def language(value):
|
||||
"""Validates the language parameter"""
|
||||
if len(value) != 2:
|
||||
raise ArgumentTypeError(
|
||||
"Invalid language. Expected a 2 letter abbreviation according to "
|
||||
"the ISO 639-1 standard."
|
||||
)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def visibility(value):
|
||||
"""Validates the visibility parameter"""
|
||||
if value not in VISIBILITY_CHOICES:
|
||||
raise ValueError("Invalid visibility value")
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def privacy(value):
|
||||
"""Validates the privacy parameter"""
|
||||
if value not in PRIVACY_CHOICES:
|
||||
raise ValueError(f"Invalid privacy value. Expected one of {PRIVACY_CHOICES_STR}.")
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def timeline_count(value):
|
||||
n = int(value)
|
||||
if not 0 < n <= 20:
|
||||
raise ArgumentTypeError("Number of toots should be between 1 and 20.")
|
||||
return n
|
||||
|
||||
|
||||
DURATION_UNITS = {
|
||||
"m": 60,
|
||||
"h": 60 * 60,
|
||||
"d": 60 * 60 * 24,
|
||||
}
|
||||
|
||||
|
||||
DURATION_EXAMPLES = """e.g. "1 day", "2 hours 30 minutes", "5 minutes 30
|
||||
seconds" or any combination of above. Shorthand: "1d", "2h30m", "5m30s\""""
|
||||
|
||||
|
||||
def duration(value: str):
|
||||
match = re.match(r"""^
|
||||
(([0-9]+)\s*(days|day|d))?\s*
|
||||
(([0-9]+)\s*(hours|hour|h))?\s*
|
||||
(([0-9]+)\s*(minutes|minute|m))?\s*
|
||||
(([0-9]+)\s*(seconds|second|s))?\s*
|
||||
$""", value, re.X)
|
||||
|
||||
if not match:
|
||||
raise ArgumentTypeError(f"Invalid duration: {value}")
|
||||
|
||||
days = match.group(2)
|
||||
hours = match.group(5)
|
||||
minutes = match.group(8)
|
||||
seconds = match.group(11)
|
||||
|
||||
days = int(match.group(2) or 0) * 60 * 60 * 24
|
||||
hours = int(match.group(5) or 0) * 60 * 60
|
||||
minutes = int(match.group(8) or 0) * 60
|
||||
seconds = int(match.group(11) or 0)
|
||||
|
||||
duration = days + hours + minutes + seconds
|
||||
|
||||
if duration == 0:
|
||||
raise ArgumentTypeError("Empty duration")
|
||||
|
||||
return duration
|
||||
|
||||
|
||||
def editor(value):
|
||||
if not value:
|
||||
raise ArgumentTypeError(
|
||||
"Editor not specified in --editor option and $EDITOR environment "
|
||||
"variable not set."
|
||||
)
|
||||
|
||||
# Check editor executable exists
|
||||
exe = shutil.which(value)
|
||||
if not exe:
|
||||
raise ArgumentTypeError("Editor `{}` not found".format(value))
|
||||
|
||||
return exe
|
||||
|
||||
|
||||
Command = namedtuple("Command", ["name", "description", "require_auth", "arguments"])
|
||||
|
||||
|
||||
# Arguments added to every command
|
||||
common_args = [
|
||||
(["--no-color"], {
|
||||
"help": "don't use ANSI colors in output",
|
||||
"action": 'store_true',
|
||||
"default": False,
|
||||
}),
|
||||
(["--quiet"], {
|
||||
"help": "don't write to stdout on success",
|
||||
"action": 'store_true',
|
||||
"default": False,
|
||||
}),
|
||||
(["--debug"], {
|
||||
"help": "show debug log in console",
|
||||
"action": 'store_true',
|
||||
"default": False,
|
||||
}),
|
||||
(["--verbose"], {
|
||||
"help": "show extra detail in debug log; used with --debug",
|
||||
"action": 'store_true',
|
||||
"default": False,
|
||||
}),
|
||||
]
|
||||
|
||||
# Arguments added to commands which require authentication
|
||||
common_auth_args = [
|
||||
(["-u", "--using"], {
|
||||
"help": "the account to use, overrides active account",
|
||||
}),
|
||||
]
|
||||
|
||||
account_arg = (["account"], {
|
||||
"help": "account name, e.g. 'Gargron@mastodon.social'",
|
||||
})
|
||||
|
||||
optional_account_arg = (["account"], {
|
||||
"nargs": "?",
|
||||
"help": "account name, e.g. 'Gargron@mastodon.social'",
|
||||
})
|
||||
|
||||
instance_arg = (["-i", "--instance"], {
|
||||
"type": str,
|
||||
"help": 'mastodon instance to log into e.g. "mastodon.social"',
|
||||
})
|
||||
|
||||
email_arg = (["-e", "--email"], {
|
||||
"type": str,
|
||||
"help": 'email address to log in with',
|
||||
})
|
||||
|
||||
scheme_arg = (["--disable-https"], {
|
||||
"help": "disable HTTPS and use insecure HTTP",
|
||||
"dest": "scheme",
|
||||
"default": "https",
|
||||
"action": "store_const",
|
||||
"const": "http",
|
||||
})
|
||||
|
||||
status_id_arg = (["status_id"], {
|
||||
"help": "ID of the status",
|
||||
"type": str,
|
||||
})
|
||||
|
||||
visibility_arg = (["-v", "--visibility"], {
|
||||
"type": visibility,
|
||||
"default": get_default_visibility(),
|
||||
"help": f"Post visibility. One of: {VISIBILITY_CHOICES_STR}. Defaults to "
|
||||
f"'{get_default_visibility()}' which can be overridden by setting "
|
||||
"the TOOT_POST_VISIBILITY environment variable",
|
||||
})
|
||||
|
||||
tag_arg = (["tag_name"], {
|
||||
"type": str,
|
||||
"help": "tag name, e.g. Caturday, or \"#Caturday\"",
|
||||
})
|
||||
|
||||
json_arg = (["--json"], {
|
||||
"action": "store_true",
|
||||
"default": False,
|
||||
"help": "print json instead of plaintext",
|
||||
})
|
||||
|
||||
# Arguments for selecting a timeline (see `toot.commands.get_timeline_generator`)
|
||||
common_timeline_args = [
|
||||
(["-p", "--public"], {
|
||||
"action": "store_true",
|
||||
"default": False,
|
||||
"help": "show public timeline (does not require auth)",
|
||||
}),
|
||||
(["-t", "--tag"], {
|
||||
"type": str,
|
||||
"help": "show hashtag timeline (does not require auth)",
|
||||
}),
|
||||
(["-a", "--account"], {
|
||||
"type": str,
|
||||
"help": "show timeline for the given account",
|
||||
}),
|
||||
(["-l", "--local"], {
|
||||
"action": "store_true",
|
||||
"default": False,
|
||||
"help": "show only statuses from local instance (public and tag timelines only)",
|
||||
}),
|
||||
(["-i", "--instance"], {
|
||||
"type": str,
|
||||
"help": "mastodon instance from which to read (public and tag timelines only)",
|
||||
}),
|
||||
(["--list"], {
|
||||
"type": str,
|
||||
"help": "show timeline for given list.",
|
||||
}),
|
||||
]
|
||||
|
||||
timeline_and_bookmark_args = [
|
||||
(["-c", "--count"], {
|
||||
"type": timeline_count,
|
||||
"help": "number of toots to show per page (1-20, default 10).",
|
||||
"default": 10,
|
||||
}),
|
||||
(["-r", "--reverse"], {
|
||||
"action": "store_true",
|
||||
"default": False,
|
||||
"help": "Reverse the order of the shown timeline (to new posts at the bottom)",
|
||||
}),
|
||||
(["-1", "--once"], {
|
||||
"action": "store_true",
|
||||
"default": False,
|
||||
"help": "Only show the first <count> toots, do not prompt to continue.",
|
||||
}),
|
||||
]
|
||||
|
||||
timeline_args = common_timeline_args + timeline_and_bookmark_args
|
||||
|
||||
AUTH_COMMANDS = [
|
||||
Command(
|
||||
name="login",
|
||||
description="Log into a mastodon instance using your browser (recommended)",
|
||||
arguments=[instance_arg, scheme_arg],
|
||||
require_auth=False,
|
||||
),
|
||||
Command(
|
||||
name="login_cli",
|
||||
description="Log in from the console, does NOT support two factor authentication",
|
||||
arguments=[instance_arg, email_arg, scheme_arg],
|
||||
require_auth=False,
|
||||
),
|
||||
Command(
|
||||
name="activate",
|
||||
description="Switch between logged in accounts.",
|
||||
arguments=[optional_account_arg],
|
||||
require_auth=False,
|
||||
),
|
||||
Command(
|
||||
name="logout",
|
||||
description="Log out, delete stored access keys",
|
||||
arguments=[account_arg],
|
||||
require_auth=False,
|
||||
),
|
||||
Command(
|
||||
name="auth",
|
||||
description="Show logged in accounts and instances",
|
||||
arguments=[],
|
||||
require_auth=False,
|
||||
),
|
||||
Command(
|
||||
name="env",
|
||||
description="Print environment information for inclusion in bug reports.",
|
||||
arguments=[],
|
||||
require_auth=False,
|
||||
),
|
||||
Command(
|
||||
name="update_account",
|
||||
description="Update your account details",
|
||||
arguments=[
|
||||
(["--display-name"], {
|
||||
"type": str,
|
||||
"help": "The display name to use for the profile.",
|
||||
}),
|
||||
(["--note"], {
|
||||
"type": str,
|
||||
"help": "The account bio.",
|
||||
}),
|
||||
(["--avatar"], {
|
||||
"type": FileType("rb"),
|
||||
"help": "Path to the avatar image to set.",
|
||||
}),
|
||||
(["--header"], {
|
||||
"type": FileType("rb"),
|
||||
"help": "Path to the header image to set.",
|
||||
}),
|
||||
(["--bot"], {
|
||||
"action": BooleanOptionalAction,
|
||||
"help": "Whether the account has a bot flag.",
|
||||
}),
|
||||
(["--discoverable"], {
|
||||
"action": BooleanOptionalAction,
|
||||
"help": "Whether the account should be shown in the profile directory.",
|
||||
}),
|
||||
(["--locked"], {
|
||||
"action": BooleanOptionalAction,
|
||||
"help": "Whether manual approval of follow requests is required.",
|
||||
}),
|
||||
(["--privacy"], {
|
||||
"type": privacy,
|
||||
"help": f"Default post privacy for authored statuses. One of: {PRIVACY_CHOICES_STR}."
|
||||
}),
|
||||
(["--sensitive"], {
|
||||
"action": BooleanOptionalAction,
|
||||
"help": "Whether to mark authored statuses as sensitive by default."
|
||||
}),
|
||||
(["--language"], {
|
||||
"type": language,
|
||||
"help": "Default language to use for authored statuses (ISO 639-1)."
|
||||
}),
|
||||
json_arg,
|
||||
],
|
||||
require_auth=True,
|
||||
),
|
||||
]
|
||||
|
||||
TUI_COMMANDS = [
|
||||
Command(
|
||||
name="tui",
|
||||
description="Launches the toot terminal user interface",
|
||||
arguments=[
|
||||
(["--relative-datetimes"], {
|
||||
"action": "store_true",
|
||||
"default": False,
|
||||
"help": "Show relative datetimes in status list.",
|
||||
}),
|
||||
],
|
||||
require_auth=True,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
READ_COMMANDS = [
|
||||
Command(
|
||||
name="whoami",
|
||||
description="Display logged in user details",
|
||||
arguments=[json_arg],
|
||||
require_auth=True,
|
||||
),
|
||||
Command(
|
||||
name="whois",
|
||||
description="Display account details",
|
||||
arguments=[
|
||||
(["account"], {
|
||||
"help": "account name or numeric ID"
|
||||
}),
|
||||
json_arg,
|
||||
],
|
||||
require_auth=True,
|
||||
),
|
||||
Command(
|
||||
name="notifications",
|
||||
description="Notifications for logged in user",
|
||||
arguments=[
|
||||
(["--clear"], {
|
||||
"help": "delete all notifications from the server",
|
||||
"action": 'store_true',
|
||||
"default": False,
|
||||
}),
|
||||
(["-r", "--reverse"], {
|
||||
"action": "store_true",
|
||||
"default": False,
|
||||
"help": "Reverse the order of the shown notifications (newest on top)",
|
||||
}),
|
||||
(["-m", "--mentions"], {
|
||||
"action": "store_true",
|
||||
"default": False,
|
||||
"help": "Only print mentions",
|
||||
})
|
||||
],
|
||||
require_auth=True,
|
||||
),
|
||||
Command(
|
||||
name="instance",
|
||||
description="Display instance details",
|
||||
arguments=[
|
||||
(["instance"], {
|
||||
"help": "instance domain (e.g. 'mastodon.social') or blank to use current",
|
||||
"nargs": "?",
|
||||
}),
|
||||
scheme_arg,
|
||||
json_arg,
|
||||
],
|
||||
require_auth=False,
|
||||
),
|
||||
Command(
|
||||
name="search",
|
||||
description="Search for users or hashtags",
|
||||
arguments=[
|
||||
(["query"], {
|
||||
"help": "the search query",
|
||||
}),
|
||||
(["-r", "--resolve"], {
|
||||
"action": 'store_true',
|
||||
"default": False,
|
||||
"help": "Resolve non-local accounts",
|
||||
}),
|
||||
json_arg,
|
||||
],
|
||||
require_auth=True,
|
||||
),
|
||||
Command(
|
||||
name="thread",
|
||||
description="Show toot thread items",
|
||||
arguments=[
|
||||
(["status_id"], {
|
||||
"help": "Show thread for toot.",
|
||||
}),
|
||||
json_arg,
|
||||
],
|
||||
require_auth=True,
|
||||
),
|
||||
Command(
|
||||
name="status",
|
||||
description="Show a single status",
|
||||
arguments=[
|
||||
(["status_id"], {
|
||||
"help": "ID of the status to show.",
|
||||
}),
|
||||
json_arg,
|
||||
],
|
||||
require_auth=True,
|
||||
),
|
||||
Command(
|
||||
name="timeline",
|
||||
description="Show recent items in a timeline (home by default)",
|
||||
arguments=timeline_args,
|
||||
require_auth=True,
|
||||
),
|
||||
Command(
|
||||
name="bookmarks",
|
||||
description="Show bookmarked posts",
|
||||
arguments=timeline_and_bookmark_args,
|
||||
require_auth=True,
|
||||
),
|
||||
]
|
||||
|
||||
POST_COMMANDS = [
|
||||
Command(
|
||||
name="post",
|
||||
description="Post a status text to your timeline",
|
||||
arguments=[
|
||||
(["text"], {
|
||||
"help": "The status text to post.",
|
||||
"nargs": "?",
|
||||
}),
|
||||
(["-m", "--media"], {
|
||||
"action": "append",
|
||||
"type": FileType("rb"),
|
||||
"help": "path to the media file to attach (specify multiple "
|
||||
"times to attach up to 4 files)"
|
||||
}),
|
||||
(["-d", "--description"], {
|
||||
"action": "append",
|
||||
"type": str,
|
||||
"help": "plain-text description of the media for accessibility "
|
||||
"purposes, one per attached media"
|
||||
}),
|
||||
(["--thumbnail"], {
|
||||
"action": "append",
|
||||
"type": FileType("rb"),
|
||||
"help": "path to an image file to serve as media thumbnail, "
|
||||
"one per attached media"
|
||||
}),
|
||||
visibility_arg,
|
||||
(["-s", "--sensitive"], {
|
||||
"action": 'store_true',
|
||||
"default": False,
|
||||
"help": "mark the media as NSFW",
|
||||
}),
|
||||
(["-p", "--spoiler-text"], {
|
||||
"type": str,
|
||||
"help": "text to be shown as a warning before the actual content",
|
||||
}),
|
||||
(["-r", "--reply-to"], {
|
||||
"type": str,
|
||||
"help": "local ID of the status you want to reply to",
|
||||
}),
|
||||
(["-l", "--language"], {
|
||||
"type": language,
|
||||
"help": "ISO 639-1 language code of the toot, to skip automatic detection",
|
||||
}),
|
||||
(["-e", "--editor"], {
|
||||
"type": editor,
|
||||
"nargs": "?",
|
||||
"const": os.getenv("EDITOR", ""), # option given without value
|
||||
"help": "Specify an editor to compose your toot, "
|
||||
"defaults to editor defined in $EDITOR env variable.",
|
||||
}),
|
||||
(["--scheduled-at"], {
|
||||
"type": str,
|
||||
"help": "ISO 8601 Datetime at which to schedule a status. Must "
|
||||
"be at least 5 minutes in the future.",
|
||||
}),
|
||||
(["--scheduled-in"], {
|
||||
"type": duration,
|
||||
"help": f"""Schedule the toot to be posted after a given amount
|
||||
of time, {DURATION_EXAMPLES}. Must be at least 5
|
||||
minutes.""",
|
||||
}),
|
||||
(["-t", "--content-type"], {
|
||||
"type": str,
|
||||
"help": "MIME type for the status text (not supported on all instances)",
|
||||
}),
|
||||
(["--poll-option"], {
|
||||
"action": "append",
|
||||
"type": str,
|
||||
"help": "Possible answer to the poll"
|
||||
}),
|
||||
(["--poll-expires-in"], {
|
||||
"type": duration,
|
||||
"help": f"""Duration that the poll should be open,
|
||||
{DURATION_EXAMPLES}. Defaults to 24h.""",
|
||||
"default": 24 * 60 * 60,
|
||||
}),
|
||||
(["--poll-multiple"], {
|
||||
"action": "store_true",
|
||||
"default": False,
|
||||
"help": "Allow multiple answers to be selected."
|
||||
}),
|
||||
(["--poll-hide-totals"], {
|
||||
"action": "store_true",
|
||||
"default": False,
|
||||
"help": "Hide vote counts until the poll ends. Defaults to false."
|
||||
}),
|
||||
json_arg,
|
||||
],
|
||||
require_auth=True,
|
||||
),
|
||||
Command(
|
||||
name="upload",
|
||||
description="Upload an image or video file",
|
||||
arguments=[
|
||||
(["file"], {
|
||||
"help": "Path to the file to upload",
|
||||
"type": FileType('rb')
|
||||
}),
|
||||
(["-d", "--description"], {
|
||||
"type": str,
|
||||
"help": "plain-text description of the media for accessibility purposes"
|
||||
}),
|
||||
],
|
||||
require_auth=True,
|
||||
),
|
||||
]
|
||||
|
||||
STATUS_COMMANDS = [
|
||||
Command(
|
||||
name="delete",
|
||||
description="Delete a status",
|
||||
arguments=[status_id_arg, json_arg],
|
||||
require_auth=True,
|
||||
),
|
||||
Command(
|
||||
name="favourite",
|
||||
description="Favourite a status",
|
||||
arguments=[status_id_arg, json_arg],
|
||||
require_auth=True,
|
||||
),
|
||||
Command(
|
||||
name="unfavourite",
|
||||
description="Unfavourite a status",
|
||||
arguments=[status_id_arg, json_arg],
|
||||
require_auth=True,
|
||||
),
|
||||
Command(
|
||||
name="reblog",
|
||||
description="Reblog a status",
|
||||
arguments=[status_id_arg, visibility_arg, json_arg],
|
||||
require_auth=True,
|
||||
),
|
||||
Command(
|
||||
name="unreblog",
|
||||
description="Unreblog a status",
|
||||
arguments=[status_id_arg, json_arg],
|
||||
require_auth=True,
|
||||
),
|
||||
Command(
|
||||
name="reblogged_by",
|
||||
description="Show accounts that reblogged the status",
|
||||
arguments=[status_id_arg, json_arg],
|
||||
require_auth=False,
|
||||
),
|
||||
Command(
|
||||
name="pin",
|
||||
description="Pin a status",
|
||||
arguments=[status_id_arg, json_arg],
|
||||
require_auth=True,
|
||||
),
|
||||
Command(
|
||||
name="unpin",
|
||||
description="Unpin a status",
|
||||
arguments=[status_id_arg, json_arg],
|
||||
require_auth=True,
|
||||
),
|
||||
Command(
|
||||
name="bookmark",
|
||||
description="Bookmark a status",
|
||||
arguments=[status_id_arg, json_arg],
|
||||
require_auth=True,
|
||||
),
|
||||
Command(
|
||||
name="unbookmark",
|
||||
description="Unbookmark a status",
|
||||
arguments=[status_id_arg, json_arg],
|
||||
require_auth=True,
|
||||
),
|
||||
]
|
||||
|
||||
ACCOUNTS_COMMANDS = [
|
||||
Command(
|
||||
name="follow",
|
||||
description="Follow an account",
|
||||
arguments=[account_arg, json_arg],
|
||||
require_auth=True,
|
||||
),
|
||||
Command(
|
||||
name="unfollow",
|
||||
description="Unfollow an account",
|
||||
arguments=[account_arg, json_arg],
|
||||
require_auth=True,
|
||||
),
|
||||
Command(
|
||||
name="following",
|
||||
description="List accounts followed by the given account, " +
|
||||
"or your account if no account given",
|
||||
arguments=[optional_account_arg, json_arg],
|
||||
require_auth=True,
|
||||
),
|
||||
Command(
|
||||
name="followers",
|
||||
description="List accounts following the given account, " +
|
||||
"or your account if no account given",
|
||||
arguments=[optional_account_arg, json_arg],
|
||||
require_auth=True,
|
||||
),
|
||||
Command(
|
||||
name="mute",
|
||||
description="Mute an account",
|
||||
arguments=[account_arg, json_arg],
|
||||
require_auth=True,
|
||||
),
|
||||
Command(
|
||||
name="unmute",
|
||||
description="Unmute an account",
|
||||
arguments=[account_arg, json_arg],
|
||||
require_auth=True,
|
||||
),
|
||||
Command(
|
||||
name="muted",
|
||||
description="List muted accounts",
|
||||
arguments=[json_arg],
|
||||
require_auth=True,
|
||||
),
|
||||
Command(
|
||||
name="block",
|
||||
description="Block an account",
|
||||
arguments=[account_arg, json_arg],
|
||||
require_auth=True,
|
||||
),
|
||||
Command(
|
||||
name="unblock",
|
||||
description="Unblock an account",
|
||||
arguments=[account_arg, json_arg],
|
||||
require_auth=True,
|
||||
),
|
||||
Command(
|
||||
name="blocked",
|
||||
description="List blocked accounts",
|
||||
arguments=[json_arg],
|
||||
require_auth=True,
|
||||
),
|
||||
]
|
||||
|
||||
TAG_COMMANDS = [
|
||||
Command(
|
||||
name="tags_followed",
|
||||
description="List hashtags you follow",
|
||||
arguments=[],
|
||||
require_auth=True,
|
||||
),
|
||||
Command(
|
||||
name="tags_follow",
|
||||
description="Follow a hashtag",
|
||||
arguments=[tag_arg],
|
||||
require_auth=True,
|
||||
),
|
||||
Command(
|
||||
name="tags_unfollow",
|
||||
description="Unfollow a hashtag",
|
||||
arguments=[tag_arg],
|
||||
require_auth=True,
|
||||
),
|
||||
]
|
||||
|
||||
LIST_COMMANDS = [
|
||||
Command(
|
||||
name="lists",
|
||||
description="List all lists",
|
||||
arguments=[],
|
||||
require_auth=True,
|
||||
),
|
||||
Command(
|
||||
name="list_accounts",
|
||||
description="List the accounts in a list",
|
||||
arguments=[
|
||||
(["--id"], {
|
||||
"type": str,
|
||||
"help": "ID of the list"
|
||||
}),
|
||||
(["title"], {
|
||||
"type": str,
|
||||
"nargs": "?",
|
||||
"help": "title of the list"
|
||||
}),
|
||||
],
|
||||
require_auth=True,
|
||||
),
|
||||
Command(
|
||||
name="list_create",
|
||||
description="Create a list",
|
||||
arguments=[
|
||||
(["title"], {
|
||||
"type": str,
|
||||
"help": "title of the list"
|
||||
}),
|
||||
(["--replies-policy"], {
|
||||
"type": str,
|
||||
"help": "replies policy: 'followed', 'list', or 'none' (defaults to 'none')"
|
||||
}),
|
||||
],
|
||||
require_auth=True,
|
||||
),
|
||||
Command(
|
||||
name="list_delete",
|
||||
description="Delete a list",
|
||||
arguments=[
|
||||
(["--id"], {
|
||||
"type": str,
|
||||
"help": "ID of the list"
|
||||
}),
|
||||
(["title"], {
|
||||
"type": str,
|
||||
"nargs": "?",
|
||||
"help": "title of the list"
|
||||
}),
|
||||
],
|
||||
require_auth=True,
|
||||
),
|
||||
Command(
|
||||
name="list_add",
|
||||
description="Add account to list",
|
||||
arguments=[
|
||||
(["--id"], {
|
||||
"type": str,
|
||||
"help": "ID of the list"
|
||||
}),
|
||||
(["title"], {
|
||||
"type": str,
|
||||
"nargs": "?",
|
||||
"help": "title of the list"
|
||||
}),
|
||||
(["account"], {
|
||||
"type": str,
|
||||
"help": "Account to add"
|
||||
}),
|
||||
],
|
||||
require_auth=True,
|
||||
),
|
||||
Command(
|
||||
name="list_remove",
|
||||
description="Remove account from list",
|
||||
arguments=[
|
||||
(["--id"], {
|
||||
"type": str,
|
||||
"help": "ID of the list"
|
||||
}),
|
||||
(["title"], {
|
||||
"type": str,
|
||||
"nargs": "?",
|
||||
"help": "title of the list"
|
||||
}),
|
||||
(["account"], {
|
||||
"type": str,
|
||||
"help": "Account to remove"
|
||||
}),
|
||||
],
|
||||
require_auth=True,
|
||||
),
|
||||
]
|
||||
COMMAND_GROUPS = [
|
||||
("Authentication", AUTH_COMMANDS),
|
||||
("TUI", TUI_COMMANDS),
|
||||
("Read", READ_COMMANDS),
|
||||
("Post", POST_COMMANDS),
|
||||
("Status", STATUS_COMMANDS),
|
||||
("Accounts", ACCOUNTS_COMMANDS),
|
||||
("Hashtags", TAG_COMMANDS),
|
||||
("Lists", LIST_COMMANDS),
|
||||
]
|
||||
|
||||
COMMANDS = list(chain(*[commands for _, commands in COMMAND_GROUPS]))
|
||||
|
||||
|
||||
def print_usage():
|
||||
max_name_len = max(len(name) for name, _ in COMMAND_GROUPS)
|
||||
|
||||
print_out("<green>{}</green>".format(CLIENT_NAME))
|
||||
print_out("<blue>v{}</blue>".format(__version__))
|
||||
|
||||
for name, cmds in COMMAND_GROUPS:
|
||||
print_out("")
|
||||
print_out(name + ":")
|
||||
|
||||
for cmd in cmds:
|
||||
cmd_name = cmd.name.ljust(max_name_len + 2)
|
||||
print_out(" <yellow>toot {}</yellow> {}".format(cmd_name, cmd.description))
|
||||
|
||||
print_out("")
|
||||
print_out("To get help for each command run:")
|
||||
print_out(" <yellow>toot \\<command> --help</yellow>")
|
||||
print_out("")
|
||||
print_out("<green>{}</green>".format(CLIENT_WEBSITE))
|
||||
|
||||
|
||||
def get_argument_parser(name, command):
|
||||
parser = ArgumentParser(
|
||||
prog='toot %s' % name,
|
||||
description=command.description,
|
||||
epilog=CLIENT_WEBSITE)
|
||||
|
||||
combined_args = command.arguments + common_args
|
||||
if command.require_auth:
|
||||
combined_args += common_auth_args
|
||||
|
||||
defaults = get_setting(f"commands.{name}", dict, {})
|
||||
|
||||
for args, kwargs in combined_args:
|
||||
# Set default value from settings if exists
|
||||
default = get_default_value(defaults, args)
|
||||
if default is not None:
|
||||
kwargs["default"] = default
|
||||
parser.add_argument(*args, **kwargs)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def get_default_value(defaults, args):
|
||||
# Hacky way to determine command name from argparse args
|
||||
name = args[-1].lstrip("-").replace("-", "_")
|
||||
return defaults.get(name)
|
||||
|
||||
|
||||
def run_command(app, user, name, args):
|
||||
command = next((c for c in COMMANDS if c.name == name), None)
|
||||
|
||||
if not command:
|
||||
print_err(f"Unknown command '{name}'")
|
||||
print_out("Run <yellow>toot --help</yellow> to show a list of available commands.")
|
||||
return
|
||||
|
||||
parser = get_argument_parser(name, command)
|
||||
parsed_args = parser.parse_args(args)
|
||||
|
||||
# Override the active account if 'using' option is given
|
||||
if command.require_auth and parsed_args.using:
|
||||
user, app = config.get_user_app(parsed_args.using)
|
||||
if not user or not app:
|
||||
raise ConsoleError("User '{}' not found".format(parsed_args.using))
|
||||
|
||||
if command.require_auth and (not user or not app):
|
||||
print_err("This command requires that you are logged in.")
|
||||
print_err("Please run `toot login` first.")
|
||||
return
|
||||
|
||||
fn = commands.__dict__.get(name)
|
||||
|
||||
if not fn:
|
||||
raise NotImplementedError("Command '{}' does not have an implementation.".format(name))
|
||||
|
||||
return fn(app, user, parsed_args)
|
||||
|
||||
|
||||
def main():
|
||||
if settings.get_debug():
|
||||
filename = settings.get_debug_file()
|
||||
logging.basicConfig(level=logging.DEBUG, filename=filename)
|
||||
logging.getLogger("urllib3").setLevel(logging.INFO)
|
||||
|
||||
command_name = sys.argv[1] if len(sys.argv) > 1 else None
|
||||
args = sys.argv[2:]
|
||||
|
||||
if not command_name or command_name == "--help":
|
||||
return print_usage()
|
||||
|
||||
user, app = config.get_active_user_app()
|
||||
|
||||
try:
|
||||
run_command(app, user, command_name, args)
|
||||
except (ConsoleError, ApiError) as e:
|
||||
print_err(str(e))
|
||||
sys.exit(1)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
Loading…
Reference in New Issue
Block a user