Merge pull request 'Add htmlMfm key when relevant' (#878) from ilja/akkoma:add_fep-c16b_discovery_mechanism_to_not_always_reparse_mfm into develop

Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/878
Reviewed-by: Oneric <oneric@noreply.akkoma>
This commit is contained in:
Oneric 2025-06-22 14:31:32 +00:00
commit f2c2ec5e27
10 changed files with 170 additions and 14 deletions

View file

@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased
### Added
- We mark our MFM posts as FEP-c16b compliant, and retain remote HTML representations for incoming posts marked as FEP-c16b-compliant. (Safety scrubbers are still applied)
- Prometheus stats now exposes failed ActivityPub deliveries
which failed all attempts and the failure reason
- status and user HTML pages now provide ActivityPub alternate links

View file

@ -34,7 +34,8 @@ Depending on instance configuration the same may be true for GET requests.
### FEP-c16b: Formatting MFM functions
The optional extension term `htmlMfm` is currently not used.
We set the optional extension term `htmlMfm: true` when using content type "text/x.misskeymarkdown".
Incoming messages containing `htmlMfm: true` will not have their content re-parsed.
## Nodeinfo

View file

@ -124,6 +124,8 @@ defp remote_mention_resolver(
end
end
defp fix_misskey_content(object = %{"htmlMfm" => true}), do: object
# See https://akkoma.dev/FoundKeyGang/FoundKey/issues/343
# Misskey/Foundkey drops some of the custom formatting when it sends remotely
# So this basically reprocesses the MFM source
@ -144,20 +146,13 @@ defp fix_misskey_content(
# See https://github.com/misskey-dev/misskey/pull/8787
# This is for compatibility with older Misskey instances
defp fix_misskey_content(%{"_misskey_content" => content} = object) when is_binary(content) do
mention_handler = fn nick, buffer, opts, acc ->
remote_mention_resolver(object, nick, buffer, opts, acc)
end
{linked, _, _} =
Utils.format_input(content, "text/x.misskeymarkdown", mention_handler: mention_handler)
object
|> Map.put("source", %{
"content" => content,
"mediaType" => "text/x.misskeymarkdown"
})
|> Map.put("content", linked)
|> Map.delete("_misskey_content")
|> fix_misskey_content()
end
defp fix_misskey_content(data), do: data

View file

@ -31,6 +31,7 @@ defmacro activity_fields do
defmacro object_fields do
quote bind_quoted: binding() do
field(:content, :string)
field(:htmlMfm, :boolean)
field(:published, ObjectValidators.DateTime)
field(:updated, ObjectValidators.DateTime)

View file

@ -102,7 +102,8 @@ def make_json_ld_header do
"https://www.w3.org/ns/activitystreams",
"#{Endpoint.url()}/schemas/litepub-0.1.jsonld",
%{
"@language" => "und"
"@language" => "und",
"htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm"
}
]
}

View file

@ -47,8 +47,7 @@ defp new(user, params) do
end
def create(user, params) do
user
|> new(params)
new(user, params)
|> status()
|> summary()
|> with_valid(&attachments/1)
@ -236,6 +235,7 @@ defp object(draft) do
end
emoji = Map.merge(emoji, summary_emoji)
media_type = Utils.get_content_type(draft.params[:content_type])
{:ok, note_data, _meta} = Builder.note(draft)
object =
@ -243,14 +243,18 @@ defp object(draft) do
|> Map.put("emoji", emoji)
|> Map.put("source", %{
"content" => draft.status,
"mediaType" => Utils.get_content_type(draft.params[:content_type])
"mediaType" => media_type
})
|> maybe_put("htmlMfm", true, media_type == "text/x.misskeymarkdown")
|> Map.put("generator", draft.params[:generator])
|> Map.put("contentMap", draft.content_map)
%__MODULE__{draft | object: object}
end
defp maybe_put(map, key, value, true), do: map |> Map.put(key, value)
defp maybe_put(map, _, _, _), do: map
defp preview?(draft) do
preview? = Pleroma.Web.Utils.Params.truthy_param?(draft.params[:preview])
%__MODULE__{draft | preview?: preview?}

View file

@ -191,6 +191,31 @@ test "a misskey MFM status with a _misskey_content field should work and be link
end
end
test "an MFM status with htmlMfm should not have its content re-parsed" do
insert(:user, %{ap_id: "https://misskey.local.live/users/92hzkskwgy"})
note =
"test/fixtures/misskey/mfm_x_format.json"
|> File.read!()
|> Jason.decode!()
|> Map.put("htmlMfm", true)
|> Map.put("content", "do not re-parse")
changes = ArticleNotePageValidator.cast_and_validate(note)
%{
valid?: true,
changes: %{
content: content,
source: %{
"mediaType" => "text/x.misskeymarkdown"
}
}
} = changes
assert content == "do not re-parse"
end
test "a Note without replies/first/items validates" do
insert(:user, ap_id: "https://mastodon.social/users/emelie")

View file

@ -144,7 +144,8 @@ test "make_json_ld_header/0" 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"
}
]
}

