From e9e3d255e5349f913ed88a36e6ff38ec93c8544b Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 17 Jan 2026 00:12:48 +0400 Subject: [PATCH] AP ingest: infer tag type when missing --- .../object_validators/tag_validator.ex | 21 ++++++ .../fedidev.fun/tag_hashtag_missing_type.json | 30 +++++++++ .../fedidev.fun/tag_mention_missing_type.json | 31 +++++++++ .../fedidev_fun_tag_inference_test.exs | 65 +++++++++++++++++++ 4 files changed, 147 insertions(+) create mode 100644 test/fixtures/fedidev.fun/tag_hashtag_missing_type.json create mode 100644 test/fixtures/fedidev.fun/tag_mention_missing_type.json create mode 100644 test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_tag_inference_test.exs diff --git a/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex b/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex index dc2770189..a412c8dea 100644 --- a/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex @@ -80,6 +80,27 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.TagValidator do |> Map.put(:action, :ignore) end + def changeset(struct, data) when is_map(data) do + data = infer_type(data) + + if Map.has_key?(data, "type") do + changeset(struct, data) + else + struct + |> cast(data, []) + |> Map.put(:action, :ignore) + end + end + + defp infer_type(%{"type" => _} = data), do: data + defp infer_type(%{"name" => "#" <> _} = data), do: Map.put(data, "type", "Hashtag") + defp infer_type(%{"name" => "@" <> _} = data), do: Map.put(data, "type", "Mention") + + defp infer_type(%{"mediaType" => _media_type, "href" => _href} = data), + do: Map.put(data, "type", "Link") + + defp infer_type(data), do: data + def icon_changeset(struct, data) do struct |> cast(data, [:type, :url]) diff --git a/test/fixtures/fedidev.fun/tag_hashtag_missing_type.json b/test/fixtures/fedidev.fun/tag_hashtag_missing_type.json new file mode 100644 index 000000000..74f9d91b8 --- /dev/null +++ b/test/fixtures/fedidev.fun/tag_hashtag_missing_type.json @@ -0,0 +1,30 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams" + ], + "type": "Create", + "id": "https://fedidev.fun/activity/tag-hashtag-missing-type", + "actor": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:41:00Z", + "object": { + "type": "Note", + "id": "https://fedidev.fun/objects/note-tag-hashtag-missing-type", + "attributedTo": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:41:00Z", + "content": "hashtag without type", + "tag": [ + { + "name": "#Test" + } + ] + } +} + diff --git a/test/fixtures/fedidev.fun/tag_mention_missing_type.json b/test/fixtures/fedidev.fun/tag_mention_missing_type.json new file mode 100644 index 000000000..d290be6c2 --- /dev/null +++ b/test/fixtures/fedidev.fun/tag_mention_missing_type.json @@ -0,0 +1,31 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams" + ], + "type": "Create", + "id": "https://fedidev.fun/activity/tag-mention-missing-type", + "actor": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:41:01Z", + "object": { + "type": "Note", + "id": "https://fedidev.fun/objects/note-tag-mention-missing-type", + "attributedTo": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:41:01Z", + "content": "mention without type", + "tag": [ + { + "name": "@bob", + "href": "https://remote.example/users/bob" + } + ] + } +} + diff --git a/test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_tag_inference_test.exs b/test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_tag_inference_test.exs new file mode 100644 index 000000000..ef7feb8ad --- /dev/null +++ b/test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_tag_inference_test.exs @@ -0,0 +1,65 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.FedidevFunTagInferenceTest do + use Oban.Testing, repo: Pleroma.Repo + use Pleroma.DataCase + + alias Pleroma.Object + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Transmogrifier + + import Pleroma.Factory + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + defp ensure_remote_actor!(ap_id) when is_binary(ap_id) do + case User.get_cached_by_ap_id(ap_id) do + %User{} -> + :ok + + _ -> + insert(:user, + local: false, + ap_id: ap_id, + follower_address: ap_id <> "/followers", + following_address: ap_id <> "/following", + featured_address: ap_id <> "/collections/featured", + inbox: ap_id <> "/inbox" + ) + + :ok + end + end + + defp ingest_fixture!(rel_path) do + data = rel_path |> File.read!() |> Jason.decode!() + ensure_remote_actor!(data["actor"]) + + assert {:ok, activity} = Transmogrifier.handle_incoming(data) + object = Object.normalize(activity.data["object"], fetch: false) + + {activity, object} + end + + test "infers Hashtag type when missing" do + {_activity, object} = + ingest_fixture!("test/fixtures/fedidev.fun/tag_hashtag_missing_type.json") + + assert "test" in Object.hashtags(object) + end + + test "infers Mention type when missing" do + {_activity, object} = + ingest_fixture!("test/fixtures/fedidev.fun/tag_mention_missing_type.json") + + assert Enum.any?(object.data["tag"], fn + %{"type" => "Mention", "href" => "https://remote.example/users/bob"} -> true + _ -> false + end) + end +end