mirror of
https://github.com/ihabunek/toot.git
synced 2024-11-03 04:17:21 -05:00
Merge pull request #36 from ihabunek/new-config
Reimplement configuration to allow multiple logins
This commit is contained in:
commit
f976e7c818
21
README.rst
21
README.rst
@ -91,12 +91,14 @@ Running ``toot <command> -h`` shows the documentation for the given command.
|
||||
Authentication:
|
||||
toot login Log in from the console, does NOT support two factor authentication
|
||||
toot login_browser Log in using your browser, supports regular and two factor authentication
|
||||
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 Windows)
|
||||
@ -149,22 +151,13 @@ You will be redirected to your Mastodon instance to log in and authorize toot to
|
||||
|
||||
.. _instance: https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md
|
||||
|
||||
The application and user access tokens will be saved in two files in your home directory:
|
||||
The application and user access tokens will be saved in the configuration file located at ``~/.config/toot/instances/config.json``.
|
||||
|
||||
* ``~/.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 repeat the above process for another instance. You can see all logged in accounts by running ``toot auth``. The currently active account will have an **ACTIVE** flag next to it.
|
||||
|
||||
You can check whether you are currently logged in:
|
||||
To switch accounts, use ``toot activate``. Alternatively, most commands accept a ``--using`` option which can be used to specify the account you wish to use just that one time.
|
||||
|
||||
.. 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 remove the stored access tokens for that account.
|
||||
|
||||
License
|
||||
-------
|
||||
|
@ -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": "foo"})
|
||||
|
||||
user = auth.create_user(app, 2, 3)
|
||||
user = auth.create_user(app, 'abc')
|
||||
|
||||
assert_user(user)
|
||||
|
||||
|
121
tests/test_config.py
Normal file
121
tests/test_config.py
Normal file
@ -0,0 +1,121 @@
|
||||
import pytest
|
||||
|
||||
from toot import User, App, config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
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': {
|
||||
'foo@bar.social': {
|
||||
'access_token': 'mno',
|
||||
'instance': 'bar.social',
|
||||
'username': 'ihabunek'
|
||||
}
|
||||
},
|
||||
'active_user': 'foo@bar.social',
|
||||
}
|
||||
|
||||
|
||||
def test_extract_active_user_app(sample_config):
|
||||
user, app = config.extract_user_app(sample_config, sample_config['active_user'])
|
||||
|
||||
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, None)
|
||||
|
||||
# When active app does not exist for whatever reason
|
||||
sample_config['users']['foo@bar.social']['instance'] = 'does-not-exist'
|
||||
assert config.extract_user_app(sample_config, 'foo@bar.social') == (None, 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
|
@ -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": {
|
||||
"king@gizzard.social": u("king@gizzard.social"),
|
||||
"lizard@wizard.social": u("lizard@wizard.social"),
|
||||
},
|
||||
"active_user": "king@gizzard.social",
|
||||
}
|
||||
|
||||
def mock_save(config):
|
||||
assert config["users"] == {
|
||||
"lizard@wizard.social": u("lizard@wizard.social")
|
||||
}
|
||||
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", ["king@gizzard.social"])
|
||||
|
||||
out, err = capsys.readouterr()
|
||||
assert "✓ User king@gizzard.social logged out" in out
|
||||
|
||||
|
||||
def test_activate(monkeypatch, capsys):
|
||||
def mock_load():
|
||||
return {
|
||||
"users": {
|
||||
"king@gizzard.social": u("king@gizzard.social"),
|
||||
"lizard@wizard.social": u("lizard@wizard.social"),
|
||||
},
|
||||
"active_user": "king@gizzard.social",
|
||||
}
|
||||
|
||||
def mock_save(config):
|
||||
assert config["users"] == {
|
||||
"king@gizzard.social": u("king@gizzard.social"),
|
||||
"lizard@wizard.social": u("lizard@wizard.social"),
|
||||
}
|
||||
assert config["active_user"] == "lizard@wizard.social"
|
||||
|
||||
monkeypatch.setattr(config, "load_config", mock_load)
|
||||
monkeypatch.setattr(config, "save_config", mock_save)
|
||||
|
||||
console.run_command(None, None, "activate", ["lizard@wizard.social"])
|
||||
|
||||
out, err = capsys.readouterr()
|
||||
assert "✓ User lizard@wizard.social active" in out
|
||||
|
@ -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
|
||||
|
||||
|
28
toot/auth.py
28
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)
|
||||
|
||||
print_out("Access token saved to: <green>{}</green>".format(path))
|
||||
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
|
||||
|
||||
@ -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'])
|
||||
|
@ -9,7 +9,7 @@ from textwrap import TextWrapper
|
||||
from toot import api, config
|
||||
from toot.auth import login_interactive, login_browser_interactive, create_app_interactive
|
||||
from toot.exceptions import ConsoleError, NotFoundError
|
||||
from toot.output import print_out, print_instance, print_account, print_search_results
|
||||
from toot.output import print_out, print_err, print_instance, print_account, print_search_results
|
||||
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>{}</yellow>\n".format(
|
||||
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_label))
|
||||
|
||||
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(user)))
|
||||
|
||||
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):
|
||||
|
189
toot/config.py
189
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/'
|
||||
|
||||
# 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'
|
||||
from toot import User, App
|
||||
from toot.config_legacy import load_legacy_config
|
||||
from toot.exceptions import ConsoleError
|
||||
from toot.output import print_out
|
||||
|
||||
|
||||
def get_instance_config_path(instance):
|
||||
return INSTANCES_DIR + instance
|
||||
# The file holding toot configuration
|
||||
CONFIG_FILE = os.environ['HOME'] + '/.config/toot/config.json'
|
||||
|
||||
|
||||
def get_user_config_path():
|
||||
return CONFIG_USER_FILE
|
||||
def get_config_file_path():
|
||||
return CONFIG_FILE
|
||||
|
||||
|
||||
def _load(file, tuple_class):
|
||||
if not os.path.exists(file):
|
||||
return None
|
||||
|
||||
with open(file, 'r') as f:
|
||||
lines = f.read().split()
|
||||
try:
|
||||
return tuple_class(*lines)
|
||||
except TypeError:
|
||||
return None
|
||||
def user_id(user):
|
||||
return "{}@{}".format(user.username, user.instance)
|
||||
|
||||
|
||||
def _save(file, named_tuple):
|
||||
directory = os.path.dirname(file)
|
||||
if not os.path.exists(directory):
|
||||
os.makedirs(directory)
|
||||
def make_config(path):
|
||||
"""Creates a config file.
|
||||
|
||||
with open(file, 'w') as f:
|
||||
values = [v for v in named_tuple]
|
||||
f.write("\n".join(values))
|
||||
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
|
||||
|
||||
config = {
|
||||
"apps": apps,
|
||||
"users": users,
|
||||
"active_user": active_user,
|
||||
}
|
||||
|
||||
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 logged in."""
|
||||
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():
|
||||
path = get_user_config_path()
|
||||
return _load(path, User)
|
||||
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 save_app(app):
|
||||
path = get_instance_config_path(app.instance)
|
||||
_save(path, app)
|
||||
return path
|
||||
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
|
||||
|
||||
|
||||
def save_user(user):
|
||||
path = get_user_config_path()
|
||||
_save(path, user)
|
||||
return path
|
||||
@modify_config
|
||||
def save_app(config, app):
|
||||
assert isinstance(app, App)
|
||||
|
||||
config['apps'][app.instance] = app._asdict()
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def delete_app(instance):
|
||||
path = get_instance_config_path(instance)
|
||||
os.unlink(path)
|
||||
return path
|
||||
@modify_config
|
||||
def delete_app(config, app):
|
||||
assert isinstance(app, App)
|
||||
|
||||
config['apps'].pop(app.instance, None)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def delete_user():
|
||||
path = get_user_config_path()
|
||||
os.unlink(path)
|
||||
return path
|
||||
@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
|
||||
|
||||
|
||||
@modify_config
|
||||
def delete_user(config, user):
|
||||
assert isinstance(user, User)
|
||||
|
||||
config['users'].pop(user_id(user), None)
|
||||
|
||||
if config['active_user'] == user_id(user):
|
||||
config['active_user'] = None
|
||||
|
||||
return config
|
||||
|
||||
|
||||
@modify_config
|
||||
def activate_user(config, user):
|
||||
assert isinstance(user, User)
|
||||
|
||||
config['active_user'] = user_id(user)
|
||||
|
||||
return config
|
||||
|
57
toot/config_legacy.py
Normal file
57
toot/config_legacy.py
Normal file
@ -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
|
@ -38,7 +38,7 @@ common_args = [
|
||||
]
|
||||
|
||||
account_arg = (["account"], {
|
||||
"help": "account name, e.g. 'Gargron' or 'polymerwitch@toot.cat'",
|
||||
"help": "account name, e.g. 'Gargron@mastodon.social'",
|
||||
})
|
||||
|
||||
instance_arg = (["-i", "--instance"], {
|
||||
@ -62,18 +62,24 @@ AUTH_COMMANDS = [
|
||||
Command(
|
||||
name="login_browser",
|
||||
description="Log in using your browser, supports regular and two factor authentication",
|
||||
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, overrides active account")
|
||||
|
||||
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)
|
||||
|
@ -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))
|
||||
|
Loading…
Reference in New Issue
Block a user