Add backend MFM support
This commit is contained in:
parent
ebcc7684c1
commit
6b86e31e5d
15 changed files with 423 additions and 10 deletions
|
|
@ -203,7 +203,8 @@ config :pleroma, :instance,
|
||||||
"text/plain",
|
"text/plain",
|
||||||
"text/html",
|
"text/html",
|
||||||
"text/markdown",
|
"text/markdown",
|
||||||
"text/bbcode"
|
"text/bbcode",
|
||||||
|
"text/x.misskeymarkdown"
|
||||||
],
|
],
|
||||||
autofollowed_nicknames: [],
|
autofollowed_nicknames: [],
|
||||||
autofollowing_nicknames: [],
|
autofollowing_nicknames: [],
|
||||||
|
|
|
||||||
|
|
@ -815,7 +815,8 @@ config :pleroma, :config_description, [
|
||||||
"text/plain",
|
"text/plain",
|
||||||
"text/html",
|
"text/html",
|
||||||
"text/markdown",
|
"text/markdown",
|
||||||
"text/bbcode"
|
"text/bbcode",
|
||||||
|
"text/x.misskeymarkdown"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
|
|
@ -1394,7 +1395,13 @@ config :pleroma, :config_description, [
|
||||||
label: "Post Content Type",
|
label: "Post Content Type",
|
||||||
type: {:dropdown, :atom},
|
type: {:dropdown, :atom},
|
||||||
description: "Default post formatting option",
|
description: "Default post formatting option",
|
||||||
suggestions: ["text/plain", "text/html", "text/markdown", "text/bbcode"]
|
suggestions: [
|
||||||
|
"text/plain",
|
||||||
|
"text/html",
|
||||||
|
"text/markdown",
|
||||||
|
"text/bbcode",
|
||||||
|
"text/x.misskeymarkdown"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
key: :redirectRootNoLogin,
|
key: :redirectRootNoLogin,
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,13 @@ defmodule Pleroma.Formatter do
|
||||||
Earmark.as_html!(text, %Earmark.Options{compact_output: true, smartypants: false})
|
Earmark.as_html!(text, %Earmark.Options{compact_output: true, smartypants: false})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def markdown_to_html(text, opts) do
|
||||||
|
Earmark.as_html!(
|
||||||
|
text,
|
||||||
|
%Earmark.Options{compact_output: true, smartypants: false} |> Map.merge(opts)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
def html_escape({text, mentions, hashtags}, type) do
|
def html_escape({text, mentions, hashtags}, type) do
|
||||||
{html_escape(text, type), mentions, hashtags}
|
{html_escape(text, type), mentions, hashtags}
|
||||||
end
|
end
|
||||||
|
|
@ -135,6 +142,10 @@ defmodule Pleroma.Formatter do
|
||||||
HTML.filter_tags(text)
|
HTML.filter_tags(text)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def html_escape(text, "text/x.misskeymarkdown") do
|
||||||
|
HTML.filter_tags(text)
|
||||||
|
end
|
||||||
|
|
||||||
def html_escape(text, "text/plain") do
|
def html_escape(text, "text/plain") do
|
||||||
Regex.split(@link_regex, text, include_captures: true)
|
Regex.split(@link_regex, text, include_captures: true)
|
||||||
|> Enum.map_every(2, fn chunk ->
|
|> Enum.map_every(2, fn chunk ->
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,9 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
|
|
||||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||||
|
alias Pleroma.HTML
|
||||||
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Web.CommonAPI.Utils
|
||||||
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
|
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
|
||||||
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
|
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
|
||||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||||
|
|
@ -26,6 +29,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
|
||||||
end
|
end
|
||||||
|
|
||||||
field(:replies, {:array, ObjectValidators.ObjectID}, default: [])
|
field(:replies, {:array, ObjectValidators.ObjectID}, default: [])
|
||||||
|
field(:source, :map)
|
||||||
end
|
end
|
||||||
|
|
||||||
def cast_and_apply(data) do
|
def cast_and_apply(data) do
|
||||||
|
|
@ -80,6 +84,113 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
|
||||||
|
|
||||||
def fix_attachments(data), do: data
|
def fix_attachments(data), do: data
|
||||||
|
|
||||||
|
defp remote_mention_resolver(
|
||||||
|
%{"id" => ap_id, "tag" => tags},
|
||||||
|
"@" <> nickname = mention,
|
||||||
|
buffer,
|
||||||
|
opts,
|
||||||
|
acc
|
||||||
|
)
|
||||||
|
when is_binary(ap_id) and is_list(tags) do
|
||||||
|
initial_host =
|
||||||
|
ap_id
|
||||||
|
|> URI.parse()
|
||||||
|
|> Map.get(:host)
|
||||||
|
|
||||||
|
with mention_tag when not is_nil(mention_tag) <-
|
||||||
|
Enum.find(tags, &mention_tag?(&1, mention, initial_host)),
|
||||||
|
href when is_binary(href) <- mention_tag["href"],
|
||||||
|
%User{} = user <- User.get_cached_by_ap_id(href) do
|
||||||
|
link = Pleroma.Formatter.mention_from_user(user, opts)
|
||||||
|
{link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, user})}}
|
||||||
|
else
|
||||||
|
_ -> {buffer, acc}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp remote_mention_resolver(_object, _mention, buffer, _opts, acc), do: {buffer, acc}
|
||||||
|
|
||||||
|
defp mention_tag?(%{"type" => "Mention", "name" => name}, mention, initial_host)
|
||||||
|
when is_binary(name) do
|
||||||
|
name == mention || mention == "#{name}@#{initial_host}"
|
||||||
|
end
|
||||||
|
|
||||||
|
defp mention_tag?(_tag, _mention, _initial_host), do: false
|
||||||
|
|
||||||
|
defp scrub_content(%{"content" => content} = object) when is_binary(content) do
|
||||||
|
Map.put(object, "content", HTML.filter_tags(content))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp scrub_content(object), do: object
|
||||||
|
|
||||||
|
defp mfm_parse_limit do
|
||||||
|
min(Pleroma.Config.get([:instance, :limit]), Pleroma.Config.get([:instance, :remote_limit]))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp normalize_source(%{"source" => source} = object) when is_binary(source) do
|
||||||
|
object
|
||||||
|
|> Map.put("source", %{"content" => source})
|
||||||
|
|> normalize_source()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp normalize_source(%{"source" => source} = object) when is_map(source) do
|
||||||
|
source =
|
||||||
|
case source["content"] do
|
||||||
|
content when is_binary(content) ->
|
||||||
|
if String.length(content) <= mfm_parse_limit() do
|
||||||
|
source
|
||||||
|
else
|
||||||
|
Map.delete(source, "content")
|
||||||
|
end
|
||||||
|
|
||||||
|
nil ->
|
||||||
|
source
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
Map.delete(source, "content")
|
||||||
|
end
|
||||||
|
|
||||||
|
Map.put(object, "source", source)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp normalize_source(object), do: object
|
||||||
|
|
||||||
|
defp fix_misskey_content(%{"htmlMfm" => true, "content" => content} = object)
|
||||||
|
when is_binary(content) do
|
||||||
|
Map.put(object, "content", HTML.filter_tags(content))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fix_misskey_content(%{"htmlMfm" => true} = object), do: object
|
||||||
|
|
||||||
|
defp fix_misskey_content(
|
||||||
|
%{"source" => %{"mediaType" => "text/x.misskeymarkdown", "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, _mentions, _tags} =
|
||||||
|
Utils.format_input(content, "text/x.misskeymarkdown", mention_handler: mention_handler)
|
||||||
|
|
||||||
|
Map.put(object, "content", linked)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fix_misskey_content(%{"source" => %{"mediaType" => "text/x.misskeymarkdown"}} = object),
|
||||||
|
do: scrub_content(object)
|
||||||
|
|
||||||
|
defp fix_misskey_content(%{"_misskey_content" => content} = object) when is_binary(content) do
|
||||||
|
object
|
||||||
|
|> Map.put("source", %{
|
||||||
|
"content" => content,
|
||||||
|
"mediaType" => "text/x.misskeymarkdown"
|
||||||
|
})
|
||||||
|
|> Map.delete("_misskey_content")
|
||||||
|
|> fix_misskey_content()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fix_misskey_content(object), do: object
|
||||||
|
|
||||||
defp fix(data) do
|
defp fix(data) do
|
||||||
data
|
data
|
||||||
|> CommonFixes.fix_actor()
|
|> CommonFixes.fix_actor()
|
||||||
|
|
@ -88,6 +199,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
|
||||||
|> fix_tag()
|
|> fix_tag()
|
||||||
|> fix_replies()
|
|> fix_replies()
|
||||||
|> fix_attachments()
|
|> fix_attachments()
|
||||||
|
|> normalize_source()
|
||||||
|
|> fix_misskey_content()
|
||||||
|> CommonFixes.fix_quote_url()
|
|> CommonFixes.fix_quote_url()
|
||||||
|> CommonFixes.fix_likes()
|
|> CommonFixes.fix_likes()
|
||||||
|> Transmogrifier.fix_emoji()
|
|> Transmogrifier.fix_emoji()
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do
|
||||||
quote bind_quoted: binding() do
|
quote bind_quoted: binding() do
|
||||||
field(:content, :string)
|
field(:content, :string)
|
||||||
field(:contentMap, ObjectValidators.ContentLanguageMap)
|
field(:contentMap, ObjectValidators.ContentLanguageMap)
|
||||||
|
field(:htmlMfm, :boolean)
|
||||||
|
|
||||||
field(:published, ObjectValidators.DateTime)
|
field(:published, ObjectValidators.DateTime)
|
||||||
field(:updated, ObjectValidators.DateTime)
|
field(:updated, ObjectValidators.DateTime)
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
||||||
"https://www.w3.org/ns/activitystreams",
|
"https://www.w3.org/ns/activitystreams",
|
||||||
"#{Endpoint.url()}/schemas/litepub-0.1.jsonld",
|
"#{Endpoint.url()}/schemas/litepub-0.1.jsonld",
|
||||||
%{
|
%{
|
||||||
"@language" => get_language(data)
|
"@language" => get_language(data),
|
||||||
|
"htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -317,6 +317,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
||||||
|
|
||||||
emoji = Map.merge(emoji, summary_emoji)
|
emoji = Map.merge(emoji, summary_emoji)
|
||||||
|
|
||||||
|
media_type = Utils.get_content_type(draft.params[:content_type])
|
||||||
{:ok, note_data, _meta} = Builder.note(draft)
|
{:ok, note_data, _meta} = Builder.note(draft)
|
||||||
|
|
||||||
object =
|
object =
|
||||||
|
|
@ -324,14 +325,18 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
||||||
|> Map.put("emoji", emoji)
|
|> Map.put("emoji", emoji)
|
||||||
|> Map.put("source", %{
|
|> Map.put("source", %{
|
||||||
"content" => draft.status,
|
"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("generator", draft.params[:generator])
|
||||||
|> Map.put("language", draft.language)
|
|> Map.put("language", draft.language)
|
||||||
|
|
||||||
%{draft | object: object}
|
%{draft | object: object}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp maybe_put(map, key, value, true), do: Map.put(map, key, value)
|
||||||
|
defp maybe_put(map, _key, _value, _condition), do: map
|
||||||
|
|
||||||
defp preview?(%__MODULE__{} = draft) do
|
defp preview?(%__MODULE__{} = draft) do
|
||||||
preview? = Pleroma.Web.Utils.Params.truthy_param?(draft.params[:preview])
|
preview? = Pleroma.Web.Utils.Params.truthy_param?(draft.params[:preview])
|
||||||
%{draft | preview?: preview?}
|
%{draft | preview?: preview?}
|
||||||
|
|
|
||||||
|
|
@ -322,6 +322,14 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
||||||
|> Formatter.linkify(options)
|
|> Formatter.linkify(options)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def format_input(text, "text/x.misskeymarkdown", options) do
|
||||||
|
text
|
||||||
|
|> Formatter.markdown_to_html(%{breaks: true})
|
||||||
|
|> safe_mfm_to_html()
|
||||||
|
|> Formatter.linkify(options)
|
||||||
|
|> Formatter.html_escape("text/x.misskeymarkdown")
|
||||||
|
end
|
||||||
|
|
||||||
def format_input(text, "text/markdown", options) do
|
def format_input(text, "text/markdown", options) do
|
||||||
text
|
text
|
||||||
|> Formatter.mentions_escape(options)
|
|> Formatter.mentions_escape(options)
|
||||||
|
|
@ -330,6 +338,16 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
||||||
|> Formatter.html_escape("text/html")
|
|> Formatter.html_escape("text/html")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp safe_mfm_to_html(html) do
|
||||||
|
html
|
||||||
|
|> MfmParser.Parser.parse()
|
||||||
|
|> MfmParser.Encoder.to_html()
|
||||||
|
rescue
|
||||||
|
_ -> html
|
||||||
|
catch
|
||||||
|
_, _ -> html
|
||||||
|
end
|
||||||
|
|
||||||
def format_naive_asctime(date) do
|
def format_naive_asctime(date) do
|
||||||
date |> DateTime.from_naive!("Etc/UTC") |> format_asctime
|
date |> DateTime.from_naive!("Etc/UTC") |> format_asctime
|
||||||
end
|
end
|
||||||
|
|
|
||||||
3
mix.exs
3
mix.exs
|
|
@ -160,6 +160,9 @@ defmodule Pleroma.Mixfile do
|
||||||
{:sweet_xml, "~> 0.7.5"},
|
{:sweet_xml, "~> 0.7.5"},
|
||||||
{:earmark, "1.4.46"},
|
{:earmark, "1.4.46"},
|
||||||
{:bbcode_pleroma, "~> 0.2.0"},
|
{:bbcode_pleroma, "~> 0.2.0"},
|
||||||
|
{:mfm_parser,
|
||||||
|
git: "https://akkoma.dev/AkkomaGang/mfm-parser.git",
|
||||||
|
ref: "360a30267a847810a63ab48f606ba227b2ca05f0"},
|
||||||
{:cors_plug, "~> 2.0"},
|
{:cors_plug, "~> 2.0"},
|
||||||
{:web_push_encryption, "~> 0.3.1"},
|
{:web_push_encryption, "~> 0.3.1"},
|
||||||
{:swoosh, "~> 1.16.12"},
|
{:swoosh, "~> 1.16.12"},
|
||||||
|
|
|
||||||
1
mix.lock
1
mix.lock
|
|
@ -79,6 +79,7 @@
|
||||||
"makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"},
|
"makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"},
|
||||||
"meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"},
|
"meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"},
|
||||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
||||||
|
"mfm_parser": {:git, "https://akkoma.dev/AkkomaGang/mfm-parser.git", "360a30267a847810a63ab48f606ba227b2ca05f0", [ref: "360a30267a847810a63ab48f606ba227b2ca05f0"]},
|
||||||
"mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"},
|
"mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"},
|
||||||
"mimerl": {:hex, :mimerl, "1.5.0", "f35aca6f23242339b3666e0ac0702379e362b469d0aea167f6cc713547e777ed", [:rebar3], [], "hexpm", "db648ce065bae14ea84ca8b5dd123f42f49417cef693541110bf6f9e9be9ecc4"},
|
"mimerl": {:hex, :mimerl, "1.5.0", "f35aca6f23242339b3666e0ac0702379e362b469d0aea167f6cc713547e777ed", [:rebar3], [], "hexpm", "db648ce065bae14ea84ca8b5dd123f42f49417cef693541110bf6f9e9be9ecc4"},
|
||||||
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
|
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
|
||||||
|
|
|
||||||
|
|
@ -82,12 +82,50 @@ defmodule Pleroma.HTML.Scrubber.Default do
|
||||||
"recipients-inline",
|
"recipients-inline",
|
||||||
"quote-inline",
|
"quote-inline",
|
||||||
"invisible",
|
"invisible",
|
||||||
"ellipsis"
|
"ellipsis",
|
||||||
|
"mfm-center",
|
||||||
|
"mfm-flip",
|
||||||
|
"mfm-font",
|
||||||
|
"mfm-blur",
|
||||||
|
"mfm-rotate",
|
||||||
|
"mfm-x2",
|
||||||
|
"mfm-x3",
|
||||||
|
"mfm-x4",
|
||||||
|
"mfm-position",
|
||||||
|
"mfm-scale",
|
||||||
|
"mfm-fg",
|
||||||
|
"mfm-bg",
|
||||||
|
"mfm-jelly",
|
||||||
|
"mfm-twitch",
|
||||||
|
"mfm-shake",
|
||||||
|
"mfm-spin",
|
||||||
|
"mfm-jump",
|
||||||
|
"mfm-bounce",
|
||||||
|
"mfm-rainbow",
|
||||||
|
"mfm-tada",
|
||||||
|
"mfm-sparkle"
|
||||||
])
|
])
|
||||||
|
|
||||||
Meta.allow_tag_with_this_attribute_values(:p, "class", ["quote-inline"])
|
Meta.allow_tag_with_this_attribute_values(:p, "class", ["quote-inline"])
|
||||||
|
|
||||||
Meta.allow_tag_with_these_attributes(:span, ["lang"])
|
Meta.allow_tag_with_these_attributes(:span, [
|
||||||
|
"lang",
|
||||||
|
"data-mfm-h",
|
||||||
|
"data-mfm-v",
|
||||||
|
"data-mfm-x",
|
||||||
|
"data-mfm-y",
|
||||||
|
"data-mfm-alternate",
|
||||||
|
"data-mfm-speed",
|
||||||
|
"data-mfm-deg",
|
||||||
|
"data-mfm-left",
|
||||||
|
"data-mfm-serif",
|
||||||
|
"data-mfm-monospace",
|
||||||
|
"data-mfm-cursive",
|
||||||
|
"data-mfm-fantasy",
|
||||||
|
"data-mfm-emoji",
|
||||||
|
"data-mfm-math",
|
||||||
|
"data-mfm-color"
|
||||||
|
])
|
||||||
|
|
||||||
Meta.allow_tag_with_this_attribute_values(:code, "class", ["inline"])
|
Meta.allow_tag_with_this_attribute_values(:code, "class", ["inline"])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,13 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
|
||||||
quarantined_instances: [],
|
quarantined_instances: [],
|
||||||
managed_config: true,
|
managed_config: true,
|
||||||
static_dir: "instance/static/",
|
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: [],
|
autofollowed_nicknames: [],
|
||||||
max_pinned_statuses: 1,
|
max_pinned_statuses: 1,
|
||||||
attachment_links: false,
|
attachment_links: false,
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,171 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest
|
||||||
%{valid?: true} = ArticleNotePageValidator.cast_and_validate(note)
|
%{valid?: true} = ArticleNotePageValidator.cast_and_validate(note)
|
||||||
end
|
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('xss')"
|
||||||
|
refute Map.has_key?(source, "content")
|
||||||
|
end
|
||||||
|
|
||||||
test "a Note with validated likes collection validates" do
|
test "a Note with validated likes collection validates" do
|
||||||
insert(:user, ap_id: "https://pol.social/users/mkljczk")
|
insert(:user, ap_id: "https://pol.social/users/mkljczk")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -180,7 +180,8 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
|
||||||
"https://www.w3.org/ns/activitystreams",
|
"https://www.w3.org/ns/activitystreams",
|
||||||
"http://localhost:4001/schemas/litepub-0.1.jsonld",
|
"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",
|
"https://www.w3.org/ns/activitystreams",
|
||||||
"http://localhost:4001/schemas/litepub-0.1.jsonld",
|
"http://localhost:4001/schemas/litepub-0.1.jsonld",
|
||||||
%{
|
%{
|
||||||
"@language" => "pl"
|
"@language" => "pl",
|
||||||
|
"htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -709,6 +709,47 @@ defmodule Pleroma.Web.CommonAPITest do
|
||||||
assert object.data["source"]["content"] == post
|
assert object.data["source"]["content"] == post
|
||||||
end
|
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
|
test "it does not allow replies to direct messages that are not direct messages themselves" do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue