logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma git clone https://hacktivis.me/git/pleroma.git

force_mentions_in_content.ex (3942B)


  1. # Pleroma: A lightweight social networking server
  2. # Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
  3. # SPDX-License-Identifier: AGPL-3.0-only
  4. defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent do
  5. require Pleroma.Constants
  6. alias Pleroma.Formatter
  7. alias Pleroma.Object
  8. alias Pleroma.User
  9. @behaviour Pleroma.Web.ActivityPub.MRF.Policy
  10. @impl true
  11. def history_awareness, do: :auto
  12. defp do_extract({:a, attrs, _}, acc) do
  13. if Enum.find(attrs, fn {name, value} ->
  14. name == "class" && value in ["mention", "u-url mention", "mention u-url"]
  15. end) do
  16. href = Enum.find(attrs, fn {name, _} -> name == "href" end) |> elem(1)
  17. acc ++ [href]
  18. else
  19. acc
  20. end
  21. end
  22. defp do_extract({_, _, children}, acc) do
  23. do_extract(children, acc)
  24. end
  25. defp do_extract(nodes, acc) when is_list(nodes) do
  26. Enum.reduce(nodes, acc, fn node, acc -> do_extract(node, acc) end)
  27. end
  28. defp do_extract(_, acc), do: acc
  29. defp extract_mention_uris_from_content(content) do
  30. {:ok, tree} = :fast_html.decode(content, format: [:html_atoms])
  31. do_extract(tree, [])
  32. end
  33. defp get_replied_to_user(%{"inReplyTo" => in_reply_to}) do
  34. case Object.normalize(in_reply_to, fetch: false) do
  35. %Object{data: %{"actor" => actor}} -> User.get_cached_by_ap_id(actor)
  36. _ -> nil
  37. end
  38. end
  39. defp get_replied_to_user(_object), do: nil
  40. # Ensure the replied-to user is sorted to the left
  41. defp sort_replied_user([%User{id: user_id} | _] = users, %User{id: user_id}), do: users
  42. defp sort_replied_user(users, %User{id: user_id} = user) do
  43. if Enum.find(users, fn u -> u.id == user_id end) do
  44. users = Enum.reject(users, fn u -> u.id == user_id end)
  45. [user | users]
  46. else
  47. users
  48. end
  49. end
  50. defp sort_replied_user(users, _), do: users
  51. # Drop constants and the actor's own AP ID
  52. defp clean_recipients(recipients, object) do
  53. Enum.reject(recipients, fn ap_id ->
  54. ap_id in [
  55. object["object"]["actor"],
  56. Pleroma.Constants.as_public(),
  57. Pleroma.Web.ActivityPub.Utils.as_local_public()
  58. ]
  59. end)
  60. end
  61. @impl true
  62. def filter(
  63. %{
  64. "type" => type,
  65. "object" => %{"type" => "Note", "to" => to, "inReplyTo" => in_reply_to}
  66. } = object
  67. )
  68. when type in ["Create", "Update"] and is_list(to) and is_binary(in_reply_to) do
  69. # image-only posts from pleroma apparently reach this MRF without the content field
  70. content = object["object"]["content"] || ""
  71. # Get the replied-to user for sorting
  72. replied_to_user = get_replied_to_user(object["object"])
  73. mention_users =
  74. to
  75. |> clean_recipients(object)
  76. |> Enum.map(&User.get_cached_by_ap_id/1)
  77. |> Enum.reject(&is_nil/1)
  78. |> sort_replied_user(replied_to_user)
  79. explicitly_mentioned_uris =
  80. extract_mention_uris_from_content(content)
  81. |> MapSet.new()
  82. added_mentions =
  83. Enum.reduce(mention_users, "", fn %User{ap_id: ap_id, uri: uri} = user, acc ->
  84. if MapSet.disjoint?(MapSet.new([ap_id, uri]), explicitly_mentioned_uris) do
  85. acc <> Formatter.mention_from_user(user, %{mentions_format: :compact}) <> " "
  86. else
  87. acc
  88. end
  89. end)
  90. recipients_inline =
  91. if added_mentions != "",
  92. do: "<span class=\"recipients-inline\">#{added_mentions}</span>",
  93. else: ""
  94. content =
  95. cond do
  96. # For Markdown posts, insert the mentions inside the first <p> tag
  97. recipients_inline != "" && String.starts_with?(content, "<p>") ->
  98. "<p>" <> recipients_inline <> String.trim_leading(content, "<p>")
  99. recipients_inline != "" ->
  100. recipients_inline <> content
  101. true ->
  102. content
  103. end
  104. {:ok, put_in(object["object"]["content"], content)}
  105. end
  106. @impl true
  107. def filter(object), do: {:ok, object}
  108. @impl true
  109. def describe, do: {:ok, %{}}
  110. end