Merge pull request 'Akkoma interop fixes: emoji reacts, personal inbox, markers, data: URLs' (#8050) from lambadalambda/pleroma:fix/akkoma-interop into develop

Reviewed-on: https://git.pleroma.social/pleroma/pleroma/pulls/8050
This commit is contained in:
lain 2026-10-31 16:17:48 +00:00
commit 4b08f7a553
15 changed files with 92 additions and 93 deletions

View file

@ -0,0 +1 @@
Accept incoming emoji reactions whose tag is a single object instead of an array

View file

@ -0,0 +1 @@
Mastodon API: Use the Mastodon date format for marker updated_at

View file

@ -0,0 +1 @@
MediaProxy: Do not proxy data: URLs

View file

@ -0,0 +1 @@
Accept activities delivered to a personal inbox that do not address the inbox owner (fixes Bridgy Fed follows)

View file

@ -341,7 +341,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
{:sender_exists, User.get_or_fetch_by_ap_id(params["actor"])},
{:recipient_active, true} <- {:recipient_active, recipient.is_active},
{:sender_active, true} <- {:sender_active, actor.is_active},
true <- Utils.recipient_in_message(recipient, actor, params),
params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
Federator.incoming_ap_doc(params)
json(conn, "ok")

View file

@ -57,7 +57,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
|> CommonFixes.fix_actor()
|> CommonFixes.fix_activity_addressing()
data = Map.put_new(data, "tag", [])
data = fix_tag(data)
case Object.normalize(data["object"]) do
%Object{} = object ->
@ -70,6 +70,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
end
end
# JSON-LD compaction turns a single-element array into a bare object
defp fix_tag(%{"tag" => %{} = tag} = data), do: Map.put(data, "tag", [tag])
defp fix_tag(data), do: Map.put_new(data, "tag", [])
defp fix_emoji_qualification(%{"content" => emoji} = data) do
new_emoji = Pleroma.Emoji.fully_qualify_emoji(emoji)

View file

@ -78,22 +78,6 @@ defmodule Pleroma.Web.ActivityPub.Utils do
[params["to"], params["cc"], params["bto"], params["bcc"]]
|> Enum.any?(&label_in_collection?(label, &1))
@spec unaddressed_message?(map()) :: boolean()
def unaddressed_message?(params),
do:
[params["to"], params["cc"], params["bto"], params["bcc"]]
|> Enum.all?(fn
nil -> true
[] -> true
_ -> false
end)
@spec recipient_in_message(User.t(), User.t(), map()) :: boolean()
def recipient_in_message(%User{ap_id: ap_id} = recipient, %User{} = actor, params),
do:
label_in_message?(ap_id, params) || unaddressed_message?(params) ||
User.following?(recipient, actor)
defp extract_list(target) when is_binary(target), do: [target]
defp extract_list(lst) when is_list(lst), do: lst
defp extract_list(_), do: []

View file

@ -5,13 +5,15 @@
defmodule Pleroma.Web.MastodonAPI.MarkerView do
use Pleroma.Web, :view
alias Pleroma.Web.CommonAPI.Utils
def render("markers.json", %{markers: markers}) do
Map.new(markers, fn m ->
{m.timeline,
%{
last_read_id: m.last_read_id,
version: m.lock_version,
updated_at: NaiveDateTime.to_iso8601(m.updated_at),
updated_at: Utils.to_masto_date(m.updated_at),
pleroma: %{
unread_count: m.unread_count
}

View file

@ -45,7 +45,7 @@ defmodule Pleroma.Web.MediaProxy do
def url("/" <> _ = url), do: url
def url(url) do
if enabled?() and url_proxiable?(url) do
if enabled?() and not data_url?(url) and url_proxiable?(url) do
encode_url(url)
else
url
@ -61,7 +61,7 @@ defmodule Pleroma.Web.MediaProxy do
def preview_url("//" <> _ = url, pparams), do: preview_url("https:" <> url, pparams)
def preview_url(url, preview_params) do
if preview_enabled?() do
if preview_enabled?() and not data_url?(url) do
encode_preview_url(url, preview_params)
else
url(url)
@ -74,6 +74,8 @@ defmodule Pleroma.Web.MediaProxy do
# non-local non-whitelisted URLs through it and be sure that body size constraint is preserved.
def preview_enabled?, do: enabled?() and !!Config.get([:media_preview_proxy, :enabled])
defp data_url?(url), do: url |> String.slice(0, 5) |> String.downcase() == "data:"
def local?(url), do: String.starts_with?(url, Endpoint.url())
def whitelisted?(url) do

View file

@ -1872,6 +1872,30 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert Activity.get_by_ap_id(data["id"])
end
test "it accepts activities that do not address the inbox owner", %{conn: conn} do
user = insert(:user)
actor = insert(:user, local: false)
data = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Follow",
"id" => "#{actor.ap_id}/follows/1",
"actor" => actor.ap_id,
"object" => user.ap_id,
"to" => ["https://www.w3.org/ns/activitystreams#Public"]
}
conn =
conn
|> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
assert "ok" == json_response(conn, 200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert Activity.get_by_ap_id(data["id"])
end
test "it returns an error when receiving an activity sent to a deactivated user", %{
conn: conn,
data: data

View file

@ -86,6 +86,31 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.EmojiReactHandlingTest do
)
end
test "it works for incoming custom emoji reactions with a single tag object" do
user = insert(:user)
other_user = insert(:user, local: false)
{:ok, activity} = CommonAPI.post(user, %{status: "hello"})
data =
File.read!("test/fixtures/custom-emoji-reaction.json")
|> Jason.decode!()
|> Map.put("object", activity.data["object"])
|> Map.put("actor", other_user.ap_id)
[tag] = data["tag"]
data = Map.put(data, "tag", tag)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["content"] == ":hanapog:"
assert [%{"name" => "hanapog"}] = data["tag"]
object = Object.get_by_ap_id(data["object"])
url = tag["icon"]["url"]
assert match?([["hanapog", _, ^url]], object.data["reactions"])
end
test "it works for incoming unqualified emoji reactions" do
user = insert(:user)
other_user = insert(:user, local: false)

View file

@ -406,74 +406,6 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
end
end
describe "recipient_in_message/3" do
test "returns true when recipient in `to`" do
recipient = insert(:user)
actor = insert(:user)
assert Utils.recipient_in_message(recipient, actor, %{"to" => recipient.ap_id})
assert Utils.recipient_in_message(
recipient,
actor,
%{"to" => [recipient.ap_id], "cc" => ""}
)
end
test "returns true when recipient in `cc`" do
recipient = insert(:user)
actor = insert(:user)
assert Utils.recipient_in_message(recipient, actor, %{"cc" => recipient.ap_id})
assert Utils.recipient_in_message(
recipient,
actor,
%{"cc" => [recipient.ap_id], "to" => ""}
)
end
test "returns true when recipient in `bto`" do
recipient = insert(:user)
actor = insert(:user)
assert Utils.recipient_in_message(recipient, actor, %{"bto" => recipient.ap_id})
assert Utils.recipient_in_message(
recipient,
actor,
%{"bcc" => "", "bto" => [recipient.ap_id]}
)
end
test "returns true when recipient in `bcc`" do
recipient = insert(:user)
actor = insert(:user)
assert Utils.recipient_in_message(recipient, actor, %{"bcc" => recipient.ap_id})
assert Utils.recipient_in_message(
recipient,
actor,
%{"bto" => "", "bcc" => [recipient.ap_id]}
)
end
test "returns true when message without addresses fields" do
recipient = insert(:user)
actor = insert(:user)
assert Utils.recipient_in_message(recipient, actor, %{"bccc" => recipient.ap_id})
assert Utils.recipient_in_message(
recipient,
actor,
%{"btod" => "", "bccc" => [recipient.ap_id]}
)
end
test "returns false" do
recipient = insert(:user)
actor = insert(:user)
refute Utils.recipient_in_message(recipient, actor, %{"to" => "ap_id"})
end
end
describe "lazy_put_activity_defaults/2" do
test "returns map with id and published data" do
note_activity = insert(:note_activity)

View file

@ -8,6 +8,7 @@ defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do
alias Pleroma.Notification
alias Pleroma.Repo
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
import Pleroma.Factory
@ -33,7 +34,7 @@ defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do
assert response == %{
"notifications" => %{
"last_read_id" => "69420",
"updated_at" => NaiveDateTime.to_iso8601(marker.updated_at),
"updated_at" => Utils.to_masto_date(marker.updated_at),
"version" => 0,
"pleroma" => %{"unread_count" => 7}
}
@ -107,7 +108,7 @@ defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do
assert response == %{
"notifications" => %{
"last_read_id" => "69888",
"updated_at" => NaiveDateTime.to_iso8601(marker.updated_at),
"updated_at" => Utils.to_masto_date(marker.updated_at),
"version" => 0,
"pleroma" => %{"unread_count" => 0}
}

View file

@ -4,6 +4,7 @@
defmodule Pleroma.Web.MastodonAPI.MarkerViewTest do
use Pleroma.DataCase, async: true
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.MarkerView
import Pleroma.Factory
@ -14,16 +15,25 @@ defmodule Pleroma.Web.MastodonAPI.MarkerViewTest do
assert MarkerView.render("markers.json", %{markers: [marker1, marker2]}) == %{
"home" => %{
last_read_id: "42",
updated_at: NaiveDateTime.to_iso8601(marker2.updated_at),
updated_at: Utils.to_masto_date(marker2.updated_at),
version: 0,
pleroma: %{unread_count: 0}
},
"notifications" => %{
last_read_id: "17",
updated_at: NaiveDateTime.to_iso8601(marker1.updated_at),
updated_at: Utils.to_masto_date(marker1.updated_at),
version: 0,
pleroma: %{unread_count: 5}
}
}
end
test "renders updated_at in the Mastodon date format" do
marker = insert(:marker, timeline: "home")
%{"home" => %{updated_at: updated_at}} =
MarkerView.render("markers.json", %{markers: [marker]})
assert updated_at =~ ~r/\.\d{3}Z$/
end
end

View file

@ -45,6 +45,18 @@ defmodule Pleroma.Web.MediaProxyTest do
assert MediaProxy.url(local_root) == local_root
end
test "ignores data url" do
assert MediaProxy.url("data:image/png;base64,AAAA") == "data:image/png;base64,AAAA"
assert MediaProxy.url("DATA:image/png;base64,AAAA") == "DATA:image/png;base64,AAAA"
end
test "preview_url ignores data url" do
clear_config([:media_preview_proxy, :enabled], true)
assert MediaProxy.preview_url("data:image/png;base64,AAAA") ==
"data:image/png;base64,AAAA"
end
test "encodes and decodes URL" do
url = "https://pleroma.soykaf.com/static/logo.png"
encoded = MediaProxy.url(url)