mirror of
https://github.com/ihabunek/toot
synced 2025-02-12 18:10:45 +01:00
Migrate auth commands
This commit is contained in:
parent
696a9dcc2e
commit
d8c7084678
2
Makefile
2
Makefile
@ -15,7 +15,7 @@ test:
|
||||
coverage:
|
||||
coverage erase
|
||||
coverage run
|
||||
coverage html
|
||||
coverage html --omit toot/tui/*
|
||||
coverage report
|
||||
|
||||
clean :
|
||||
|
@ -1,3 +1,9 @@
|
||||
0.40.0:
|
||||
date: TBA
|
||||
changes:
|
||||
- "Migrated to `click` for commandline arguments. BC should be mostly preserved, please report any issues."
|
||||
- "Removed the deprecated `--disable-https` option for `login` and `login_cli`, pass the base URL instead"
|
||||
|
||||
0.39.0:
|
||||
date: 2023-11-23
|
||||
changes:
|
||||
|
217
tests/integration/test_auth.py
Normal file
217
tests/integration/test_auth.py
Normal file
@ -0,0 +1,217 @@
|
||||
from typing import Any, Dict
|
||||
from unittest import mock
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from toot import User, cli
|
||||
from toot.cli.base import Run
|
||||
|
||||
# TODO: figure out how to test login
|
||||
|
||||
|
||||
EMPTY_CONFIG: Dict[Any, Any] = {
|
||||
"apps": {},
|
||||
"users": {},
|
||||
"active_user": None
|
||||
}
|
||||
|
||||
SAMPLE_CONFIG = {
|
||||
"active_user": "frank@foo.social",
|
||||
"apps": {
|
||||
"foo.social": {
|
||||
"base_url": "http://foo.social",
|
||||
"client_id": "123",
|
||||
"client_secret": "123",
|
||||
"instance": "foo.social"
|
||||
},
|
||||
"bar.social": {
|
||||
"base_url": "http://bar.social",
|
||||
"client_id": "123",
|
||||
"client_secret": "123",
|
||||
"instance": "bar.social"
|
||||
},
|
||||
},
|
||||
"users": {
|
||||
"frank@foo.social": {
|
||||
"access_token": "123",
|
||||
"instance": "foo.social",
|
||||
"username": "frank"
|
||||
},
|
||||
"frank@bar.social": {
|
||||
"access_token": "123",
|
||||
"instance": "bar.social",
|
||||
"username": "frank"
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test_env(run: Run):
|
||||
result = run(cli.env)
|
||||
assert result.exit_code == 0
|
||||
assert "toot" in result.stdout
|
||||
assert "Python" in result.stdout
|
||||
|
||||
|
||||
@mock.patch("toot.config.load_config")
|
||||
def test_auth_empty(load_config: MagicMock, run: Run):
|
||||
load_config.return_value = EMPTY_CONFIG
|
||||
result = run(cli.auth)
|
||||
assert result.exit_code == 0
|
||||
assert result.stdout.strip() == "You are not logged in to any accounts"
|
||||
|
||||
|
||||
@mock.patch("toot.config.load_config")
|
||||
def test_auth_full(load_config: MagicMock, run: Run):
|
||||
load_config.return_value = SAMPLE_CONFIG
|
||||
result = run(cli.auth)
|
||||
assert result.exit_code == 0
|
||||
assert result.stdout.strip().startswith("Authenticated accounts:")
|
||||
assert "frank@foo.social" in result.stdout
|
||||
assert "frank@bar.social" in result.stdout
|
||||
|
||||
|
||||
# Saving config is mocked so we don't mess up our local config
|
||||
# TODO: could this be implemented using an auto-use fixture so we have it always
|
||||
# mocked?
|
||||
@mock.patch("toot.config.load_app")
|
||||
@mock.patch("toot.config.save_app")
|
||||
@mock.patch("toot.config.save_user")
|
||||
def test_login_cli(
|
||||
save_user: MagicMock,
|
||||
save_app: MagicMock,
|
||||
load_app: MagicMock,
|
||||
user: User,
|
||||
run: Run,
|
||||
):
|
||||
load_app.return_value = None
|
||||
|
||||
result = run(
|
||||
cli.login_cli,
|
||||
"--instance", "http://localhost:3000",
|
||||
"--email", f"{user.username}@example.com",
|
||||
"--password", "password",
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "✓ Successfully logged in." in result.stdout
|
||||
|
||||
save_app.assert_called_once()
|
||||
(app,) = save_app.call_args.args
|
||||
assert app.instance == "localhost:3000"
|
||||
assert app.base_url == "http://localhost:3000"
|
||||
assert app.client_id
|
||||
assert app.client_secret
|
||||
|
||||
save_user.assert_called_once()
|
||||
(new_user,) = save_user.call_args.args
|
||||
assert new_user.instance == "localhost:3000"
|
||||
assert new_user.username == user.username
|
||||
# access token will be different since this is a new login
|
||||
assert new_user.access_token and new_user.access_token != user.access_token
|
||||
assert save_user.call_args.kwargs == {"activate": True}
|
||||
|
||||
|
||||
@mock.patch("toot.config.load_app")
|
||||
@mock.patch("toot.config.save_app")
|
||||
@mock.patch("toot.config.save_user")
|
||||
def test_login_cli_wrong_password(
|
||||
save_user: MagicMock,
|
||||
save_app: MagicMock,
|
||||
load_app: MagicMock,
|
||||
user: User,
|
||||
run: Run,
|
||||
):
|
||||
load_app.return_value = None
|
||||
|
||||
result = run(
|
||||
cli.login_cli,
|
||||
"--instance", "http://localhost:3000",
|
||||
"--email", f"{user.username}@example.com",
|
||||
"--password", "wrong password",
|
||||
)
|
||||
assert result.exit_code == 1
|
||||
assert result.stderr.strip() == "Error: Login failed"
|
||||
|
||||
save_app.assert_called_once()
|
||||
(app,) = save_app.call_args.args
|
||||
assert app.instance == "localhost:3000"
|
||||
assert app.base_url == "http://localhost:3000"
|
||||
assert app.client_id
|
||||
assert app.client_secret
|
||||
|
||||
save_user.assert_not_called()
|
||||
|
||||
|
||||
@mock.patch("toot.config.load_config")
|
||||
@mock.patch("toot.config.delete_user")
|
||||
def test_logout(delete_user: MagicMock, load_config: MagicMock, run: Run):
|
||||
load_config.return_value = SAMPLE_CONFIG
|
||||
|
||||
result = run(cli.logout, "frank@foo.social")
|
||||
assert result.exit_code == 0
|
||||
assert result.stdout.strip() == "✓ Account frank@foo.social logged out"
|
||||
delete_user.assert_called_once_with(User("foo.social", "frank", "123"))
|
||||
|
||||
|
||||
@mock.patch("toot.config.load_config")
|
||||
def test_logout_not_logged_in(load_config: MagicMock, run: Run):
|
||||
load_config.return_value = EMPTY_CONFIG
|
||||
|
||||
result = run(cli.logout)
|
||||
assert result.exit_code == 1
|
||||
assert result.stderr.strip() == "Error: You're not logged into any accounts"
|
||||
|
||||
|
||||
@mock.patch("toot.config.load_config")
|
||||
def test_logout_account_not_specified(load_config: MagicMock, run: Run):
|
||||
load_config.return_value = SAMPLE_CONFIG
|
||||
|
||||
result = run(cli.logout)
|
||||
assert result.exit_code == 1
|
||||
assert result.stderr.startswith("Error: Specify account to log out")
|
||||
|
||||
|
||||
@mock.patch("toot.config.load_config")
|
||||
def test_logout_account_does_not_exist(load_config: MagicMock, run: Run):
|
||||
load_config.return_value = SAMPLE_CONFIG
|
||||
|
||||
result = run(cli.logout, "banana")
|
||||
assert result.exit_code == 1
|
||||
assert result.stderr.startswith("Error: Account not found")
|
||||
|
||||
|
||||
@mock.patch("toot.config.load_config")
|
||||
@mock.patch("toot.config.activate_user")
|
||||
def test_activate(activate_user: MagicMock, load_config: MagicMock, run: Run):
|
||||
load_config.return_value = SAMPLE_CONFIG
|
||||
|
||||
result = run(cli.activate, "frank@foo.social")
|
||||
assert result.exit_code == 0
|
||||
assert result.stdout.strip() == "✓ Account frank@foo.social activated"
|
||||
activate_user.assert_called_once_with(User("foo.social", "frank", "123"))
|
||||
|
||||
|
||||
@mock.patch("toot.config.load_config")
|
||||
def test_activate_not_logged_in(load_config: MagicMock, run: Run):
|
||||
load_config.return_value = EMPTY_CONFIG
|
||||
|
||||
result = run(cli.activate)
|
||||
assert result.exit_code == 1
|
||||
assert result.stderr.strip() == "Error: You're not logged into any accounts"
|
||||
|
||||
|
||||
@mock.patch("toot.config.load_config")
|
||||
def test_activate_account_not_given(load_config: MagicMock, run: Run):
|
||||
load_config.return_value = SAMPLE_CONFIG
|
||||
|
||||
result = run(cli.activate)
|
||||
assert result.exit_code == 1
|
||||
assert result.stderr.startswith("Error: Specify account to activate")
|
||||
|
||||
|
||||
@mock.patch("toot.config.load_config")
|
||||
def test_activate_invalid_Account(load_config: MagicMock, run: Run):
|
||||
load_config.return_value = SAMPLE_CONFIG
|
||||
|
||||
result = run(cli.activate, "banana")
|
||||
assert result.exit_code == 1
|
||||
assert result.stderr.startswith("Error: Account not found")
|
@ -1,7 +1,7 @@
|
||||
from argparse import ArgumentTypeError
|
||||
import click
|
||||
import pytest
|
||||
|
||||
from toot.console import duration
|
||||
from toot.cli.validators import validate_duration
|
||||
from toot.wcstring import wc_wrap, trunc, pad, fit_text
|
||||
from toot.utils import urlencode_url
|
||||
|
||||
@ -163,6 +163,9 @@ def test_wc_wrap_indented():
|
||||
|
||||
|
||||
def test_duration():
|
||||
def duration(value):
|
||||
return validate_duration(None, None, value)
|
||||
|
||||
# Long hand
|
||||
assert duration("1 second") == 1
|
||||
assert duration("1 seconds") == 1
|
||||
@ -190,17 +193,17 @@ def test_duration():
|
||||
assert duration("5d 10h 3m 1s") == 5 * 86400 + 10 * 3600 + 3 * 60 + 1
|
||||
assert duration("5d10h3m1s") == 5 * 86400 + 10 * 3600 + 3 * 60 + 1
|
||||
|
||||
with pytest.raises(ArgumentTypeError):
|
||||
with pytest.raises(click.BadParameter):
|
||||
duration("")
|
||||
|
||||
with pytest.raises(ArgumentTypeError):
|
||||
with pytest.raises(click.BadParameter):
|
||||
duration("100")
|
||||
|
||||
# Wrong order
|
||||
with pytest.raises(ArgumentTypeError):
|
||||
with pytest.raises(click.BadParameter):
|
||||
duration("1m1d")
|
||||
|
||||
with pytest.raises(ArgumentTypeError):
|
||||
with pytest.raises(click.BadParameter):
|
||||
duration("banana")
|
||||
|
||||
|
||||
|
14
toot/api.py
14
toot/api.py
@ -140,7 +140,7 @@ def fetch_app_token(app):
|
||||
return http.anon_post(f"{app.base_url}/oauth/token", json=json).json()
|
||||
|
||||
|
||||
def login(app, username, password):
|
||||
def login(app: App, username: str, password: str):
|
||||
url = app.base_url + '/oauth/token'
|
||||
|
||||
data = {
|
||||
@ -152,16 +152,10 @@ def login(app, username, password):
|
||||
'scope': SCOPES,
|
||||
}
|
||||
|
||||
response = http.anon_post(url, data=data, allow_redirects=False)
|
||||
|
||||
# If auth fails, it redirects to the login page
|
||||
if response.is_redirect:
|
||||
raise AuthenticationError()
|
||||
|
||||
return response.json()
|
||||
return http.anon_post(url, data=data).json()
|
||||
|
||||
|
||||
def get_browser_login_url(app):
|
||||
def get_browser_login_url(app: App) -> str:
|
||||
"""Returns the URL for manual log in via browser"""
|
||||
return "{}/oauth/authorize/?{}".format(app.base_url, urlencode({
|
||||
"response_type": "code",
|
||||
@ -171,7 +165,7 @@ def get_browser_login_url(app):
|
||||
}))
|
||||
|
||||
|
||||
def request_access_token(app, authorization_code):
|
||||
def request_access_token(app: App, authorization_code: str):
|
||||
url = app.base_url + '/oauth/token'
|
||||
|
||||
data = {
|
||||
|
128
toot/auth.py
128
toot/auth.py
@ -1,18 +1,19 @@
|
||||
import sys
|
||||
import webbrowser
|
||||
|
||||
from builtins import input
|
||||
from getpass import getpass
|
||||
|
||||
from toot import api, config, DEFAULT_INSTANCE, User, App
|
||||
from toot import api, config, User, App
|
||||
from toot.entities import from_dict, Instance
|
||||
from toot.exceptions import ApiError, ConsoleError
|
||||
from toot.output import print_out
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
def register_app(domain, base_url):
|
||||
def find_instance(base_url: str) -> Instance:
|
||||
try:
|
||||
instance = api.get_instance(base_url).json()
|
||||
return from_dict(Instance, instance)
|
||||
except Exception:
|
||||
raise ConsoleError(f"Instance not found at {base_url}")
|
||||
|
||||
|
||||
def register_app(domain: str, base_url: str) -> App:
|
||||
try:
|
||||
print_out("Registering application...")
|
||||
response = api.create_app(base_url)
|
||||
except ApiError:
|
||||
raise ConsoleError("Registration failed.")
|
||||
@ -20,109 +21,54 @@ def register_app(domain, base_url):
|
||||
app = App(domain, base_url, response['client_id'], response['client_secret'])
|
||||
config.save_app(app)
|
||||
|
||||
print_out("Application tokens saved.")
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def create_app_interactive(base_url):
|
||||
if not base_url:
|
||||
print_out(f"Enter instance URL [<green>{DEFAULT_INSTANCE}</green>]: ", end="")
|
||||
base_url = input()
|
||||
if not base_url:
|
||||
base_url = DEFAULT_INSTANCE
|
||||
|
||||
domain = get_instance_domain(base_url)
|
||||
|
||||
def get_or_create_app(base_url: str) -> App:
|
||||
instance = find_instance(base_url)
|
||||
domain = _get_instance_domain(instance)
|
||||
return config.load_app(domain) or register_app(domain, base_url)
|
||||
|
||||
|
||||
def get_instance_domain(base_url):
|
||||
print_out("Looking up instance info...")
|
||||
|
||||
instance = api.get_instance(base_url).json()
|
||||
|
||||
print_out(
|
||||
f"Found instance <blue>{instance['title']}</blue> "
|
||||
f"running Mastodon version <yellow>{instance['version']}</yellow>"
|
||||
)
|
||||
|
||||
# Pleroma and its forks return an actual URI here, rather than a
|
||||
# domain name like Mastodon. This is contrary to the spec.¯
|
||||
# in that case, parse out the domain and return it.
|
||||
uri = instance["uri"]
|
||||
if uri.startswith("http"):
|
||||
return urlparse(uri).netloc
|
||||
|
||||
return uri
|
||||
# NB: when updating to v2 instance endpoint, this field has been renamed to `domain`
|
||||
|
||||
|
||||
def create_user(app, access_token):
|
||||
def create_user(app: App, access_token: str) -> User:
|
||||
# Username is not yet known at this point, so fetch it from Mastodon
|
||||
user = User(app.instance, None, access_token)
|
||||
creds = api.verify_credentials(app, user).json()
|
||||
|
||||
user = User(app.instance, creds['username'], access_token)
|
||||
user = User(app.instance, creds["username"], access_token)
|
||||
config.save_user(user, activate=True)
|
||||
|
||||
print_out("Access token saved to config at: <green>{}</green>".format(
|
||||
config.get_config_file_path()))
|
||||
|
||||
return user
|
||||
|
||||
|
||||
def login_interactive(app, email=None):
|
||||
print_out("Log in to <green>{}</green>".format(app.instance))
|
||||
|
||||
if email:
|
||||
print_out("Email: <green>{}</green>".format(email))
|
||||
|
||||
while not email:
|
||||
email = input('Email: ')
|
||||
|
||||
# Accept password piped from stdin, useful for testing purposes but not
|
||||
# documented so people won't get ideas. Otherwise prompt for password.
|
||||
if sys.stdin.isatty():
|
||||
password = getpass('Password: ')
|
||||
else:
|
||||
password = sys.stdin.read().strip()
|
||||
print_out("Password: <green>read from stdin</green>")
|
||||
|
||||
def login_username_password(app: App, email: str, password: str) -> User:
|
||||
try:
|
||||
print_out("Authenticating...")
|
||||
response = api.login(app, email, password)
|
||||
except ApiError:
|
||||
except Exception:
|
||||
raise ConsoleError("Login failed")
|
||||
|
||||
return create_user(app, response['access_token'])
|
||||
return create_user(app, response["access_token"])
|
||||
|
||||
|
||||
BROWSER_LOGIN_EXPLANATION = """
|
||||
This authentication method requires you to log into your Mastodon instance
|
||||
in your browser, where you will be asked to authorize <yellow>toot</yellow> to access
|
||||
your account. When you do, you will be given an <yellow>authorization code</yellow>
|
||||
which you need to paste here.
|
||||
"""
|
||||
def login_auth_code(app: App, authorization_code: str) -> User:
|
||||
try:
|
||||
response = api.request_access_token(app, authorization_code)
|
||||
except Exception:
|
||||
raise ConsoleError("Login failed")
|
||||
|
||||
return create_user(app, response["access_token"])
|
||||
|
||||
|
||||
def login_browser_interactive(app):
|
||||
url = api.get_browser_login_url(app)
|
||||
print_out(BROWSER_LOGIN_EXPLANATION)
|
||||
def _get_instance_domain(instance: Instance) -> str:
|
||||
"""Extracts the instance domain name.
|
||||
|
||||
print_out("This is the login URL:")
|
||||
print_out(url)
|
||||
print_out("")
|
||||
Pleroma and its forks return an actual URI here, rather than a domain name
|
||||
like Mastodon. This is contrary to the spec.¯ in that case, parse out the
|
||||
domain and return it.
|
||||
|
||||
yesno = input("Open link in default browser? [Y/n]")
|
||||
if not yesno or yesno.lower() == 'y':
|
||||
webbrowser.open(url)
|
||||
|
||||
authorization_code = ""
|
||||
while not authorization_code:
|
||||
authorization_code = input("Authorization code: ")
|
||||
|
||||
print_out("\nRequesting access token...")
|
||||
response = api.request_access_token(app, authorization_code)
|
||||
|
||||
return create_user(app, response['access_token'])
|
||||
TODO: when updating to v2 instance endpoint, this field has been renamed to
|
||||
`domain`
|
||||
"""
|
||||
if instance.uri.startswith("http"):
|
||||
return urlparse(instance.uri).netloc
|
||||
return instance.uri
|
||||
|
@ -1,5 +1,6 @@
|
||||
from toot.cli.base import cli, Context # noqa
|
||||
|
||||
from toot.cli.auth import *
|
||||
from toot.cli.accounts import *
|
||||
from toot.cli.lists import *
|
||||
from toot.cli.post import *
|
||||
|
@ -4,9 +4,8 @@ import json as pyjson
|
||||
from typing import BinaryIO, Optional
|
||||
|
||||
from toot import api
|
||||
from toot.cli.base import cli, json_option, Context, pass_context
|
||||
from toot.cli.base import PRIVACY_CHOICES, cli, json_option, Context, pass_context
|
||||
from toot.cli.validators import validate_language
|
||||
from toot.console import PRIVACY_CHOICES
|
||||
from toot.output import print_acct_list
|
||||
|
||||
|
||||
|
143
toot/cli/auth.py
Normal file
143
toot/cli/auth.py
Normal file
@ -0,0 +1,143 @@
|
||||
import click
|
||||
import platform
|
||||
import sys
|
||||
import webbrowser
|
||||
|
||||
from toot import api, config, __version__
|
||||
from toot.auth import get_or_create_app, login_auth_code, login_username_password
|
||||
from toot.cli.base import cli
|
||||
from toot.cli.validators import validate_instance
|
||||
|
||||
|
||||
instance_option = click.option(
|
||||
"--instance", "-i", "base_url",
|
||||
prompt="Enter instance URL",
|
||||
default="https://mastodon.social",
|
||||
callback=validate_instance,
|
||||
help="""Domain or base URL of the instance to log into,
|
||||
e.g. 'mastodon.social' or 'https://mastodon.social'""",
|
||||
)
|
||||
|
||||
|
||||
@cli.command()
|
||||
def auth():
|
||||
"""Show logged in accounts and instances"""
|
||||
config_data = config.load_config()
|
||||
|
||||
if not config_data["users"]:
|
||||
click.echo("You are not logged in to any accounts")
|
||||
return
|
||||
|
||||
active_user = config_data["active_user"]
|
||||
|
||||
click.echo("Authenticated accounts:")
|
||||
for uid, u in config_data["users"].items():
|
||||
active_label = "ACTIVE" if active_user == uid else ""
|
||||
uid = click.style(uid, fg="green")
|
||||
active_label = click.style(active_label, fg="yellow")
|
||||
click.echo(f"* {uid} {active_label}")
|
||||
|
||||
path = config.get_config_file_path()
|
||||
path = click.style(path, "blue")
|
||||
click.echo(f"\nAuth tokens are stored in: {path}")
|
||||
|
||||
|
||||
@cli.command()
|
||||
def env():
|
||||
"""Print environment information for inclusion in bug reports."""
|
||||
click.echo(f"toot {__version__}")
|
||||
click.echo(f"Python {sys.version}")
|
||||
click.echo(platform.platform())
|
||||
|
||||
|
||||
@cli.command(name="login_cli")
|
||||
@instance_option
|
||||
@click.option("--email", "-e", help="Email address to log in with", prompt=True)
|
||||
@click.option("--password", "-p", hidden=True, prompt=True, hide_input=True)
|
||||
def login_cli(base_url: str, email: str, password: str):
|
||||
"""
|
||||
Log into an instance from the console (not recommended)
|
||||
|
||||
Does NOT support two factor authentication, may not work on instances
|
||||
other than Mastodon, mostly useful for scripting.
|
||||
"""
|
||||
app = get_or_create_app(base_url)
|
||||
login_username_password(app, email, password)
|
||||
|
||||
click.secho("✓ Successfully logged in.", fg="green")
|
||||
click.echo("Access token saved to config at: ", nl=False)
|
||||
click.secho(config.get_config_file_path(), fg="green")
|
||||
|
||||
|
||||
LOGIN_EXPLANATION = """This authentication method requires you to log into your
|
||||
Mastodon instance in your browser, where you will be asked to authorize toot to
|
||||
access your account. When you do, you will be given an authorization code which
|
||||
you need to paste here.""".replace("\n", " ")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@instance_option
|
||||
def login(base_url: str):
|
||||
"""Log into an instance using your browser (recommended)"""
|
||||
app = get_or_create_app(base_url)
|
||||
url = api.get_browser_login_url(app)
|
||||
|
||||
click.echo(click.wrap_text(LOGIN_EXPLANATION))
|
||||
click.echo("\nLogin URL:")
|
||||
click.echo(url)
|
||||
|
||||
yesno = click.prompt("Open link in default browser? [Y/n]", default="Y", show_default=False)
|
||||
if not yesno or yesno.lower() == 'y':
|
||||
webbrowser.open(url)
|
||||
|
||||
authorization_code = ""
|
||||
while not authorization_code:
|
||||
authorization_code = click.prompt("Authorization code")
|
||||
|
||||
login_auth_code(app, authorization_code)
|
||||
|
||||
click.echo()
|
||||
click.secho("✓ Successfully logged in.", fg="green")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument("account", required=False)
|
||||
def logout(account: str):
|
||||
"""Log out of ACCOUNT, delete stored access keys"""
|
||||
accounts = _get_accounts_list()
|
||||
|
||||
if not account:
|
||||
raise click.ClickException(f"Specify account to log out:\n{accounts}")
|
||||
|
||||
user = config.load_user(account)
|
||||
|
||||
if not user:
|
||||
raise click.ClickException(f"Account not found. Logged in accounts:\n{accounts}")
|
||||
|
||||
config.delete_user(user)
|
||||
click.secho(f"✓ Account {account} logged out", fg="green")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument("account", required=False)
|
||||
def activate(account: str):
|
||||
"""Switch to logged in ACCOUNT."""
|
||||
accounts = _get_accounts_list()
|
||||
|
||||
if not account:
|
||||
raise click.ClickException(f"Specify account to activate:\n{accounts}")
|
||||
|
||||
user = config.load_user(account)
|
||||
|
||||
if not user:
|
||||
raise click.ClickException(f"Account not found. Logged in accounts:\n{accounts}")
|
||||
|
||||
config.activate_user(user)
|
||||
click.secho(f"✓ Account {account} activated", fg="green")
|
||||
|
||||
|
||||
def _get_accounts_list() -> str:
|
||||
accounts = config.load_config()["users"].keys()
|
||||
if not accounts:
|
||||
raise click.ClickException("You're not logged into any accounts")
|
||||
return "\n".join([f"* {acct}" for acct in accounts])
|
@ -1,12 +1,29 @@
|
||||
import logging
|
||||
import sys
|
||||
import click
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
from click.testing import Result
|
||||
from functools import wraps
|
||||
from toot import App, User, config, __version__
|
||||
from typing import Callable, Concatenate, NamedTuple, Optional, ParamSpec, TypeVar
|
||||
|
||||
|
||||
PRIVACY_CHOICES = ["public", "unlisted", "private"]
|
||||
VISIBILITY_CHOICES = ["public", "unlisted", "private", "direct"]
|
||||
|
||||
DURATION_EXAMPLES = """e.g. "1 day", "2 hours 30 minutes", "5 minutes 30
|
||||
seconds" or any combination of above. Shorthand: "1d", "2h30m", "5m30s\""""
|
||||
|
||||
|
||||
# Type alias for run commands
|
||||
Run = Callable[..., Result]
|
||||
|
||||
|
||||
def get_default_visibility() -> str:
|
||||
return os.getenv("TOOT_POST_VISIBILITY", "public")
|
||||
|
||||
|
||||
# Tweak the Click context
|
||||
# https://click.palletsprojects.com/en/8.1.x/api/#context
|
||||
CONTEXT = dict(
|
||||
|
@ -8,8 +8,8 @@ from typing import Optional, Tuple
|
||||
|
||||
from toot import api
|
||||
from toot.cli.base import cli, json_option, pass_context, Context
|
||||
from toot.cli.base import DURATION_EXAMPLES, VISIBILITY_CHOICES, get_default_visibility
|
||||
from toot.cli.validators import validate_duration, validate_language
|
||||
from toot.console import DURATION_EXAMPLES, VISIBILITY_CHOICES, get_default_visibility
|
||||
from toot.utils import EOF_KEY, delete_tmp_status_file, editor_input, multiline_input
|
||||
from toot.utils.datetime import parse_datetime
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import click
|
||||
|
||||
from toot import api
|
||||
from toot.console import VISIBILITY_CHOICES, get_default_visibility
|
||||
from toot.cli.base import cli, json_option, Context, pass_context
|
||||
from toot.cli.base import VISIBILITY_CHOICES, get_default_visibility
|
||||
from toot.output import print_table
|
||||
|
||||
|
||||
|
@ -1,8 +1,11 @@
|
||||
import click
|
||||
import re
|
||||
|
||||
from click import Context
|
||||
from typing import Optional
|
||||
|
||||
def validate_language(ctx, param, value):
|
||||
|
||||
def validate_language(ctx: Context, param: str, value: Optional[str]):
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
@ -13,7 +16,7 @@ def validate_language(ctx, param, value):
|
||||
raise click.BadParameter("Language should be a two letter abbreviation.")
|
||||
|
||||
|
||||
def validate_duration(ctx, param, value: str) -> int:
|
||||
def validate_duration(ctx: Context, param: str, value: Optional[str]) -> Optional[int]:
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
@ -43,3 +46,15 @@ def validate_duration(ctx, param, value: str) -> int:
|
||||
raise click.BadParameter("Empty duration")
|
||||
|
||||
return duration
|
||||
|
||||
|
||||
def validate_instance(ctx: click.Context, param: str, value: Optional[str]):
|
||||
"""
|
||||
Instance can be given either as a base URL or the domain name.
|
||||
Return the base URL.
|
||||
"""
|
||||
if not value:
|
||||
return None
|
||||
|
||||
value = value.rstrip("/")
|
||||
return value if value.startswith("http") else f"https://{value}"
|
||||
|
@ -3,6 +3,7 @@ import os
|
||||
|
||||
from functools import wraps
|
||||
from os.path import dirname, join
|
||||
from typing import Optional
|
||||
|
||||
from toot import User, App, get_config_dir
|
||||
from toot.exceptions import ConsoleError
|
||||
@ -85,7 +86,7 @@ def get_user_app(user_id):
|
||||
return extract_user_app(load_config(), user_id)
|
||||
|
||||
|
||||
def load_app(instance):
|
||||
def load_app(instance: str) -> Optional[App]:
|
||||
config = load_config()
|
||||
if instance in config['apps']:
|
||||
return App(**config['apps'][instance])
|
||||
|
Loading…
x
Reference in New Issue
Block a user