Merge pull request #36 from ihabunek/new-config - toot - Unnamed repository; ed… | |
Log | |
Files | |
Refs | |
LICENSE | |
--- | |
commit f976e7c818ca6d1da11116758ab056c20e36e5ca | |
parent a52fdf129bc2e4d6f87e682a5c70bed2660954e6 | |
Author: Ivan Habunek <[email protected]> | |
Date: Sat, 13 Jan 2018 23:09:57 +0100 | |
Merge pull request #36 from ihabunek/new-config | |
Reimplement configuration to allow multiple logins | |
Diffstat: | |
README.rst | 21 +++++++-------------- | |
tests/test_auth.py | 10 ++++++---- | |
tests/test_config.py | 121 +++++++++++++++++++++++++++++++ | |
tests/test_console.py | 62 ++++++++++++++++++++++++++++++- | |
tests/utils.py | 1 + | |
toot/auth.py | 28 ++++++++++++++-------------- | |
toot/commands.py | 36 +++++++++++++++++++++----------- | |
toot/config.py | 183 +++++++++++++++++++++++-------- | |
toot/config_legacy.py | 57 +++++++++++++++++++++++++++++++ | |
toot/console.py | 27 +++++++++++++++++++++------ | |
toot/logging.py | 2 +- | |
11 files changed, 448 insertions(+), 100 deletions(-) | |
--- | |
diff --git a/README.rst b/README.rst | |
@@ -91,12 +91,14 @@ Running ``toot <command> -h`` shows the documentation for t… | |
Authentication: | |
toot login Log in from the console, does NOT support two facto… | |
toot login_browser Log in using your browser, supports regular and two… | |
+ toot activate Switch between logged in accounts. | |
toot logout Log out, delete stored access keys | |
- toot auth Show stored credentials | |
+ toot auth Show logged in accounts and instances | |
Read: | |
toot whoami Display logged in user details | |
toot whois Display account details | |
+ toot instance Display instance details | |
toot search Search for users or hashtags | |
toot timeline Show recent items in your public timeline | |
toot curses An experimental timeline app (doesn't work on Windo… | |
@@ -149,22 +151,13 @@ You will be redirected to your Mastodon instance to log i… | |
.. _instance: https://github.com/tootsuite/documentation/blob/master/Using-Mas… | |
-The application and user access tokens will be saved in two files in your home… | |
+The application and user access tokens will be saved in the configuration file… | |
-* ``~/.config/toot/instances/<name>`` - created for each mastodon instance once | |
-* ``~/.config/toot/user.cfg`` | |
+It's possible to be logged into **multiple accounts** at the same time. Just r… | |
-You can check whether you are currently logged in: | |
+To switch accounts, use ``toot activate``. Alternatively, most commands accept… | |
-.. code-block:: | |
- | |
- toot auth | |
- | |
-And you can logout which will remove the stored access tokens: | |
- | |
-.. code-block:: | |
- | |
- toot logout | |
+Finally you can logout from an account by using ``toot logout``. This will rem… | |
License | |
------- | |
diff --git a/tests/test_auth.py b/tests/test_auth.py | |
@@ -41,15 +41,17 @@ def test_create_app_registered(monkeypatch): | |
def test_create_user(monkeypatch): | |
app = App(4, 5, 6, 7) | |
- def assert_user(user): | |
+ def assert_user(user, activate=True): | |
+ assert activate | |
assert isinstance(user, User) | |
assert user.instance == app.instance | |
- assert user.username == 2 | |
- assert user.access_token == 3 | |
+ assert user.username == "foo" | |
+ assert user.access_token == "abc" | |
monkeypatch.setattr(config, 'save_user', assert_user) | |
+ monkeypatch.setattr(api, 'verify_credentials', lambda x, y: {"username": "… | |
- user = auth.create_user(app, 2, 3) | |
+ user = auth.create_user(app, 'abc') | |
assert_user(user) | |
diff --git a/tests/test_config.py b/tests/test_config.py | |
@@ -0,0 +1,121 @@ | |
+import pytest | |
+ | |
+from toot import User, App, config | |
+ | |
+ | |
[email protected] | |
+def sample_config(): | |
+ return { | |
+ 'apps': { | |
+ 'foo.social': { | |
+ 'base_url': 'https://foo.social', | |
+ 'client_id': 'abc', | |
+ 'client_secret': 'def', | |
+ 'instance': 'foo.social' | |
+ }, | |
+ 'bar.social': { | |
+ 'base_url': 'https://bar.social', | |
+ 'client_id': 'ghi', | |
+ 'client_secret': 'jkl', | |
+ 'instance': 'bar.social' | |
+ }, | |
+ }, | |
+ 'users': { | |
+ '[email protected]': { | |
+ 'access_token': 'mno', | |
+ 'instance': 'bar.social', | |
+ 'username': 'ihabunek' | |
+ } | |
+ }, | |
+ 'active_user': '[email protected]', | |
+ } | |
+ | |
+ | |
+def test_extract_active_user_app(sample_config): | |
+ user, app = config.extract_user_app(sample_config, sample_config['active_u… | |
+ | |
+ assert isinstance(user, User) | |
+ assert user.instance == 'bar.social' | |
+ assert user.username == 'ihabunek' | |
+ assert user.access_token == 'mno' | |
+ | |
+ assert isinstance(app, App) | |
+ assert app.instance == 'bar.social' | |
+ assert app.base_url == 'https://bar.social' | |
+ assert app.client_id == 'ghi' | |
+ assert app.client_secret == 'jkl' | |
+ | |
+ | |
+def test_extract_active_when_no_active_user(sample_config): | |
+ # When there is no active user | |
+ assert config.extract_user_app(sample_config, None) == (None, None) | |
+ | |
+ # When active user does not exist for whatever reason | |
+ assert config.extract_user_app(sample_config, 'does-not-exist') == (None, … | |
+ | |
+ # When active app does not exist for whatever reason | |
+ sample_config['users']['[email protected]']['instance'] = 'does-not-exist' | |
+ assert config.extract_user_app(sample_config, '[email protected]') == (None, … | |
+ | |
+ | |
+def test_save_app(sample_config): | |
+ app = App('xxx.yyy', 2, 3, 4) | |
+ app2 = App('moo.foo', 5, 6, 7) | |
+ | |
+ app_count = len(sample_config['apps']) | |
+ assert 'xxx.yyy' not in sample_config['apps'] | |
+ assert 'moo.foo' not in sample_config['apps'] | |
+ | |
+ # Sets | |
+ config.save_app.__wrapped__(sample_config, app) | |
+ assert len(sample_config['apps']) == app_count + 1 | |
+ assert 'xxx.yyy' in sample_config['apps'] | |
+ assert sample_config['apps']['xxx.yyy']['instance'] == 'xxx.yyy' | |
+ assert sample_config['apps']['xxx.yyy']['base_url'] == 2 | |
+ assert sample_config['apps']['xxx.yyy']['client_id'] == 3 | |
+ assert sample_config['apps']['xxx.yyy']['client_secret'] == 4 | |
+ | |
+ # Overwrites | |
+ config.save_app.__wrapped__(sample_config, app2) | |
+ assert len(sample_config['apps']) == app_count + 2 | |
+ assert 'xxx.yyy' in sample_config['apps'] | |
+ assert 'moo.foo' in sample_config['apps'] | |
+ assert sample_config['apps']['xxx.yyy']['instance'] == 'xxx.yyy' | |
+ assert sample_config['apps']['xxx.yyy']['base_url'] == 2 | |
+ assert sample_config['apps']['xxx.yyy']['client_id'] == 3 | |
+ assert sample_config['apps']['xxx.yyy']['client_secret'] == 4 | |
+ assert sample_config['apps']['moo.foo']['instance'] == 'moo.foo' | |
+ assert sample_config['apps']['moo.foo']['base_url'] == 5 | |
+ assert sample_config['apps']['moo.foo']['client_id'] == 6 | |
+ assert sample_config['apps']['moo.foo']['client_secret'] == 7 | |
+ | |
+ # Idempotent | |
+ config.save_app.__wrapped__(sample_config, app2) | |
+ assert len(sample_config['apps']) == app_count + 2 | |
+ assert 'xxx.yyy' in sample_config['apps'] | |
+ assert 'moo.foo' in sample_config['apps'] | |
+ assert sample_config['apps']['xxx.yyy']['instance'] == 'xxx.yyy' | |
+ assert sample_config['apps']['xxx.yyy']['base_url'] == 2 | |
+ assert sample_config['apps']['xxx.yyy']['client_id'] == 3 | |
+ assert sample_config['apps']['xxx.yyy']['client_secret'] == 4 | |
+ assert sample_config['apps']['moo.foo']['instance'] == 'moo.foo' | |
+ assert sample_config['apps']['moo.foo']['base_url'] == 5 | |
+ assert sample_config['apps']['moo.foo']['client_id'] == 6 | |
+ assert sample_config['apps']['moo.foo']['client_secret'] == 7 | |
+ | |
+ | |
+def test_delete_app(sample_config): | |
+ app = App('foo.social', 2, 3, 4) | |
+ | |
+ app_count = len(sample_config['apps']) | |
+ | |
+ assert 'foo.social' in sample_config['apps'] | |
+ | |
+ config.delete_app.__wrapped__(sample_config, app) | |
+ assert 'foo.social' not in sample_config['apps'] | |
+ assert len(sample_config['apps']) == app_count - 1 | |
+ | |
+ # Idempotent | |
+ config.delete_app.__wrapped__(sample_config, app) | |
+ assert 'foo.social' not in sample_config['apps'] | |
+ assert len(sample_config['apps']) == app_count - 1 | |
diff --git a/tests/test_console.py b/tests/test_console.py | |
@@ -5,7 +5,7 @@ import re | |
from requests import Request | |
-from toot import console, User, App | |
+from toot import config, console, User, App | |
from toot.exceptions import ConsoleError | |
from tests.utils import MockResponse, Expectations | |
@@ -292,3 +292,63 @@ def test_whoami(monkeypatch, capsys): | |
assert "Followers: 5" in out | |
assert "Following: 9" in out | |
assert "Statuses: 19" in out | |
+ | |
+ | |
+def u(user_id, access_token="abc"): | |
+ username, instance = user_id.split("@") | |
+ return { | |
+ "instance": instance, | |
+ "username": username, | |
+ "access_token": access_token, | |
+ } | |
+ | |
+ | |
+def test_logout(monkeypatch, capsys): | |
+ def mock_load(): | |
+ return { | |
+ "users": { | |
+ "[email protected]": u("[email protected]"), | |
+ "[email protected]": u("[email protected]"), | |
+ }, | |
+ "active_user": "[email protected]", | |
+ } | |
+ | |
+ def mock_save(config): | |
+ assert config["users"] == { | |
+ "[email protected]": u("[email protected]") | |
+ } | |
+ assert config["active_user"] is None | |
+ | |
+ monkeypatch.setattr(config, "load_config", mock_load) | |
+ monkeypatch.setattr(config, "save_config", mock_save) | |
+ | |
+ console.run_command(None, None, "logout", ["[email protected]"]) | |
+ | |
+ out, err = capsys.readouterr() | |
+ assert "✓ User [email protected] logged out" in out | |
+ | |
+ | |
+def test_activate(monkeypatch, capsys): | |
+ def mock_load(): | |
+ return { | |
+ "users": { | |
+ "[email protected]": u("[email protected]"), | |
+ "[email protected]": u("[email protected]"), | |
+ }, | |
+ "active_user": "[email protected]", | |
+ } | |
+ | |
+ def mock_save(config): | |
+ assert config["users"] == { | |
+ "[email protected]": u("[email protected]"), | |
+ "[email protected]": u("[email protected]"), | |
+ } | |
+ assert config["active_user"] == "[email protected]" | |
+ | |
+ monkeypatch.setattr(config, "load_config", mock_load) | |
+ monkeypatch.setattr(config, "save_config", mock_save) | |
+ | |
+ console.run_command(None, None, "activate", ["[email protected]"]) | |
+ | |
+ out, err = capsys.readouterr() | |
+ assert "✓ User [email protected] active" in out | |
diff --git a/tests/utils.py b/tests/utils.py | |
@@ -30,6 +30,7 @@ class Expectations(): | |
class MockResponse: | |
def __init__(self, response_data={}, ok=True, is_redirect=False): | |
self.response_data = response_data | |
+ self.content = response_data | |
self.ok = ok | |
self.is_redirect = is_redirect | |
diff --git a/toot/auth.py b/toot/auth.py | |
@@ -26,8 +26,9 @@ def register_app(domain): | |
base_url = 'https://' + domain | |
app = App(domain, base_url, response['client_id'], response['client_secret… | |
- path = config.save_app(app) | |
- print_out("Application tokens saved to: <green>{}</green>\n".format(path)) | |
+ config.save_app(app) | |
+ | |
+ print_out("Application tokens saved.") | |
return app | |
@@ -42,11 +43,16 @@ def create_app_interactive(instance=None): | |
return config.load_app(instance) or register_app(instance) | |
-def create_user(app, email, access_token): | |
- user = User(app.instance, email, access_token) | |
- path = config.save_user(user) | |
+def create_user(app, access_token): | |
+ # 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) | |
+ | |
+ user = User(app.instance, creds['username'], access_token) | |
+ config.save_user(user, activate=True) | |
- print_out("Access token saved to: <green>{}</green>".format(path)) | |
+ print_out("Access token saved to config at: <green>{}</green>".format( | |
+ config.get_config_file_path())) | |
return user | |
@@ -68,7 +74,7 @@ def login_interactive(app, email=None): | |
except ApiError: | |
raise ConsoleError("Login failed") | |
- return create_user(app, email, response['access_token']) | |
+ return create_user(app, response['access_token']) | |
BROWSER_LOGIN_EXPLANATION = """ | |
@@ -81,7 +87,6 @@ which you need to paste here. | |
def login_browser_interactive(app): | |
url = api.get_browser_login_url(app) | |
- | |
print_out(BROWSER_LOGIN_EXPLANATION) | |
print_out("This is the login URL:") | |
@@ -99,9 +104,4 @@ def login_browser_interactive(app): | |
print_out("\nRequesting access token...") | |
response = api.request_access_token(app, authorization_code) | |
- # TODO: user email is not available in this workflow, maybe change the User | |
- # to store the username instead? Currently set to "unknown" since it's not | |
- # used anywhere. | |
- email = "unknown" | |
- | |
- return create_user(app, email, response['access_token']) | |
+ return create_user(app, response['access_token']) | |
diff --git a/toot/commands.py b/toot/commands.py | |
@@ -9,7 +9,7 @@ from textwrap import TextWrapper | |
from toot import api, config | |
from toot.auth import login_interactive, login_browser_interactive, create_app… | |
from toot.exceptions import ConsoleError, NotFoundError | |
-from toot.output import print_out, print_instance, print_account, print_search… | |
+from toot.output import print_out, print_err, print_instance, print_account, p… | |
from toot.utils import assert_domain_exists | |
@@ -89,15 +89,21 @@ def post(app, user, args): | |
def auth(app, user, args): | |
- if app and user: | |
- print_out("You are logged in to <yellow>{}</yellow> as <yellow>{}</yel… | |
- app.instance, user.username)) | |
- print_out("User data: <green>{}</green>".format( | |
- config.get_user_config_path())) | |
- print_out("App data: <green>{}</green>".format( | |
- config.get_instance_config_path(app.instance))) | |
- else: | |
- print_out("You are not logged in") | |
+ 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… | |
+ | |
+ path = config.get_config_file_path() | |
+ print_out("\nAuth tokens are stored in: <blue>{}</blue>".format(path)) | |
def login(app, user, args): | |
@@ -117,9 +123,15 @@ def login_browser(app, user, args): | |
def logout(app, user, args): | |
- config.delete_user() | |
+ user = config.load_user(args.account, throw=True) | |
+ config.delete_user(user) | |
+ print_out("<green>✓ User {} logged out</green>".format(config.user_id(us… | |
+ | |
- print_out("<green>✓ You are now logged out.</green>") | |
+def activate(app, user, args): | |
+ 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): | |
diff --git a/toot/config.py b/toot/config.py | |
@@ -1,78 +1,165 @@ | |
# -*- coding: utf-8 -*- | |
import os | |
+import json | |
-from . import User, App | |
+from functools import wraps | |
-# The dir where all toot configuration is stored | |
-CONFIG_DIR = os.environ['HOME'] + '/.config/toot/' | |
+from toot import User, App | |
+from toot.config_legacy import load_legacy_config | |
+from toot.exceptions import ConsoleError | |
+from toot.output import print_out | |
-# Subfolder where application access keys for various instances are stored | |
-INSTANCES_DIR = CONFIG_DIR + 'instances/' | |
-# File in which user access token is stored | |
-CONFIG_USER_FILE = CONFIG_DIR + 'user.cfg' | |
+# The file holding toot configuration | |
+CONFIG_FILE = os.environ['HOME'] + '/.config/toot/config.json' | |
-def get_instance_config_path(instance): | |
- return INSTANCES_DIR + instance | |
+def get_config_file_path(): | |
+ return CONFIG_FILE | |
-def get_user_config_path(): | |
- return CONFIG_USER_FILE | |
+def user_id(user): | |
+ return "{}@{}".format(user.username, user.instance) | |
-def _load(file, tuple_class): | |
- if not os.path.exists(file): | |
- return None | |
+def make_config(path): | |
+ """Creates a config file. | |
- with open(file, 'r') as f: | |
- lines = f.read().split() | |
- try: | |
- return tuple_class(*lines) | |
- except TypeError: | |
- return None | |
+ Attempts to load data from legacy config files if they exist. | |
+ """ | |
+ apps, user = load_legacy_config() | |
+ apps = {a.instance: a._asdict() for a in apps} | |
+ users = {user_id(user): user._asdict()} if user else {} | |
+ active_user = user_id(user) if user else None | |
-def _save(file, named_tuple): | |
- directory = os.path.dirname(file) | |
- if not os.path.exists(directory): | |
- os.makedirs(directory) | |
+ config = { | |
+ "apps": apps, | |
+ "users": users, | |
+ "active_user": active_user, | |
+ } | |
- with open(file, 'w') as f: | |
- values = [v for v in named_tuple] | |
- f.write("\n".join(values)) | |
+ print_out("Creating config file at <blue>{}</blue>".format(path)) | |
+ with open(path, 'w') as f: | |
+ json.dump(config, f, indent=True) | |
+ | |
+ | |
+def load_config(): | |
+ if not os.path.exists(CONFIG_FILE): | |
+ make_config(CONFIG_FILE) | |
+ | |
+ with open(CONFIG_FILE) as f: | |
+ return json.load(f) | |
+ | |
+ | |
+def save_config(config): | |
+ with open(CONFIG_FILE, 'w') as f: | |
+ return json.dump(config, f, indent=True) | |
+ | |
+ | |
+def extract_user_app(config, user_id): | |
+ if user_id not in config['users']: | |
+ return None, None | |
+ | |
+ user_data = config['users'][user_id] | |
+ instance = user_data['instance'] | |
+ | |
+ if instance not in config['apps']: | |
+ return None, None | |
+ | |
+ app_data = config['apps'][instance] | |
+ return User(**user_data), App(**app_data) | |
+ | |
+ | |
+def get_active_user_app(): | |
+ """Returns (User, App) of active user or (None, None) if no user is active… | |
+ config = load_config() | |
+ | |
+ if config['active_user']: | |
+ return extract_user_app(config, config['active_user']) | |
+ | |
+ return None, None | |
+ | |
+ | |
+def get_user_app(user_id): | |
+ """Returns (User, App) for given user ID or (None, None) if user is not lo… | |
+ return extract_user_app(load_config(), user_id) | |
def load_app(instance): | |
- path = get_instance_config_path(instance) | |
- return _load(path, App) | |
+ config = load_config() | |
+ if instance in config['apps']: | |
+ return App(**config['apps'][instance]) | |
+ | |
+ | |
+def load_user(user_id, throw=False): | |
+ config = load_config() | |
+ | |
+ if user_id in config['users']: | |
+ return User(**config['users'][user_id]) | |
+ | |
+ if throw: | |
+ raise ConsoleError("User '{}' not found".format(user_id)) | |
+ | |
+ | |
+def modify_config(f): | |
+ @wraps(f) | |
+ def wrapper(*args, **kwargs): | |
+ config = load_config() | |
+ config = f(config, *args, **kwargs) | |
+ save_config(config) | |
+ return config | |
+ | |
+ return wrapper | |
+ | |
+ | |
+@modify_config | |
+def save_app(config, app): | |
+ assert isinstance(app, App) | |
+ | |
+ config['apps'][app.instance] = app._asdict() | |
+ | |
+ return config | |
+ | |
+ | |
+@modify_config | |
+def delete_app(config, app): | |
+ assert isinstance(app, App) | |
+ | |
+ config['apps'].pop(app.instance, None) | |
+ | |
+ return config | |
+ | |
+ | |
+@modify_config | |
+def save_user(config, user, activate=True): | |
+ assert isinstance(user, User) | |
+ | |
+ config['users'][user_id(user)] = user._asdict() | |
+ | |
+ if activate: | |
+ config['active_user'] = user_id(user) | |
+ return config | |
-def load_user(): | |
- path = get_user_config_path() | |
- return _load(path, User) | |
+@modify_config | |
+def delete_user(config, user): | |
+ assert isinstance(user, User) | |
-def save_app(app): | |
- path = get_instance_config_path(app.instance) | |
- _save(path, app) | |
- return path | |
+ config['users'].pop(user_id(user), None) | |
+ if config['active_user'] == user_id(user): | |
+ config['active_user'] = None | |
-def save_user(user): | |
- path = get_user_config_path() | |
- _save(path, user) | |
- return path | |
+ return config | |
-def delete_app(instance): | |
- path = get_instance_config_path(instance) | |
- os.unlink(path) | |
- return path | |
+@modify_config | |
+def activate_user(config, user): | |
+ assert isinstance(user, User) | |
+ config['active_user'] = user_id(user) | |
-def delete_user(): | |
- path = get_user_config_path() | |
- os.unlink(path) | |
- return path | |
+ return config | |
diff --git a/toot/config_legacy.py b/toot/config_legacy.py | |
@@ -0,0 +1,57 @@ | |
+# -*- coding: utf-8 -*- | |
+ | |
+import os | |
+ | |
+from . import User, App | |
+ | |
+# The dir where all toot configuration is stored | |
+CONFIG_DIR = os.environ['HOME'] + '/.config/toot/' | |
+ | |
+# Subfolder where application access keys for various instances are stored | |
+INSTANCES_DIR = CONFIG_DIR + 'instances/' | |
+ | |
+# File in which user access token is stored | |
+CONFIG_USER_FILE = CONFIG_DIR + 'user.cfg' | |
+ | |
+ | |
+def load_user(path): | |
+ if not os.path.exists(path): | |
+ return None | |
+ | |
+ with open(path, 'r') as f: | |
+ lines = f.read().split() | |
+ return User(*lines) | |
+ | |
+ | |
+def load_apps(path): | |
+ if not os.path.exists(path): | |
+ return [] | |
+ | |
+ for name in os.listdir(path): | |
+ with open(path + name) as f: | |
+ values = f.read().split() | |
+ yield App(*values) | |
+ | |
+ | |
+def add_username(user, apps): | |
+ """When using broser login, username was not stored so look it up""" | |
+ if not user: | |
+ return None | |
+ | |
+ apps = [a for a in apps if a.instance == user.instance] | |
+ | |
+ if not apps: | |
+ return None | |
+ | |
+ from toot.api import verify_credentials | |
+ creds = verify_credentials(apps.pop(), user) | |
+ | |
+ return User(user.instance, creds['username'], user.access_token) | |
+ | |
+ | |
+def load_legacy_config(): | |
+ apps = list(load_apps(INSTANCES_DIR)) | |
+ user = load_user(CONFIG_USER_FILE) | |
+ user = add_username(user, apps) | |
+ | |
+ return apps, user | |
diff --git a/toot/console.py b/toot/console.py | |
@@ -38,7 +38,7 @@ common_args = [ | |
] | |
account_arg = (["account"], { | |
- "help": "account name, e.g. 'Gargron' or '[email protected]'", | |
+ "help": "account name, e.g. '[email protected]'", | |
}) | |
instance_arg = (["-i", "--instance"], { | |
@@ -62,18 +62,24 @@ AUTH_COMMANDS = [ | |
Command( | |
name="login_browser", | |
description="Log in using your browser, supports regular and two facto… | |
- arguments=[instance_arg, email_arg], | |
+ arguments=[instance_arg], | |
+ require_auth=False, | |
+ ), | |
+ Command( | |
+ name="activate", | |
+ description="Switch between logged in accounts.", | |
+ arguments=[account_arg], | |
require_auth=False, | |
), | |
Command( | |
name="logout", | |
description="Log out, delete stored access keys", | |
- arguments=[], | |
+ arguments=[account_arg], | |
require_auth=False, | |
), | |
Command( | |
name="auth", | |
- description="Show stored credentials", | |
+ description="Show logged in accounts and instances", | |
arguments=[], | |
require_auth=False, | |
), | |
@@ -261,6 +267,10 @@ def get_argument_parser(name, command): | |
for args, kwargs in command.arguments + common_args: | |
parser.add_argument(*args, **kwargs) | |
+ # If the command requires auth, give an option to select account | |
+ if command.require_auth: | |
+ parser.add_argument("-u", "--using", help="the account to use, overrid… | |
+ | |
return parser | |
@@ -275,6 +285,12 @@ def run_command(app, user, name, args): | |
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.") | |
@@ -305,8 +321,7 @@ def main(): | |
if not command_name: | |
return print_usage() | |
- user = config.load_user() | |
- app = config.load_app(user.instance) if user else None | |
+ user, app = config.get_active_user_app() | |
try: | |
run_command(app, user, command_name, args) | |
diff --git a/toot/logging.py b/toot/logging.py | |
@@ -22,7 +22,7 @@ def log_request(request): | |
def log_response(response): | |
if response.ok: | |
logger.debug("<<< \033[32m{}\033[0m".format(response)) | |
- logger.debug("<<< \033[33m{}\033[0m".format(response.json())) | |
+ logger.debug("<<< \033[33m{}\033[0m".format(response.content)) | |
else: | |
logger.debug("<<< \033[31m{}\033[0m".format(response)) | |
logger.debug("<<< \033[31m{}\033[0m".format(response.content)) |