From 381e3583ef84b5a05f4f5d28b12b72cb0e6e864e Mon Sep 17 00:00:00 2001 From: Ivan Habunek Date: Wed, 13 Dec 2023 07:50:45 +0100 Subject: [PATCH] Add featured tag commands --- tests/integration/test_tags.py | 101 +++++++++++++++++++++++++++++---- toot/api.py | 23 ++++++++ toot/cli/tags.py | 57 ++++++++++++++++++- toot/entities.py | 17 ++++++ toot/output.py | 7 +-- 5 files changed, 188 insertions(+), 17 deletions(-) diff --git a/tests/integration/test_tags.py b/tests/integration/test_tags.py index bc97f78..eaddaba 100644 --- a/tests/integration/test_tags.py +++ b/tests/integration/test_tags.py @@ -1,11 +1,14 @@ -from toot import cli -from toot.entities import Tag, from_dict, from_dict_list +import re +from typing import List + +from toot import api, cli +from toot.entities import FeaturedTag, Tag, from_dict, from_dict_list -def test_tags(run, base_url): +def test_tags(run): result = run(cli.tags, "followed") assert result.exit_code == 0 - assert result.stdout.strip() == "You're not following any hashtags." + assert result.stdout.strip() == "You're not following any hashtags" result = run(cli.tags, "follow", "foo") assert result.exit_code == 0 @@ -13,7 +16,7 @@ def test_tags(run, base_url): result = run(cli.tags, "followed") assert result.exit_code == 0 - assert result.stdout.strip() == f"* #foo\t{base_url}/tags/foo" + assert _find_tags(result.stdout) == ["#foo"] result = run(cli.tags, "follow", "bar") assert result.exit_code == 0 @@ -21,10 +24,7 @@ def test_tags(run, base_url): result = run(cli.tags, "followed") assert result.exit_code == 0 - assert result.stdout.strip() == "\n".join([ - f"* #bar\t{base_url}/tags/bar", - f"* #foo\t{base_url}/tags/foo", - ]) + assert _find_tags(result.stdout) == ["#bar", "#foo"] result = run(cli.tags, "unfollow", "foo") assert result.exit_code == 0 @@ -32,7 +32,7 @@ def test_tags(run, base_url): result = run(cli.tags, "followed") assert result.exit_code == 0 - assert result.stdout.strip() == f"* #bar\t{base_url}/tags/bar" + assert _find_tags(result.stdout) == ["#bar"] result = run(cli.tags, "unfollow", "bar") assert result.exit_code == 0 @@ -40,7 +40,7 @@ def test_tags(run, base_url): result = run(cli.tags, "followed") assert result.exit_code == 0 - assert result.stdout.strip() == "You're not following any hashtags." + assert result.stdout.strip() == "You're not following any hashtags" def test_tags_json(run_json): @@ -82,3 +82,82 @@ def test_tags_json(run_json): result = run_json(cli.tags, "followed", "--json") assert result == [] + + +def test_tags_featured(run, app, user): + result = run(cli.tags, "featured") + assert result.exit_code == 0 + assert result.stdout.strip() == "You don't have any featured hashtags" + + result = run(cli.tags, "feature", "foo") + assert result.exit_code == 0 + assert result.stdout.strip() == "✓ Tag #foo is now featured" + + result = run(cli.tags, "featured") + assert result.exit_code == 0 + assert _find_tags(result.stdout) == ["#foo"] + + result = run(cli.tags, "feature", "bar") + assert result.exit_code == 0 + assert result.stdout.strip() == "✓ Tag #bar is now featured" + + result = run(cli.tags, "featured") + assert result.exit_code == 0 + assert _find_tags(result.stdout) == ["#bar", "#foo"] + + # Unfeature by Name + result = run(cli.tags, "unfeature", "foo") + assert result.exit_code == 0 + assert result.stdout.strip() == "✓ Tag #foo is no longer featured" + + result = run(cli.tags, "featured") + assert result.exit_code == 0 + assert _find_tags(result.stdout) == ["#bar"] + + # Unfeature by ID + tag = api.find_featured_tag(app, user, "bar") + assert tag is not None + + result = run(cli.tags, "unfeature", tag["id"]) + assert result.exit_code == 0 + assert result.stdout.strip() == "✓ Tag #bar is no longer featured" + + result = run(cli.tags, "featured") + assert result.exit_code == 0 + assert result.stdout.strip() == "You don't have any featured hashtags" + + +def test_tags_featured_json(run_json): + result = run_json(cli.tags, "featured", "--json") + assert result == [] + + result = run_json(cli.tags, "feature", "foo", "--json") + tag = from_dict(FeaturedTag, result) + assert tag.name == "foo" + + result = run_json(cli.tags, "featured", "--json") + [tag] = from_dict_list(FeaturedTag, result) + assert tag.name == "foo" + + result = run_json(cli.tags, "feature", "bar", "--json") + tag = from_dict(FeaturedTag, result) + assert tag.name == "bar" + + result = run_json(cli.tags, "featured", "--json") + tags = from_dict_list(FeaturedTag, result) + [bar, foo] = sorted(tags, key=lambda t: t.name) + assert foo.name == "foo" + assert bar.name == "bar" + + result = run_json(cli.tags, "unfeature", "foo", "--json") + assert result == {} + + result = run_json(cli.tags, "unfeature", "bar", "--json") + assert result == {} + + result = run_json(cli.tags, "featured", "--json") + assert result == [] + + +def _find_tags(txt: str) -> List[str]: + return sorted(re.findall(r"#\w+", txt)) diff --git a/toot/api.py b/toot/api.py index 2a58f30..4775aa3 100644 --- a/toot/api.py +++ b/toot/api.py @@ -531,6 +531,29 @@ def followed_tags(app, user): return _get_response_list(app, user, path) +def featured_tags(app, user): + return http.get(app, user, "/api/v1/featured_tags") + + +def feature_tag(app, user, tag: str) -> Response: + return http.post(app, user, "/api/v1/featured_tags", data={"name": tag}) + + +def unfeature_tag(app, user, tag_id: str) -> Response: + return http.delete(app, user, f"/api/v1/featured_tags/{tag_id}") + + +def find_featured_tag(app, user, tag) -> Optional[dict]: + """Find a featured tag by tag name or ID""" + return next( + ( + t for t in featured_tags(app, user).json() + if t["name"].lower() == tag.lstrip("#").lower() or t["id"] == tag + ), + None + ) + + def whois(app, user, account): return http.get(app, user, f'/api/v1/accounts/{account}').json() diff --git a/toot/cli/tags.py b/toot/cli/tags.py index 852c0f7..1621d66 100644 --- a/toot/cli/tags.py +++ b/toot/cli/tags.py @@ -20,7 +20,10 @@ def followed(ctx: Context, json: bool): if json: click.echo(pyjson.dumps(tags)) else: - print_tag_list(tags) + if tags: + print_tag_list(tags) + else: + click.echo("You're not following any hashtags") @tags.command() @@ -51,6 +54,58 @@ def unfollow(ctx: Context, tag: str, json: bool): click.secho(f"✓ You are no longer following #{tag}", fg="green") +@tags.command() +@json_option +@pass_context +def featured(ctx: Context, json: bool): + """List hashtags featured on your profile.""" + response = api.featured_tags(ctx.app, ctx.user) + if json: + click.echo(response.text) + else: + tags = response.json() + if tags: + print_tag_list(tags) + else: + click.echo("You don't have any featured hashtags") + + +@tags.command() +@click.argument("tag") +@json_option +@pass_context +def feature(ctx: Context, tag: str, json: bool): + """Feature a hashtag on your profile""" + tag = tag.lstrip("#") + response = api.feature_tag(ctx.app, ctx.user, tag) + if json: + click.echo(response.text) + else: + click.secho(f"✓ Tag #{tag} is now featured", fg="green") + + +@tags.command() +@click.argument("tag") +@json_option +@pass_context +def unfeature(ctx: Context, tag: str, json: bool): + """Unfollow a hashtag + + TAG can either be a tag name like "#foo" or "foo" or a tag ID. + """ + featured_tag = api.find_featured_tag(ctx.app, ctx.user, tag) + + # TODO: should this be idempotent? + if not featured_tag: + raise click.ClickException(f"Tag {tag} is not featured") + + response = api.unfeature_tag(ctx.app, ctx.user, featured_tag["id"]) + if json: + click.echo(response.text) + else: + click.secho(f"✓ Tag #{featured_tag['name']} is no longer featured", fg="green") + + # -- Deprecated commands ------------------------------------------------------- @cli.command(name="tags_followed", hidden=True) diff --git a/toot/entities.py b/toot/entities.py index 47d0cd7..309d84e 100644 --- a/toot/entities.py +++ b/toot/entities.py @@ -411,6 +411,10 @@ class Relationship: @dataclass class TagHistory: + """ + Usage statistics for given days (typically the past week). + https://docs.joinmastodon.org/entities/Tag/#history + """ day: str uses: str accounts: str @@ -428,6 +432,19 @@ class Tag: following: Optional[bool] +@dataclass +class FeaturedTag: + """ + Represents a hashtag that is featured on a profile. + https://docs.joinmastodon.org/entities/FeaturedTag/ + """ + id: str + name: str + url: str + statuses_count: int + last_status_at: datetime + + # Generic data class instance T = TypeVar("T") diff --git a/toot/output.py b/toot/output.py index 02f1083..266f467 100644 --- a/toot/output.py +++ b/toot/output.py @@ -115,11 +115,8 @@ def print_acct_list(accounts): def print_tag_list(tags): - if tags: - for tag in tags: - click.echo(f"* {format_tag_name(tag)}\t{tag['url']}") - else: - click.echo("You're not following any hashtags.") + for tag in tags: + click.echo(f"* {format_tag_name(tag)}\t{tag['url']}") def print_lists(lists):