Add backend MFM support

This commit is contained in:
Lain Soykaf 2026-05-11 14:53:06 +04:00
commit 6b86e31e5d
No known key found for this signature in database
15 changed files with 423 additions and 10 deletions

View file

@ -144,7 +144,13 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
quarantined_instances: [],
managed_config: true,
static_dir: "instance/static/",
allowed_post_formats: ["text/plain", "text/html", "text/markdown", "text/bbcode"],
allowed_post_formats: [
"text/plain",
"text/html",
"text/markdown",
"text/bbcode",
"text/x.misskeymarkdown"
],
autofollowed_nicknames: [],
max_pinned_statuses: 1,
attachment_links: false,

View file

@ -149,6 +149,171 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest
%{valid?: true} = ArticleNotePageValidator.cast_and_validate(note)
end
test "a Misskey MFM note is rendered from source content" do
user = insert(:user, ap_id: "https://misskey.example/users/alice")
note = %{
"id" => "https://misskey.example/notes/1",
"type" => "Note",
"actor" => user.ap_id,
"attributedTo" => user.ap_id,
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"content" => "original content",
"context" => Utils.generate_context_id(),
"source" => %{
"content" => "$[spin.speed=1s mfm goes here] <script>alert('xss')</script>",
"mediaType" => "text/x.misskeymarkdown"
}
}
%{valid?: true, changes: %{content: content, source: source}} =
ArticleNotePageValidator.cast_and_validate(note)
assert source["mediaType"] == "text/x.misskeymarkdown"
assert content =~ ~s(class="mfm-spin")
assert content =~ ~s(data-mfm-speed="1s")
assert content =~ "mfm goes here"
refute content =~ "original content"
refute content =~ "<script"
end
test "a Misskey MFM note resolves only cached AP mention tags" do
remote_user = insert(:user, ap_id: "https://misskey.example/users/carol")
local_user = insert(:user, nickname: "local_user")
note = %{
"id" => "https://misskey.example/notes/3",
"type" => "Note",
"actor" => remote_user.ap_id,
"attributedTo" => remote_user.ap_id,
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"content" => "original content",
"context" => Utils.generate_context_id(),
"tag" => [
%{
"type" => "Mention",
"name" => "@local_user",
"href" => local_user.ap_id
},
%{
"type" => "Mention",
"name" => "@uncached",
"href" => "https://misskey.example/users/uncached"
}
],
"source" => %{
"content" => "@local_user @uncached $[spin hello]",
"mediaType" => "text/x.misskeymarkdown"
}
}
%{valid?: true, changes: %{content: content}} =
ArticleNotePageValidator.cast_and_validate(note)
assert content =~ local_user.ap_id
assert content =~ "@uncached"
end
test "a Misskey MFM note drops oversized source content instead of parsing it" do
user = insert(:user, ap_id: "https://misskey.example/users/oversized")
note = %{
"id" => "https://misskey.example/notes/4",
"type" => "Note",
"actor" => user.ap_id,
"attributedTo" => user.ap_id,
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"content" => "<span class=\"mfm-spin\">safe fallback</span>",
"context" => Utils.generate_context_id(),
"source" => %{
"content" => String.duplicate("x", 5_001),
"mediaType" => "text/x.misskeymarkdown"
}
}
%{valid?: true, changes: %{content: content, source: source}} =
ArticleNotePageValidator.cast_and_validate(note)
assert content == "<span class=\"mfm-spin\">safe fallback</span>"
refute Map.has_key?(source, "content")
end
test "a note drops oversized non-MFM source content" do
user = insert(:user, ap_id: "https://example.com/users/source")
note = %{
"id" => "https://example.com/notes/1",
"type" => "Note",
"actor" => user.ap_id,
"attributedTo" => user.ap_id,
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"content" => "regular content",
"context" => Utils.generate_context_id(),
"source" => %{
"content" => String.duplicate("x", 5_001),
"mediaType" => "text/markdown"
}
}
%{valid?: true, changes: %{source: source}} = ArticleNotePageValidator.cast_and_validate(note)
assert source == %{"mediaType" => "text/markdown"}
end
test "a Misskey MFM note with legacy _misskey_content is rendered" do
user = insert(:user, ap_id: "https://misskey.example/users/legacy")
note = %{
"id" => "https://misskey.example/notes/5",
"type" => "Note",
"actor" => user.ap_id,
"attributedTo" => user.ap_id,
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"content" => "original content",
"context" => Utils.generate_context_id(),
"_misskey_content" => "$[spin legacy]"
}
%{valid?: true, changes: %{content: content, source: source}} =
ArticleNotePageValidator.cast_and_validate(note)
assert source == %{"content" => "$[spin legacy]", "mediaType" => "text/x.misskeymarkdown"}
assert content =~ ~s(class="mfm-spin")
assert content =~ "legacy"
end
test "a Misskey MFM note with htmlMfm is scrubbed but not rendered from source content" do
user = insert(:user, ap_id: "https://misskey.example/users/bob")
note = %{
"id" => "https://misskey.example/notes/2",
"type" => "Note",
"actor" => user.ap_id,
"attributedTo" => user.ap_id,
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"content" =>
"<span class=\"mfm-spin\">already rendered</span><script>alert('xss')</script>",
"htmlMfm" => true,
"context" => Utils.generate_context_id(),
"source" => %{
"content" => String.duplicate("x", 5_001),
"mediaType" => "text/x.misskeymarkdown"
}
}
%{valid?: true, changes: %{content: content, htmlMfm: true, source: source}} =
ArticleNotePageValidator.cast_and_validate(note)
assert content == "<span class=\"mfm-spin\">already rendered</span>alert(&#39;xss&#39;)"
refute Map.has_key?(source, "content")
end
test "a Note with validated likes collection validates" do
insert(:user, ap_id: "https://pol.social/users/mkljczk")

View file

@ -180,7 +180,8 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
"https://www.w3.org/ns/activitystreams",
"http://localhost:4001/schemas/litepub-0.1.jsonld",
%{
"@language" => "und"
"@language" => "und",
"htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm"
}
]
}
@ -192,7 +193,8 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
"https://www.w3.org/ns/activitystreams",
"http://localhost:4001/schemas/litepub-0.1.jsonld",
%{
"@language" => "pl"
"@language" => "pl",
"htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm"
}
]
}

View file

@ -709,6 +709,47 @@ defmodule Pleroma.Web.CommonAPITest do
assert object.data["source"]["content"] == post
end
test "it renders MFM posts and marks their ActivityPub representation" do
user = insert(:user)
post = "<p class='scrub-this'>$[spin.speed=1s 13:37]</p>"
{:ok, activity} =
CommonAPI.post(user, %{
status: post,
content_type: "text/x.misskeymarkdown"
})
object = Object.normalize(activity, fetch: false)
assert object.data["htmlMfm"] == true
assert object.data["source"] == %{
"content" => post,
"mediaType" => "text/x.misskeymarkdown"
}
assert object.data["content"] =~ ~s(class="mfm-spin")
assert object.data["content"] =~ ~s(data-mfm-speed="1s")
assert object.data["content"] =~ "13:37"
refute object.data["content"] =~ "scrub-this"
end
test "it falls back safely for malformed MFM" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "$[spin.speed=1s=boom malformed]",
content_type: "text/x.misskeymarkdown"
})
object = Object.normalize(activity, fetch: false)
refute object.data["content"] =~ ~s(class="mfm-spin")
assert object.data["content"] =~ "malformed"
end
test "it does not allow replies to direct messages that are not direct messages themselves" do
user = insert(:user)