diff --git a/changelog.d/cat-ears.add b/changelog.d/cat-ears.add new file mode 100644 index 0000000000..9b8f2fc958 --- /dev/null +++ b/changelog.d/cat-ears.add @@ -0,0 +1 @@ +Support `isCat` and `speakAsCat` diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 468e124b5b..3aac910495 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -162,6 +162,8 @@ defmodule Pleroma.User do field(:birthday, :date) field(:show_birthday, :boolean, default: false) field(:language, :string) + field(:is_cat, :boolean, default: false) + field(:speak_as_cat, :boolean, default: false) embeds_one( :notification_settings, @@ -527,7 +529,9 @@ defmodule Pleroma.User do :accepts_chat_messages, :pinned_objects, :birthday, - :show_birthday + :show_birthday, + :is_cat, + :speak_as_cat ] ) |> cast(params, [:name], empty_values: []) @@ -590,7 +594,9 @@ defmodule Pleroma.User do :accepts_chat_messages, :disclose_client, :birthday, - :show_birthday + :show_birthday, + :is_cat, + :speak_as_cat ] ) |> validate_min_age() diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 3e5239a286..63887793c0 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1635,6 +1635,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do show_birthday = !!birthday + is_cat = Map.get(data, "isCat", false) + speak_as_cat = Map.get(data, "speakAsCat", is_cat) + # if WebFinger request was already done, we probably have acct, otherwise # we request WebFinger here nickname = additional[:nickname_from_acct] || generate_nickname(data) @@ -1663,7 +1666,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do birthday: birthday, show_birthday: show_birthday, pinned_objects: pinned_objects, - nickname: nickname + nickname: nickname, + is_cat: is_cat, + speak_as_cat: speak_as_cat } end diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 61975387b3..186e2ed015 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -128,7 +128,9 @@ defmodule Pleroma.Web.ActivityPub.UserView do "alsoKnownAs" => user.also_known_as, "vcard:bday" => birthday, "webfinger" => "acct:#{User.full_nickname(user)}", - "published" => Pleroma.Web.CommonAPI.Utils.to_masto_date(user.inserted_at) + "published" => Pleroma.Web.CommonAPI.Utils.to_masto_date(user.inserted_at), + "isCat" => user.is_cat, + "speakAsCat" => user.speak_as_cat } |> Map.merge( maybe_make_image( diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index 5a19e0fbb7..637e097867 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -838,6 +838,16 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do type: :string, nullable: true, description: "Header image description." + }, + is_cat: %Schema{ + type: :boolean, + nullable: true, + description: "Whether the user is a cat" + }, + speak_as_cat: %Schema{ + type: :boolean, + nullable: true, + description: "Whether the user speaks as a cat" } }, example: %{ diff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex index 19827e9968..44084bc36d 100644 --- a/lib/pleroma/web/api_spec/schemas/account.ex +++ b/lib/pleroma/web/api_spec/schemas/account.ex @@ -114,7 +114,17 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do description: "Favicon image of the user's instance" }, avatar_description: %Schema{type: :string}, - header_description: %Schema{type: :string} + header_description: %Schema{type: :string}, + is_cat: %Schema{ + type: :boolean, + nullable: true, + description: "Whether the user is a cat" + }, + speak_as_cat: %Schema{ + type: :boolean, + nullable: true, + description: "Whether the user speaks as a cat" + } } }, source: %Schema{ @@ -211,7 +221,9 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do "settings_store" => %{ "pleroma-fe" => %{} }, - "birthday" => "2001-02-12" + "birthday" => "2001-02-12", + "is_cat" => true, + "speak_as_cat" => true }, "source" => %{ "fields" => [], diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index d374e8c011..b0b6dd9128 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -202,7 +202,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do :allow_following_move, :also_known_as, :accepts_chat_messages, - :show_birthday + :show_birthday, + :is_cat, + :speak_as_cat ] |> Enum.reduce(%{}, fn key, acc -> Maps.put_if_present(acc, key, params[key], &{:ok, Params.truthy_param?(&1)}) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 03a2fc55a5..0695c1c485 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -325,7 +325,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do accepts_chat_messages: user.accepts_chat_messages, favicon: favicon, avatar_description: avatar_description, - header_description: header_description + header_description: header_description, + is_cat: user.is_cat, + speak_as_cat: user.speak_as_cat } } |> maybe_put_role(user, opts[:for]) diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index 1b6f26af72..8f5cdd18b5 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -158,7 +158,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do if Pleroma.Language.LanguageDetector.configured?() do "pleroma:language_detection" end, - "pleroma:block_expiration" + "pleroma:block_expiration", + "pleroma:is_cat" ] |> Enum.filter(& &1) end diff --git a/priv/repo/migrations/20250402213700_add_is_cat_to_users.exs b/priv/repo/migrations/20250402213700_add_is_cat_to_users.exs new file mode 100644 index 0000000000..4828b43253 --- /dev/null +++ b/priv/repo/migrations/20250402213700_add_is_cat_to_users.exs @@ -0,0 +1,10 @@ +defmodule Pleroma.Repo.Migrations.AddIsCatToUsers do + use Ecto.Migration + + def change do + alter table(:users) do + add_if_not_exists(:is_cat, :boolean, default: false) + add_if_not_exists(:speak_as_cat, :boolean, default: false) + end + end +end diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld index 3569165a40..b6378a630b 100644 --- a/priv/static/schemas/litepub-0.1.jsonld +++ b/priv/static/schemas/litepub-0.1.jsonld @@ -43,7 +43,11 @@ "vcard": "http://www.w3.org/2006/vcard/ns#", "formerRepresentations": "litepub:formerRepresentations", "sm": "http://smithereen.software/ns#", - "nonAnonymous": "sm:nonAnonymous" + "nonAnonymous": "sm:nonAnonymous", + "misskey": "https://misskey-hub.net/ns#", + "isCat": "misskey:isCat", + "firefish": "https://joinfirefish.org/ns#", + "speakAsCat": "firefish:speakAsCat" } ] } diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs index 7fc4f47cd6..54d8f5dbb8 100644 --- a/test/pleroma/web/activity_pub/activity_pub_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_test.exs @@ -2754,4 +2754,112 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do "first" => "https://social.example/users/alice/collections/featured?page=true" }) end + + describe "cat ears" do + test "it respects isCat and speakAsCat" do + cat_id = "https://example.com/users/cat" + + cat_data = + "test/fixtures/users_mock/user.json" + |> File.read!() + |> String.replace("{{nickname}}", "cat") + |> Jason.decode!() + |> Map.delete("featured") + |> Map.put("isCat", true) + |> Map.put("speakAsCat", true) + |> Jason.encode!() + + dog_id = "https://example.com/users/dog" + + dog_data = + "test/fixtures/users_mock/user.json" + |> File.read!() + |> String.replace("{{nickname}}", "dog") + |> Jason.decode!() + |> Map.delete("featured") + |> Map.put("isCat", false) + |> Map.put("speakAsCat", false) + |> Jason.encode!() + + Tesla.Mock.mock(fn + %{ + method: :get, + url: ^cat_id + } -> + %Tesla.Env{ + status: 200, + body: cat_data, + headers: [{"content-type", "application/activity+json"}] + } + + %{ + method: :get, + url: ^dog_id + } -> + %Tesla.Env{ + status: 200, + body: dog_data, + headers: [{"content-type", "application/activity+json"}] + } + end) + + {:ok, cat} = ActivityPub.make_user_from_ap_id(cat_id) + {:ok, dog} = ActivityPub.make_user_from_ap_id(dog_id) + + assert %{is_cat: true, speak_as_cat: true} = cat + assert %{is_cat: false, speak_as_cat: false} = dog + end + + test "it infers speakAsCat from isCat, when missing" do + cat_id = "https://example.com/users/cat" + + cat_data = + "test/fixtures/users_mock/user.json" + |> File.read!() + |> String.replace("{{nickname}}", "cat") + |> Jason.decode!() + |> Map.delete("featured") + |> Map.put("isCat", true) + |> Jason.encode!() + + dog_id = "https://example.com/users/dog" + + dog_data = + "test/fixtures/users_mock/user.json" + |> File.read!() + |> String.replace("{{nickname}}", "dog") + |> Jason.decode!() + |> Map.delete("featured") + |> Map.put("isCat", false) + |> Jason.encode!() + + Tesla.Mock.mock(fn + %{ + method: :get, + url: ^cat_id + } -> + %Tesla.Env{ + status: 200, + body: cat_data, + headers: [{"content-type", "application/activity+json"}] + } + + %{ + method: :get, + url: ^dog_id + } -> + %Tesla.Env{ + status: 200, + body: dog_data, + headers: [{"content-type", "application/activity+json"}] + } + end) + + {:ok, cat} = ActivityPub.make_user_from_ap_id(cat_id) + {:ok, dog} = ActivityPub.make_user_from_ap_id(dog_id) + + assert %{is_cat: true, speak_as_cat: true} = cat + assert %{is_cat: false, speak_as_cat: false} = dog + end + end end diff --git a/test/pleroma/web/mastodon_api/update_credentials_test.exs b/test/pleroma/web/mastodon_api/update_credentials_test.exs index 97ad2e849f..bb90ab1b25 100644 --- a/test/pleroma/web/mastodon_api/update_credentials_test.exs +++ b/test/pleroma/web/mastodon_api/update_credentials_test.exs @@ -823,4 +823,19 @@ defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do assert account["source"]["pleroma"]["actor_type"] == "Group" end end + + describe "cat ears" do + setup do: oauth_access(["write:accounts"]) + setup :request_content_type + + test "sets cat ears preferences", %{conn: conn} do + account = + conn + |> patch("/api/v1/accounts/update_credentials", %{is_cat: true, speak_as_cat: false}) + |> json_response_and_validate_schema(200) + + assert account["pleroma"]["is_cat"] + assert not account["pleroma"]["speak_as_cat"] + end + end end diff --git a/test/pleroma/web/mastodon_api/views/account_view_test.exs b/test/pleroma/web/mastodon_api/views/account_view_test.exs index 5d24c0e9fb..c0591e93a7 100644 --- a/test/pleroma/web/mastodon_api/views/account_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/account_view_test.exs @@ -98,7 +98,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do skip_thread_containment: false, accepts_chat_messages: nil, avatar_description: "", - header_description: "" + header_description: "", + is_cat: false, + speak_as_cat: false } } @@ -344,7 +346,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do skip_thread_containment: false, accepts_chat_messages: nil, avatar_description: "", - header_description: "" + header_description: "", + is_cat: false, + speak_as_cat: false } } @@ -845,4 +849,17 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do DateTime.utc_now() |> DateTime.add(24 * 60 * 60) ) in -3..3 end + + test "renders isCat and speakAsCat" do + cat = insert(:user, is_cat: true, speak_as_cat: true) + dog = insert(:user, is_cat: false, speak_as_cat: false) + + %{ + pleroma: %{is_cat: true, speak_as_cat: true} + } = AccountView.render("show.json", %{user: cat, skip_visibility_check: true}) + + %{ + pleroma: %{is_cat: false, speak_as_cat: false} + } = AccountView.render("show.json", %{user: dog, skip_visibility_check: true}) + end end