mirror of
https://github.com/ihabunek/toot
synced 2025-02-02 20:36:45 +01:00
Reimplement configuration to allow multiple logins
The configuration is now stored in a single json encoded file instead of separate files.
This commit is contained in:
parent
647a896ab5
commit
6a3c877270
21
README.rst
21
README.rst
@ -81,12 +81,14 @@ Running ``toot <command> -h`` shows the documentation for the given command.
|
|||||||
Authentication:
|
Authentication:
|
||||||
toot login Log in from the console, does NOT support two factor 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 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 logout Log out, delete stored access keys
|
||||||
toot auth Show stored credentials
|
toot auth Show logged in accounts and instances
|
||||||
|
|
||||||
Read:
|
Read:
|
||||||
toot whoami Display logged in user details
|
toot whoami Display logged in user details
|
||||||
toot whois Display account details
|
toot whois Display account details
|
||||||
|
toot instance Display instance details
|
||||||
toot search Search for users or hashtags
|
toot search Search for users or hashtags
|
||||||
toot timeline Show recent items in your public timeline
|
toot timeline Show recent items in your public timeline
|
||||||
toot curses An experimental timeline app (doesn't work on Windows)
|
toot curses An experimental timeline app (doesn't work on Windows)
|
||||||
@ -139,22 +141,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
|
.. _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
|
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.
|
||||||
* ``~/.config/toot/user.cfg``
|
|
||||||
|
|
||||||
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::
|
Finally you can logout from an account by using ``toot logout``. This will remove the stored access tokens for that account.
|
||||||
|
|
||||||
toot auth
|
|
||||||
|
|
||||||
And you can logout which will remove the stored access tokens:
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
|
|
||||||
toot logout
|
|
||||||
|
|
||||||
License
|
License
|
||||||
-------
|
-------
|
||||||
|
@ -41,15 +41,17 @@ def test_create_app_registered(monkeypatch):
|
|||||||
def test_create_user(monkeypatch):
|
def test_create_user(monkeypatch):
|
||||||
app = App(4, 5, 6, 7)
|
app = App(4, 5, 6, 7)
|
||||||
|
|
||||||
def assert_user(user):
|
def assert_user(user, activate=True):
|
||||||
|
assert activate
|
||||||
assert isinstance(user, User)
|
assert isinstance(user, User)
|
||||||
assert user.instance == app.instance
|
assert user.instance == app.instance
|
||||||
assert user.username == 2
|
assert user.username == "foo"
|
||||||
assert user.access_token == 3
|
assert user.access_token == "abc"
|
||||||
|
|
||||||
monkeypatch.setattr(config, 'save_user', assert_user)
|
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)
|
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 requests import Request
|
||||||
|
|
||||||
from toot import console, User, App
|
from toot import config, console, User, App
|
||||||
from toot.exceptions import ConsoleError
|
from toot.exceptions import ConsoleError
|
||||||
|
|
||||||
from tests.utils import MockResponse, Expectations
|
from tests.utils import MockResponse, Expectations
|
||||||
@ -292,3 +292,63 @@ def test_whoami(monkeypatch, capsys):
|
|||||||
assert "Followers: 5" in out
|
assert "Followers: 5" in out
|
||||||
assert "Following: 9" in out
|
assert "Following: 9" in out
|
||||||
assert "Statuses: 19" 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:
|
class MockResponse:
|
||||||
def __init__(self, response_data={}, ok=True, is_redirect=False):
|
def __init__(self, response_data={}, ok=True, is_redirect=False):
|
||||||
self.response_data = response_data
|
self.response_data = response_data
|
||||||
|
self.content = response_data
|
||||||
self.ok = ok
|
self.ok = ok
|
||||||
self.is_redirect = is_redirect
|
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
|
base_url = 'https://' + domain
|
||||||
|
|
||||||
app = App(domain, base_url, response['client_id'], response['client_secret'])
|
app = App(domain, base_url, response['client_id'], response['client_secret'])
|
||||||
path = config.save_app(app)
|
config.save_app(app)
|
||||||
print_out("Application tokens saved to: <green>{}</green>\n".format(path))
|
|
||||||
|
print_out("Application tokens saved.")
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
@ -42,11 +43,16 @@ def create_app_interactive(instance=None):
|
|||||||
return config.load_app(instance) or register_app(instance)
|
return config.load_app(instance) or register_app(instance)
|
||||||
|
|
||||||
|
|
||||||
def create_user(app, email, access_token):
|
def create_user(app, access_token):
|
||||||
user = User(app.instance, email, access_token)
|
# Username is not yet known at this point, so fetch it from Mastodon
|
||||||
path = config.save_user(user)
|
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
|
return user
|
||||||
|
|
||||||
@ -68,7 +74,7 @@ def login_interactive(app, email=None):
|
|||||||
except ApiError:
|
except ApiError:
|
||||||
raise ConsoleError("Login failed")
|
raise ConsoleError("Login failed")
|
||||||
|
|
||||||
return create_user(app, email, response['access_token'])
|
return create_user(app, response['access_token'])
|
||||||
|
|
||||||
|
|
||||||
BROWSER_LOGIN_EXPLANATION = """
|
BROWSER_LOGIN_EXPLANATION = """
|
||||||
@ -81,7 +87,6 @@ which you need to paste here.
|
|||||||
|
|
||||||
def login_browser_interactive(app):
|
def login_browser_interactive(app):
|
||||||
url = api.get_browser_login_url(app)
|
url = api.get_browser_login_url(app)
|
||||||
|
|
||||||
print_out(BROWSER_LOGIN_EXPLANATION)
|
print_out(BROWSER_LOGIN_EXPLANATION)
|
||||||
|
|
||||||
print_out("This is the login URL:")
|
print_out("This is the login URL:")
|
||||||
@ -99,9 +104,4 @@ def login_browser_interactive(app):
|
|||||||
print_out("\nRequesting access token...")
|
print_out("\nRequesting access token...")
|
||||||
response = api.request_access_token(app, authorization_code)
|
response = api.request_access_token(app, authorization_code)
|
||||||
|
|
||||||
# TODO: user email is not available in this workflow, maybe change the User
|
return create_user(app, response['access_token'])
|
||||||
# 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'])
|
|
||||||
|
@ -9,7 +9,7 @@ from textwrap import TextWrapper
|
|||||||
from toot import api, config
|
from toot import api, config
|
||||||
from toot.auth import login_interactive, login_browser_interactive, create_app_interactive
|
from toot.auth import login_interactive, login_browser_interactive, create_app_interactive
|
||||||
from toot.exceptions import ConsoleError, NotFoundError
|
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
|
from toot.utils import assert_domain_exists
|
||||||
|
|
||||||
|
|
||||||
@ -89,15 +89,21 @@ def post(app, user, args):
|
|||||||
|
|
||||||
|
|
||||||
def auth(app, user, args):
|
def auth(app, user, args):
|
||||||
if app and user:
|
config_data = config.load_config()
|
||||||
print_out("You are logged in to <yellow>{}</yellow> as <yellow>{}</yellow>\n".format(
|
|
||||||
app.instance, user.username))
|
if not config_data["users"]:
|
||||||
print_out("User data: <green>{}</green>".format(
|
print_out("You are not logged in to any accounts")
|
||||||
config.get_user_config_path()))
|
return
|
||||||
print_out("App data: <green>{}</green>".format(
|
|
||||||
config.get_instance_config_path(app.instance)))
|
active_user = config_data["active_user"]
|
||||||
else:
|
|
||||||
print_out("You are not logged in")
|
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):
|
def login(app, user, args):
|
||||||
@ -117,9 +123,15 @@ def login_browser(app, user, args):
|
|||||||
|
|
||||||
|
|
||||||
def logout(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):
|
def upload(app, user, args):
|
||||||
|
189
toot/config.py
189
toot/config.py
@ -1,78 +1,165 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
from . import User, App
|
from functools import wraps
|
||||||
|
|
||||||
# The dir where all toot configuration is stored
|
from toot import User, App
|
||||||
CONFIG_DIR = os.environ['HOME'] + '/.config/toot/'
|
from toot.config_legacy import load_legacy_config
|
||||||
|
from toot.exceptions import ConsoleError
|
||||||
# Subfolder where application access keys for various instances are stored
|
from toot.output import print_out
|
||||||
INSTANCES_DIR = CONFIG_DIR + 'instances/'
|
|
||||||
|
|
||||||
# File in which user access token is stored
|
|
||||||
CONFIG_USER_FILE = CONFIG_DIR + 'user.cfg'
|
|
||||||
|
|
||||||
|
|
||||||
def get_instance_config_path(instance):
|
# The file holding toot configuration
|
||||||
return INSTANCES_DIR + instance
|
CONFIG_FILE = os.environ['HOME'] + '/.config/toot/config.json'
|
||||||
|
|
||||||
|
|
||||||
def get_user_config_path():
|
def get_config_file_path():
|
||||||
return CONFIG_USER_FILE
|
return CONFIG_FILE
|
||||||
|
|
||||||
|
|
||||||
def _load(file, tuple_class):
|
def user_id(user):
|
||||||
if not os.path.exists(file):
|
return "{}@{}".format(user.username, user.instance)
|
||||||
return None
|
|
||||||
|
|
||||||
with open(file, 'r') as f:
|
|
||||||
lines = f.read().split()
|
|
||||||
try:
|
|
||||||
return tuple_class(*lines)
|
|
||||||
except TypeError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _save(file, named_tuple):
|
def make_config(path):
|
||||||
directory = os.path.dirname(file)
|
"""Creates a config file.
|
||||||
if not os.path.exists(directory):
|
|
||||||
os.makedirs(directory)
|
|
||||||
|
|
||||||
with open(file, 'w') as f:
|
Attempts to load data from legacy config files if they exist.
|
||||||
values = [v for v in named_tuple]
|
"""
|
||||||
f.write("\n".join(values))
|
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):
|
def load_app(instance):
|
||||||
path = get_instance_config_path(instance)
|
config = load_config()
|
||||||
return _load(path, App)
|
if instance in config['apps']:
|
||||||
|
return App(**config['apps'][instance])
|
||||||
|
|
||||||
|
|
||||||
def load_user():
|
def load_user(user_id, throw=False):
|
||||||
path = get_user_config_path()
|
config = load_config()
|
||||||
return _load(path, User)
|
|
||||||
|
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):
|
def modify_config(f):
|
||||||
path = get_instance_config_path(app.instance)
|
@wraps(f)
|
||||||
_save(path, app)
|
def wrapper(*args, **kwargs):
|
||||||
return path
|
config = load_config()
|
||||||
|
config = f(config, *args, **kwargs)
|
||||||
|
save_config(config)
|
||||||
|
return config
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
def save_user(user):
|
@modify_config
|
||||||
path = get_user_config_path()
|
def save_app(config, app):
|
||||||
_save(path, user)
|
assert isinstance(app, App)
|
||||||
return path
|
|
||||||
|
config['apps'][app.instance] = app._asdict()
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
def delete_app(instance):
|
@modify_config
|
||||||
path = get_instance_config_path(instance)
|
def delete_app(config, app):
|
||||||
os.unlink(path)
|
assert isinstance(app, App)
|
||||||
return path
|
|
||||||
|
config['apps'].pop(app.instance, None)
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
def delete_user():
|
@modify_config
|
||||||
path = get_user_config_path()
|
def save_user(config, user, activate=True):
|
||||||
os.unlink(path)
|
assert isinstance(user, User)
|
||||||
return path
|
|
||||||
|
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"], {
|
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"], {
|
instance_arg = (["-i", "--instance"], {
|
||||||
@ -62,18 +62,24 @@ AUTH_COMMANDS = [
|
|||||||
Command(
|
Command(
|
||||||
name="login_browser",
|
name="login_browser",
|
||||||
description="Log in using your browser, supports regular and two factor authentication",
|
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,
|
require_auth=False,
|
||||||
),
|
),
|
||||||
Command(
|
Command(
|
||||||
name="logout",
|
name="logout",
|
||||||
description="Log out, delete stored access keys",
|
description="Log out, delete stored access keys",
|
||||||
arguments=[],
|
arguments=[account_arg],
|
||||||
require_auth=False,
|
require_auth=False,
|
||||||
),
|
),
|
||||||
Command(
|
Command(
|
||||||
name="auth",
|
name="auth",
|
||||||
description="Show stored credentials",
|
description="Show logged in accounts and instances",
|
||||||
arguments=[],
|
arguments=[],
|
||||||
require_auth=False,
|
require_auth=False,
|
||||||
),
|
),
|
||||||
@ -261,6 +267,10 @@ def get_argument_parser(name, command):
|
|||||||
for args, kwargs in command.arguments + common_args:
|
for args, kwargs in command.arguments + common_args:
|
||||||
parser.add_argument(*args, **kwargs)
|
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
|
return parser
|
||||||
|
|
||||||
|
|
||||||
@ -275,6 +285,12 @@ def run_command(app, user, name, args):
|
|||||||
parser = get_argument_parser(name, command)
|
parser = get_argument_parser(name, command)
|
||||||
parsed_args = parser.parse_args(args)
|
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):
|
if command.require_auth and (not user or not app):
|
||||||
print_err("This command requires that you are logged in.")
|
print_err("This command requires that you are logged in.")
|
||||||
print_err("Please run `toot login` first.")
|
print_err("Please run `toot login` first.")
|
||||||
@ -305,8 +321,7 @@ def main():
|
|||||||
if not command_name:
|
if not command_name:
|
||||||
return print_usage()
|
return print_usage()
|
||||||
|
|
||||||
user = config.load_user()
|
user, app = config.get_active_user_app()
|
||||||
app = config.load_app(user.instance) if user else None
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
run_command(app, user, command_name, args)
|
run_command(app, user, command_name, args)
|
||||||
|
@ -22,7 +22,7 @@ def log_request(request):
|
|||||||
def log_response(response):
|
def log_response(response):
|
||||||
if response.ok:
|
if response.ok:
|
||||||
logger.debug("<<< \033[32m{}\033[0m".format(response))
|
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:
|
else:
|
||||||
logger.debug("<<< \033[31m{}\033[0m".format(response))
|
logger.debug("<<< \033[31m{}\033[0m".format(response))
|
||||||
logger.debug("<<< \033[31m{}\033[0m".format(response.content))
|
logger.debug("<<< \033[31m{}\033[0m".format(response.content))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user