View file

@ -534,6 +534,34 @@ test "it allows to address a list" do
assert activity.data["listMessage"] == list.ap_id
end
test "it adds the htmlMFM term to MFM posts and properly processes it" do
user = insert(:user)
assert {:ok,
%Pleroma.Activity{
object: %Pleroma.Object{
data: %{
"content" => content,
"source" => %{
"content" => source_content,
"mediaType" => "text/x.misskeymarkdown"
},
"htmlMfm" => html_mfm
}
}
}} =
CommonAPI.post(user, %{
status: "<p class='scrub-this'>$[spin 13:37]</p>",
content_type: "text/x.misskeymarkdown"
})
assert html_mfm == true
assert content =~ "mfm-spin"
assert content =~ "13:37"
refute content =~ "scrub-this"
assert source_content == "<p class='scrub-this'>$[spin 13:37]</p>"
end
test "it returns error when status is empty and no attachments" do
user = insert(:user)

View file

@ -135,6 +135,105 @@ test "successfully processes incoming AP docs with correct origin" do
assert {:cancel, :already_present} = ObanHelpers.perform(job)
end
test "properly processes objects with the htmlMfm attribute true" do
params = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"actor" => "http://mastodon.example.org/users/admin",
"type" => "Create",
"id" => "http://mastodon.example.org/users/admin/activities/1",
"object" => %{
"type" => "Note",
"content" => "<p this-should-be-scrubbed-away>this is the original content</p>",
"source" => %{
"content" =>
"this source content is irrelevant because the object content should be kept",
"mediaType" => "text/x.misskeymarkdown"
},
"htmlMfm" => true,
"id" => "http://mastodon.example.org/users/admin/objects/1",
"attributedTo" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"]
},
"to" => ["https://www.w3.org/ns/activitystreams#Public"]
}
{:ok, job} = Federator.incoming_ap_doc(params)
{:ok, %Pleroma.Activity{data: %{"object" => object_ap_id}}} = ObanHelpers.perform(job)
%Pleroma.Object{data: %{"content" => content, "htmlMfm" => html_mfm}} =
Pleroma.Object.get_by_ap_id(object_ap_id)
assert html_mfm == true
refute content =~ "this-should-be-scrubbed-away"
refute content =~ "source content is irrelevant"
assert content =~ "this is the original content"
end
test "properly processes objects with the htmlMfm attribute false" do
params = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"actor" => "http://mastodon.example.org/users/admin",
"type" => "Create",
"id" => "http://mastodon.example.org/users/admin/activities/1",
"object" => %{
"type" => "Note",
"content" => "<p this-should-be-scrubbed-away>this is the original content</p>",
"source" => %{
"content" => "<p this-should-be-scrubbed-away>$[spin the source content is used]</p>",
"mediaType" => "text/x.misskeymarkdown"
},
"htmlMfm" => false,
"id" => "http://mastodon.example.org/users/admin/objects/1",
"attributedTo" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"]
},
"to" => ["https://www.w3.org/ns/activitystreams#Public"]
}
{:ok, job} = Federator.incoming_ap_doc(params)
{:ok, %Pleroma.Activity{data: %{"object" => object_ap_id}}} = ObanHelpers.perform(job)
%Pleroma.Object{data: %{"content" => content, "htmlMfm" => html_mfm}} =
Pleroma.Object.get_by_ap_id(object_ap_id)
assert html_mfm == false
refute content =~ "this-should-be-scrubbed-away"
assert content =~ "the source content is used"
refute content =~ "this is the original content"
end
test "properly processes objects with the htmlMfm attribute not set" do
params = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"actor" => "http://mastodon.example.org/users/admin",
"type" => "Create",
"id" => "http://mastodon.example.org/users/admin/activities/1",
"object" => %{
"type" => "Note",
"content" => "<p this-should-be-scrubbed-away>this is the original content</p>",
"source" => %{
"content" => "<p this-should-be-scrubbed-away>$[spin the source content is used]</p>",
"mediaType" => "text/x.misskeymarkdown"
},
"id" => "http://mastodon.example.org/users/admin/objects/1",
"attributedTo" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"]
},
"to" => ["https://www.w3.org/ns/activitystreams#Public"]
}
{:ok, job} = Federator.incoming_ap_doc(params)
{:ok, %Pleroma.Activity{data: %{"object" => object_ap_id}}} = ObanHelpers.perform(job)
%Pleroma.Object{data: %{"content" => content} = data} =
Pleroma.Object.get_by_ap_id(object_ap_id)
assert Map.get(data, "htmlMfm") == nil
refute content =~ "this-should-be-scrubbed-away"
assert content =~ "the source content is used"
refute content =~ "this is the original content"
end
test "successfully normalises public scope descriptors" do
params = %{
"@context" => "https://www.w3.org/ns/activitystreams",