diff --git a/tests/test_api.py b/tests/test_api.py index 65f815a..3b5c5b1 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -14,7 +14,7 @@ def test_create_app(mock_post): '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={ 'website': CLIENT_WEBSITE, diff --git a/tests/test_auth.py b/tests/test_auth.py index e8e3301..a3267cc 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -13,17 +13,18 @@ def test_register_app(monkeypatch): assert app.client_secret == "cs" 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) - app = auth.register_app("foo.bar") + app = auth.register_app("foo.bar", "https://foo.bar") assert_app(app) def test_create_app_from_config(monkeypatch): """When there is saved config, it's returned""" 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' @@ -31,6 +32,7 @@ def test_create_app_registered(monkeypatch): """When there is no saved config, a new app is registered""" monkeypatch.setattr(config, 'load_app', retval(None)) 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") assert app == 'registered app' diff --git a/tests/test_integration.py b/tests/test_integration.py index ba254cc..1939fee 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -30,7 +30,7 @@ from unittest import mock # Host name of a test instance to run integration tests against # 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 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") -if not HOSTNAME or not DATABASE_DSN: +if not BASE_URL or not DATABASE_DSN: pytest.skip("Skipping integration tests", allow_module_level=True) # ------------------------------------------------------------------------------ @@ -48,8 +48,9 @@ if not HOSTNAME or not DATABASE_DSN: def create_app(): - response = api.create_app(HOSTNAME, scheme="http") - return App(HOSTNAME, f"http://{HOSTNAME}", response["client_id"], response["client_secret"]) + instance = api.get_instance(BASE_URL) + response = api.create_app(BASE_URL) + return App(instance["uri"], BASE_URL, response["client_id"], response["client_secret"]) def register_account(app: App): @@ -115,7 +116,7 @@ def test_instance(app, run): 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 app.instance 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 with pytest.raises(ConsoleError) as exc: 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): @@ -411,7 +412,6 @@ def test_whoami(user, run): out = run("whoami") # TODO: test other fields once updating account is supported assert f"@{user.username}" in out - assert f"http://{HOSTNAME}/@{user.username}" in out def test_whois(app, friend, run): @@ -425,7 +425,6 @@ def test_whois(app, friend, run): for username in variants: out = run("whois", username) assert f"@{friend.username}" in out - assert f"http://{HOSTNAME}/@{friend.username}" in out def test_search_account(friend, run): @@ -514,22 +513,22 @@ def test_tags(run): assert out == "✓ You are now following #foo" 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") assert out == "✓ You are now following #bar" out = run("tags_followed") assert out == "\n".join([ - "* #bar\thttp://localhost:3000/tags/bar", - "* #foo\thttp://localhost:3000/tags/foo", + f"* #bar\t{BASE_URL}/tags/bar", + f"* #foo\t{BASE_URL}/tags/foo", ]) out = run("tags_unfollow", "foo") assert out == "✓ You are no longer following #foo" 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): @@ -667,7 +666,6 @@ def _posted_status_id(out): match = re.search(pattern, out) assert match - host, _, status_id = match.groups() - assert host == HOSTNAME + _, _, status_id = match.groups() return status_id diff --git a/toot/__init__.py b/toot/__init__.py index 05e15ac..b3f3d1a 100644 --- a/toot/__init__.py +++ b/toot/__init__.py @@ -5,7 +5,7 @@ __version__ = '0.35.0' App = namedtuple('App', ['instance', 'base_url', 'client_id', 'client_secret']) User = namedtuple('User', ['instance', 'username', 'access_token']) -DEFAULT_INSTANCE = 'mastodon.social' +DEFAULT_INSTANCE = 'https://mastodon.social' CLIENT_NAME = 'toot - a Mastodon CLI client' CLIENT_WEBSITE = 'https://github.com/ihabunek/toot' diff --git a/toot/api.py b/toot/api.py index 3bb2c89..672849b 100644 --- a/toot/api.py +++ b/toot/api.py @@ -28,8 +28,8 @@ def _tag_action(app, user, tag_name, action): return http.post(app, user, url).json() -def create_app(domain, scheme='https'): - url = f"{scheme}://{domain}/api/v1/apps" +def create_app(base_url): + url = f"{base_url}/api/v1/apps" json = { 'client_name': CLIENT_NAME, @@ -504,6 +504,6 @@ def clear_notifications(app, user): http.post(app, user, '/api/v1/notifications/clear') -def get_instance(domain, scheme="https"): - url = f"{scheme}://{domain}/api/v1/instance" +def get_instance(base_url): + url = f"{base_url}/api/v1/instance" return http.anon_get(url).json() diff --git a/toot/auth.py b/toot/auth.py index 05b61b6..0ee2bac 100644 --- a/toot/auth.py +++ b/toot/auth.py @@ -9,21 +9,13 @@ from toot.exceptions import ApiError, ConsoleError from toot.output import print_out -def register_app(domain, scheme='https'): - print_out("Looking up instance info...") - instance = api.get_instance(domain, scheme) - - print_out("Found instance {} running Mastodon version {}".format( - instance['title'], instance['version'])) - +def register_app(domain, base_url): try: print_out("Registering application...") - response = api.create_app(domain, scheme) + response = api.create_app(base_url) except ApiError: raise ConsoleError("Registration failed.") - base_url = scheme + '://' + domain - app = App(domain, base_url, response['client_id'], response['client_secret']) config.save_app(app) @@ -32,14 +24,30 @@ def register_app(domain, scheme='https'): return app -def create_app_interactive(instance=None, scheme='https'): - if not instance: - print_out("Choose an instance [{}]: ".format(DEFAULT_INSTANCE), end="") - instance = input() - if not instance: - instance = DEFAULT_INSTANCE +def create_app_interactive(base_url): + if not base_url: + print_out(f"Enter instance URL [{DEFAULT_INSTANCE}]: ", end="") + base_url = input() + if not base_url: + 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 {instance['title']} " + f"running Mastodon version {instance['version']}" + ) + + # NB: when updating to v2 instance endpoint, this field has been renamed to `domain` + return instance["uri"] def create_user(app, access_token): diff --git a/toot/commands.py b/toot/commands.py index e21f5b3..18e7be6 100644 --- a/toot/commands.py +++ b/toot/commands.py @@ -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_tag_list) 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): @@ -305,7 +305,8 @@ def update_account(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) print_out() @@ -313,7 +314,8 @@ def login_cli(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) print_out() @@ -452,17 +454,19 @@ def whois(app, user, args): def instance(app, user, args): - name = args.instance or (app and app.instance) - if not name: - raise ConsoleError("Please specify instance name.") + default = app.base_url if app else None + base_url = args_get_instance(args.instance, args.scheme, default) + + if not base_url: + raise ConsoleError("Please specify an instance.") try: - instance = api.get_instance(name, args.scheme) + instance = api.get_instance(base_url) print_instance(instance) except ApiError: raise ConsoleError( - "Instance not found at {}.\n" - "The given domain probably does not host a Mastodon instance.".format(name) + f"Instance not found at {base_url}.\n" + "The given domain probably does not host a Mastodon instance." ) diff --git a/toot/tui/app.py b/toot/tui/app.py index b1d885a..330035b 100644 --- a/toot/tui/app.py +++ b/toot/tui/app.py @@ -336,7 +336,7 @@ class TUI(urwid.Frame): See: https://github.com/mastodon/mastodon/issues/19328 """ def _load_instance(): - return api.get_instance(self.app.instance) + return api.get_instance(self.app.base_url) def _done(instance): if "max_toot_chars" in instance: diff --git a/toot/utils/__init__.py b/toot/utils/__init__.py index 8a39fd2..e8103ac 100644 --- a/toot/utils/__init__.py +++ b/toot/utils/__init__.py @@ -160,3 +160,29 @@ def _use_existing_tmp_file(tmp_path) -> bool: def drop_empty_values(data: Dict) -> Dict: """Remove keys whose values are null""" 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" + ]))