Merge pull request #335 from ihabunek/local-domain
Add support for custom instance domains
This commit is contained in:
commit
a282bc3c0b
9 changed files with 88 additions and 50 deletions
|
@ -14,7 +14,7 @@ def test_create_app(mock_post):
|
||||||
'client_secret': 'bar',
|
'client_secret': 'bar',
|
||||||
})
|
})
|
||||||
|
|
||||||
create_app('bigfish.software')
|
create_app('https://bigfish.software')
|
||||||
|
|
||||||
mock_post.assert_called_once_with('https://bigfish.software/api/v1/apps', json={
|
mock_post.assert_called_once_with('https://bigfish.software/api/v1/apps', json={
|
||||||
'website': CLIENT_WEBSITE,
|
'website': CLIENT_WEBSITE,
|
||||||
|
|
|
@ -13,17 +13,18 @@ def test_register_app(monkeypatch):
|
||||||
assert app.client_secret == "cs"
|
assert app.client_secret == "cs"
|
||||||
|
|
||||||
monkeypatch.setattr(api, 'create_app', retval(app_data))
|
monkeypatch.setattr(api, 'create_app', retval(app_data))
|
||||||
monkeypatch.setattr(api, 'get_instance', retval({"title": "foo", "version": "1"}))
|
monkeypatch.setattr(api, 'get_instance', retval({"title": "foo", "version": "1", "uri": "bezdomni.net"}))
|
||||||
monkeypatch.setattr(config, 'save_app', assert_app)
|
monkeypatch.setattr(config, 'save_app', assert_app)
|
||||||
|
|
||||||
app = auth.register_app("foo.bar")
|
app = auth.register_app("foo.bar", "https://foo.bar")
|
||||||
assert_app(app)
|
assert_app(app)
|
||||||
|
|
||||||
|
|
||||||
def test_create_app_from_config(monkeypatch):
|
def test_create_app_from_config(monkeypatch):
|
||||||
"""When there is saved config, it's returned"""
|
"""When there is saved config, it's returned"""
|
||||||
monkeypatch.setattr(config, 'load_app', retval("loaded app"))
|
monkeypatch.setattr(config, 'load_app', retval("loaded app"))
|
||||||
app = auth.create_app_interactive("bezdomni.net")
|
monkeypatch.setattr(api, 'get_instance', retval({"title": "foo", "version": "1", "uri": "bezdomni.net"}))
|
||||||
|
app = auth.create_app_interactive("https://bezdomni.net")
|
||||||
assert app == 'loaded app'
|
assert app == 'loaded app'
|
||||||
|
|
||||||
|
|
||||||
|
@ -31,6 +32,7 @@ def test_create_app_registered(monkeypatch):
|
||||||
"""When there is no saved config, a new app is registered"""
|
"""When there is no saved config, a new app is registered"""
|
||||||
monkeypatch.setattr(config, 'load_app', retval(None))
|
monkeypatch.setattr(config, 'load_app', retval(None))
|
||||||
monkeypatch.setattr(auth, 'register_app', retval("registered app"))
|
monkeypatch.setattr(auth, 'register_app', retval("registered app"))
|
||||||
|
monkeypatch.setattr(api, 'get_instance', retval({"title": "foo", "version": "1", "uri": "bezdomni.net"}))
|
||||||
|
|
||||||
app = auth.create_app_interactive("bezdomni.net")
|
app = auth.create_app_interactive("bezdomni.net")
|
||||||
assert app == 'registered app'
|
assert app == 'registered app'
|
||||||
|
|
|
@ -30,7 +30,7 @@ from unittest import mock
|
||||||
|
|
||||||
# Host name of a test instance to run integration tests against
|
# Host name of a test instance to run integration tests against
|
||||||
# DO NOT USE PUBLIC INSTANCES!!!
|
# DO NOT USE PUBLIC INSTANCES!!!
|
||||||
HOSTNAME = os.getenv("TOOT_TEST_HOSTNAME")
|
BASE_URL = os.getenv("TOOT_TEST_BASE_URL")
|
||||||
|
|
||||||
# Mastodon database name, used to confirm user registration without having to click the link
|
# Mastodon database name, used to confirm user registration without having to click the link
|
||||||
DATABASE_DSN = os.getenv("TOOT_TEST_DATABASE_DSN")
|
DATABASE_DSN = os.getenv("TOOT_TEST_DATABASE_DSN")
|
||||||
|
@ -39,7 +39,7 @@ DATABASE_DSN = os.getenv("TOOT_TEST_DATABASE_DSN")
|
||||||
TRUMPET = path.join(path.dirname(path.dirname(path.realpath(__file__))), "trumpet.png")
|
TRUMPET = path.join(path.dirname(path.dirname(path.realpath(__file__))), "trumpet.png")
|
||||||
|
|
||||||
|
|
||||||
if not HOSTNAME or not DATABASE_DSN:
|
if not BASE_URL or not DATABASE_DSN:
|
||||||
pytest.skip("Skipping integration tests", allow_module_level=True)
|
pytest.skip("Skipping integration tests", allow_module_level=True)
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
@ -48,8 +48,9 @@ if not HOSTNAME or not DATABASE_DSN:
|
||||||
|
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
response = api.create_app(HOSTNAME, scheme="http")
|
instance = api.get_instance(BASE_URL)
|
||||||
return App(HOSTNAME, f"http://{HOSTNAME}", response["client_id"], response["client_secret"])
|
response = api.create_app(BASE_URL)
|
||||||
|
return App(instance["uri"], BASE_URL, response["client_id"], response["client_secret"])
|
||||||
|
|
||||||
|
|
||||||
def register_account(app: App):
|
def register_account(app: App):
|
||||||
|
@ -115,7 +116,7 @@ def test_instance(app, run):
|
||||||
|
|
||||||
|
|
||||||
def test_instance_anon(app, run_anon):
|
def test_instance_anon(app, run_anon):
|
||||||
out = run_anon("instance", "--disable-https", HOSTNAME)
|
out = run_anon("instance", BASE_URL)
|
||||||
assert "Mastodon" in out
|
assert "Mastodon" in out
|
||||||
assert app.instance in out
|
assert app.instance in out
|
||||||
assert "running Mastodon" in out
|
assert "running Mastodon" in out
|
||||||
|
@ -123,7 +124,7 @@ def test_instance_anon(app, run_anon):
|
||||||
# Need to specify the instance name when running anon
|
# Need to specify the instance name when running anon
|
||||||
with pytest.raises(ConsoleError) as exc:
|
with pytest.raises(ConsoleError) as exc:
|
||||||
run_anon("instance")
|
run_anon("instance")
|
||||||
assert str(exc.value) == "Please specify instance name."
|
assert str(exc.value) == "Please specify an instance."
|
||||||
|
|
||||||
|
|
||||||
def test_post(app, user, run):
|
def test_post(app, user, run):
|
||||||
|
@ -411,7 +412,6 @@ def test_whoami(user, run):
|
||||||
out = run("whoami")
|
out = run("whoami")
|
||||||
# TODO: test other fields once updating account is supported
|
# TODO: test other fields once updating account is supported
|
||||||
assert f"@{user.username}" in out
|
assert f"@{user.username}" in out
|
||||||
assert f"http://{HOSTNAME}/@{user.username}" in out
|
|
||||||
|
|
||||||
|
|
||||||
def test_whois(app, friend, run):
|
def test_whois(app, friend, run):
|
||||||
|
@ -425,7 +425,6 @@ def test_whois(app, friend, run):
|
||||||
for username in variants:
|
for username in variants:
|
||||||
out = run("whois", username)
|
out = run("whois", username)
|
||||||
assert f"@{friend.username}" in out
|
assert f"@{friend.username}" in out
|
||||||
assert f"http://{HOSTNAME}/@{friend.username}" in out
|
|
||||||
|
|
||||||
|
|
||||||
def test_search_account(friend, run):
|
def test_search_account(friend, run):
|
||||||
|
@ -514,22 +513,22 @@ def test_tags(run):
|
||||||
assert out == "✓ You are now following #foo"
|
assert out == "✓ You are now following #foo"
|
||||||
|
|
||||||
out = run("tags_followed")
|
out = run("tags_followed")
|
||||||
assert out == "* #foo\thttp://localhost:3000/tags/foo"
|
assert out == f"* #foo\t{BASE_URL}/tags/foo"
|
||||||
|
|
||||||
out = run("tags_follow", "bar")
|
out = run("tags_follow", "bar")
|
||||||
assert out == "✓ You are now following #bar"
|
assert out == "✓ You are now following #bar"
|
||||||
|
|
||||||
out = run("tags_followed")
|
out = run("tags_followed")
|
||||||
assert out == "\n".join([
|
assert out == "\n".join([
|
||||||
"* #bar\thttp://localhost:3000/tags/bar",
|
f"* #bar\t{BASE_URL}/tags/bar",
|
||||||
"* #foo\thttp://localhost:3000/tags/foo",
|
f"* #foo\t{BASE_URL}/tags/foo",
|
||||||
])
|
])
|
||||||
|
|
||||||
out = run("tags_unfollow", "foo")
|
out = run("tags_unfollow", "foo")
|
||||||
assert out == "✓ You are no longer following #foo"
|
assert out == "✓ You are no longer following #foo"
|
||||||
|
|
||||||
out = run("tags_followed")
|
out = run("tags_followed")
|
||||||
assert out == "* #bar\thttp://localhost:3000/tags/bar"
|
assert out == f"* #bar\t{BASE_URL}/tags/bar"
|
||||||
|
|
||||||
|
|
||||||
def test_update_account_no_options(run):
|
def test_update_account_no_options(run):
|
||||||
|
@ -667,7 +666,6 @@ def _posted_status_id(out):
|
||||||
match = re.search(pattern, out)
|
match = re.search(pattern, out)
|
||||||
assert match
|
assert match
|
||||||
|
|
||||||
host, _, status_id = match.groups()
|
_, _, status_id = match.groups()
|
||||||
assert host == HOSTNAME
|
|
||||||
|
|
||||||
return status_id
|
return status_id
|
||||||
|
|
|
@ -5,7 +5,7 @@ __version__ = '0.35.0'
|
||||||
App = namedtuple('App', ['instance', 'base_url', 'client_id', 'client_secret'])
|
App = namedtuple('App', ['instance', 'base_url', 'client_id', 'client_secret'])
|
||||||
User = namedtuple('User', ['instance', 'username', 'access_token'])
|
User = namedtuple('User', ['instance', 'username', 'access_token'])
|
||||||
|
|
||||||
DEFAULT_INSTANCE = 'mastodon.social'
|
DEFAULT_INSTANCE = 'https://mastodon.social'
|
||||||
|
|
||||||
CLIENT_NAME = 'toot - a Mastodon CLI client'
|
CLIENT_NAME = 'toot - a Mastodon CLI client'
|
||||||
CLIENT_WEBSITE = 'https://github.com/ihabunek/toot'
|
CLIENT_WEBSITE = 'https://github.com/ihabunek/toot'
|
||||||
|
|
|
@ -28,8 +28,8 @@ def _tag_action(app, user, tag_name, action):
|
||||||
return http.post(app, user, url).json()
|
return http.post(app, user, url).json()
|
||||||
|
|
||||||
|
|
||||||
def create_app(domain, scheme='https'):
|
def create_app(base_url):
|
||||||
url = f"{scheme}://{domain}/api/v1/apps"
|
url = f"{base_url}/api/v1/apps"
|
||||||
|
|
||||||
json = {
|
json = {
|
||||||
'client_name': CLIENT_NAME,
|
'client_name': CLIENT_NAME,
|
||||||
|
@ -504,6 +504,6 @@ def clear_notifications(app, user):
|
||||||
http.post(app, user, '/api/v1/notifications/clear')
|
http.post(app, user, '/api/v1/notifications/clear')
|
||||||
|
|
||||||
|
|
||||||
def get_instance(domain, scheme="https"):
|
def get_instance(base_url):
|
||||||
url = f"{scheme}://{domain}/api/v1/instance"
|
url = f"{base_url}/api/v1/instance"
|
||||||
return http.anon_get(url).json()
|
return http.anon_get(url).json()
|
||||||
|
|
42
toot/auth.py
42
toot/auth.py
|
@ -9,21 +9,13 @@ from toot.exceptions import ApiError, ConsoleError
|
||||||
from toot.output import print_out
|
from toot.output import print_out
|
||||||
|
|
||||||
|
|
||||||
def register_app(domain, scheme='https'):
|
def register_app(domain, base_url):
|
||||||
print_out("Looking up instance info...")
|
|
||||||
instance = api.get_instance(domain, scheme)
|
|
||||||
|
|
||||||
print_out("Found instance <blue>{}</blue> running Mastodon version <yellow>{}</yellow>".format(
|
|
||||||
instance['title'], instance['version']))
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
print_out("Registering application...")
|
print_out("Registering application...")
|
||||||
response = api.create_app(domain, scheme)
|
response = api.create_app(base_url)
|
||||||
except ApiError:
|
except ApiError:
|
||||||
raise ConsoleError("Registration failed.")
|
raise ConsoleError("Registration failed.")
|
||||||
|
|
||||||
base_url = scheme + '://' + domain
|
|
||||||
|
|
||||||
app = App(domain, base_url, response['client_id'], response['client_secret'])
|
app = App(domain, base_url, response['client_id'], response['client_secret'])
|
||||||
config.save_app(app)
|
config.save_app(app)
|
||||||
|
|
||||||
|
@ -32,14 +24,30 @@ def register_app(domain, scheme='https'):
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
def create_app_interactive(instance=None, scheme='https'):
|
def create_app_interactive(base_url):
|
||||||
if not instance:
|
if not base_url:
|
||||||
print_out("Choose an instance [<green>{}</green>]: ".format(DEFAULT_INSTANCE), end="")
|
print_out(f"Enter instance URL [<green>{DEFAULT_INSTANCE}</green>]: ", end="")
|
||||||
instance = input()
|
base_url = input()
|
||||||
if not instance:
|
if not base_url:
|
||||||
instance = DEFAULT_INSTANCE
|
base_url = DEFAULT_INSTANCE
|
||||||
|
|
||||||
return config.load_app(instance) or register_app(instance, scheme)
|
domain = get_instance_domain(base_url)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
print_out(
|
||||||
|
f"Found instance <blue>{instance['title']}</blue> "
|
||||||
|
f"running Mastodon version <yellow>{instance['version']}</yellow>"
|
||||||
|
)
|
||||||
|
|
||||||
|
# NB: when updating to v2 instance endpoint, this field has been renamed to `domain`
|
||||||
|
return instance["uri"]
|
||||||
|
|
||||||
|
|
||||||
def create_user(app, access_token):
|
def create_user(app, access_token):
|
||||||
|
|
|
@ -10,7 +10,7 @@ from toot.output import (print_out, print_instance, print_account, print_acct_li
|
||||||
print_search_results, print_timeline, print_notifications,
|
print_search_results, print_timeline, print_notifications,
|
||||||
print_tag_list)
|
print_tag_list)
|
||||||
from toot.tui.utils import parse_datetime
|
from toot.tui.utils import parse_datetime
|
||||||
from toot.utils import delete_tmp_status_file, editor_input, multiline_input, EOF_KEY
|
from toot.utils import args_get_instance, delete_tmp_status_file, editor_input, multiline_input, EOF_KEY
|
||||||
|
|
||||||
|
|
||||||
def get_timeline_generator(app, user, args):
|
def get_timeline_generator(app, user, args):
|
||||||
|
@ -305,7 +305,8 @@ def update_account(app, user, args):
|
||||||
|
|
||||||
|
|
||||||
def login_cli(app, user, args):
|
def login_cli(app, user, args):
|
||||||
app = create_app_interactive(instance=args.instance, scheme=args.scheme)
|
base_url = args_get_instance(args.instance, args.scheme)
|
||||||
|
app = create_app_interactive(base_url)
|
||||||
login_interactive(app, args.email)
|
login_interactive(app, args.email)
|
||||||
|
|
||||||
print_out()
|
print_out()
|
||||||
|
@ -313,7 +314,8 @@ def login_cli(app, user, args):
|
||||||
|
|
||||||
|
|
||||||
def login(app, user, args):
|
def login(app, user, args):
|
||||||
app = create_app_interactive(instance=args.instance, scheme=args.scheme)
|
base_url = args_get_instance(args.instance, args.scheme)
|
||||||
|
app = create_app_interactive(base_url)
|
||||||
login_browser_interactive(app)
|
login_browser_interactive(app)
|
||||||
|
|
||||||
print_out()
|
print_out()
|
||||||
|
@ -452,17 +454,19 @@ def whois(app, user, args):
|
||||||
|
|
||||||
|
|
||||||
def instance(app, user, args):
|
def instance(app, user, args):
|
||||||
name = args.instance or (app and app.instance)
|
default = app.base_url if app else None
|
||||||
if not name:
|
base_url = args_get_instance(args.instance, args.scheme, default)
|
||||||
raise ConsoleError("Please specify instance name.")
|
|
||||||
|
if not base_url:
|
||||||
|
raise ConsoleError("Please specify an instance.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
instance = api.get_instance(name, args.scheme)
|
instance = api.get_instance(base_url)
|
||||||
print_instance(instance)
|
print_instance(instance)
|
||||||
except ApiError:
|
except ApiError:
|
||||||
raise ConsoleError(
|
raise ConsoleError(
|
||||||
"Instance not found at {}.\n"
|
f"Instance not found at {base_url}.\n"
|
||||||
"The given domain probably does not host a Mastodon instance.".format(name)
|
"The given domain probably does not host a Mastodon instance."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -336,7 +336,7 @@ class TUI(urwid.Frame):
|
||||||
See: https://github.com/mastodon/mastodon/issues/19328
|
See: https://github.com/mastodon/mastodon/issues/19328
|
||||||
"""
|
"""
|
||||||
def _load_instance():
|
def _load_instance():
|
||||||
return api.get_instance(self.app.instance)
|
return api.get_instance(self.app.base_url)
|
||||||
|
|
||||||
def _done(instance):
|
def _done(instance):
|
||||||
if "max_toot_chars" in instance:
|
if "max_toot_chars" in instance:
|
||||||
|
|
|
@ -160,3 +160,29 @@ def _use_existing_tmp_file(tmp_path) -> bool:
|
||||||
def drop_empty_values(data: Dict) -> Dict:
|
def drop_empty_values(data: Dict) -> Dict:
|
||||||
"""Remove keys whose values are null"""
|
"""Remove keys whose values are null"""
|
||||||
return {k: v for k, v in data.items() if v is not None}
|
return {k: v for k, v in data.items() if v is not None}
|
||||||
|
|
||||||
|
|
||||||
|
def args_get_instance(instance, scheme, default=None):
|
||||||
|
if not instance:
|
||||||
|
return default
|
||||||
|
|
||||||
|
if scheme == "http":
|
||||||
|
_warn_scheme_deprecated()
|
||||||
|
|
||||||
|
if instance.startswith("http"):
|
||||||
|
return instance.rstrip("/")
|
||||||
|
else:
|
||||||
|
return f"{scheme}://{instance}"
|
||||||
|
|
||||||
|
|
||||||
|
def _warn_scheme_deprecated():
|
||||||
|
from toot.output import print_err
|
||||||
|
|
||||||
|
print_err("\n".join([
|
||||||
|
"--disable-https flag is deprecated and will be removed.",
|
||||||
|
"Please specify the instance as URL instead.",
|
||||||
|
"e.g. instead of writing:",
|
||||||
|
" toot instance unsafehost.com --disable-https",
|
||||||
|
"instead write:",
|
||||||
|
" toot instance http://unsafehost.com\n"
|
||||||
|
]))
|
||||||
|
|
Loading…
Reference in a new issue