logo

pleroma

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

activity_draft.ex (9337B)


  1. # Pleroma: A lightweight social networking server
  2. # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
  3. # SPDX-License-Identifier: AGPL-3.0-only
  4. defmodule Pleroma.Web.CommonAPI.ActivityDraft do
  5. alias Pleroma.Activity
  6. alias Pleroma.Conversation.Participation
  7. alias Pleroma.Object
  8. alias Pleroma.Web.ActivityPub.Builder
  9. alias Pleroma.Web.ActivityPub.Visibility
  10. alias Pleroma.Web.CommonAPI
  11. alias Pleroma.Web.CommonAPI.Utils
  12. import Pleroma.Web.Gettext
  13. import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
  14. @type t :: %__MODULE__{}
  15. defstruct valid?: true,
  16. errors: [],
  17. user: nil,
  18. params: %{},
  19. status: nil,
  20. summary: nil,
  21. full_payload: nil,
  22. attachments: [],
  23. in_reply_to: nil,
  24. in_reply_to_conversation: nil,
  25. quote_post: nil,
  26. visibility: nil,
  27. expires_at: nil,
  28. extra: nil,
  29. emoji: %{},
  30. content_html: nil,
  31. mentions: [],
  32. tags: [],
  33. to: [],
  34. cc: [],
  35. context: nil,
  36. sensitive: false,
  37. object: nil,
  38. preview?: false,
  39. changes: %{}
  40. def new(user, params) do
  41. %__MODULE__{user: user}
  42. |> put_params(params)
  43. end
  44. def create(user, params) do
  45. user
  46. |> new(params)
  47. |> status()
  48. |> summary()
  49. |> with_valid(&attachments/1)
  50. |> full_payload()
  51. |> expires_at()
  52. |> poll()
  53. |> with_valid(&in_reply_to/1)
  54. |> with_valid(&in_reply_to_conversation/1)
  55. |> with_valid(&quote_post/1)
  56. |> with_valid(&visibility/1)
  57. |> with_valid(&quoting_visibility/1)
  58. |> content()
  59. |> with_valid(&to_and_cc/1)
  60. |> with_valid(&context/1)
  61. |> sensitive()
  62. |> with_valid(&object/1)
  63. |> preview?()
  64. |> with_valid(&changes/1)
  65. |> validate()
  66. end
  67. def listen(user, params) do
  68. user
  69. |> new(params)
  70. |> visibility()
  71. |> to_and_cc()
  72. |> context()
  73. |> listen_object()
  74. |> with_valid(&changes/1)
  75. |> validate()
  76. end
  77. defp listen_object(draft) do
  78. object =
  79. draft.params
  80. |> Map.take([:album, :artist, :title, :length, :externalLink])
  81. |> Map.new(fn {key, value} -> {to_string(key), value} end)
  82. |> Map.put("type", "Audio")
  83. |> Map.put("to", draft.to)
  84. |> Map.put("cc", draft.cc)
  85. |> Map.put("actor", draft.user.ap_id)
  86. %__MODULE__{draft | object: object}
  87. end
  88. defp put_params(draft, params) do
  89. params = Map.put_new(params, :in_reply_to_status_id, params[:in_reply_to_id])
  90. %__MODULE__{draft | params: params}
  91. end
  92. defp status(%{params: %{status: status}} = draft) do
  93. %__MODULE__{draft | status: String.trim(status)}
  94. end
  95. defp summary(%{params: params} = draft) do
  96. %__MODULE__{draft | summary: Map.get(params, :spoiler_text, "")}
  97. end
  98. defp full_payload(%{status: status, summary: summary} = draft) do
  99. full_payload = String.trim(status <> summary)
  100. case Utils.validate_character_limit(full_payload, draft.attachments) do
  101. :ok -> %__MODULE__{draft | full_payload: full_payload}
  102. {:error, message} -> add_error(draft, message)
  103. end
  104. end
  105. defp attachments(%{params: params} = draft) do
  106. attachments = Utils.attachments_from_ids(params, draft.user)
  107. draft = %__MODULE__{draft | attachments: attachments}
  108. case Utils.validate_attachments_count(attachments) do
  109. :ok -> draft
  110. {:error, message} -> add_error(draft, message)
  111. end
  112. end
  113. defp in_reply_to(%{params: %{in_reply_to_status_id: ""}} = draft), do: draft
  114. defp in_reply_to(%{params: %{in_reply_to_status_id: id}} = draft) when is_binary(id) do
  115. %__MODULE__{draft | in_reply_to: Activity.get_by_id(id)}
  116. end
  117. defp in_reply_to(%{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft) do
  118. %__MODULE__{draft | in_reply_to: in_reply_to}
  119. end
  120. defp in_reply_to(draft), do: draft
  121. defp quote_post(%{params: %{quote_id: id}} = draft) when not_empty_string(id) do
  122. case Activity.get_by_id_with_object(id) do
  123. %Activity{} = activity ->
  124. %__MODULE__{draft | quote_post: activity}
  125. _ ->
  126. draft
  127. end
  128. end
  129. defp quote_post(draft), do: draft
  130. defp in_reply_to_conversation(draft) do
  131. in_reply_to_conversation = Participation.get(draft.params[:in_reply_to_conversation_id])
  132. %__MODULE__{draft | in_reply_to_conversation: in_reply_to_conversation}
  133. end
  134. defp visibility(%{params: params} = draft) do
  135. case CommonAPI.get_visibility(params, draft.in_reply_to, draft.in_reply_to_conversation) do
  136. {visibility, "direct"} when visibility != "direct" ->
  137. add_error(draft, dgettext("errors", "The message visibility must be direct"))
  138. {visibility, _} ->
  139. %__MODULE__{draft | visibility: visibility}
  140. end
  141. end
  142. defp can_quote?(_draft, _object, visibility) when visibility in ~w(public unlisted local) do
  143. true
  144. end
  145. defp can_quote?(draft, object, "private") do
  146. draft.user.ap_id == object.data["actor"]
  147. end
  148. defp can_quote?(_, _, _) do
  149. false
  150. end
  151. defp quoting_visibility(%{quote_post: %Activity{}} = draft) do
  152. with %Object{} = object <- Object.normalize(draft.quote_post, fetch: false),
  153. true <- can_quote?(draft, object, Visibility.get_visibility(object)) do
  154. draft
  155. else
  156. _ -> add_error(draft, dgettext("errors", "Cannot quote private message"))
  157. end
  158. end
  159. defp quoting_visibility(draft), do: draft
  160. defp expires_at(draft) do
  161. case CommonAPI.check_expiry_date(draft.params[:expires_in]) do
  162. {:ok, expires_at} -> %__MODULE__{draft | expires_at: expires_at}
  163. {:error, message} -> add_error(draft, message)
  164. end
  165. end
  166. defp poll(draft) do
  167. case Utils.make_poll_data(draft.params) do
  168. {:ok, {poll, poll_emoji}} ->
  169. %__MODULE__{draft | extra: poll, emoji: Map.merge(draft.emoji, poll_emoji)}
  170. {:error, message} ->
  171. add_error(draft, message)
  172. end
  173. end
  174. defp content(%{mentions: mentions} = draft) do
  175. {content_html, mentioned_users, tags} = Utils.make_content_html(draft)
  176. mentioned_ap_ids =
  177. Enum.map(mentioned_users, fn {_, mentioned_user} -> mentioned_user.ap_id end)
  178. mentions =
  179. mentions
  180. |> Kernel.++(mentioned_ap_ids)
  181. |> Utils.get_addressed_users(draft.params[:to])
  182. %__MODULE__{draft | content_html: content_html, mentions: mentions, tags: tags}
  183. end
  184. defp to_and_cc(draft) do
  185. {to, cc} = Utils.get_to_and_cc(draft)
  186. %__MODULE__{draft | to: to, cc: cc}
  187. end
  188. defp context(draft) do
  189. context = Utils.make_context(draft.in_reply_to, draft.in_reply_to_conversation)
  190. %__MODULE__{draft | context: context}
  191. end
  192. defp sensitive(draft) do
  193. sensitive = draft.params[:sensitive]
  194. %__MODULE__{draft | sensitive: sensitive}
  195. end
  196. defp object(draft) do
  197. emoji = Map.merge(Pleroma.Emoji.Formatter.get_emoji_map(draft.full_payload), draft.emoji)
  198. # Sometimes people create posts with subject containing emoji,
  199. # since subjects are usually copied this will result in a broken
  200. # subject when someone replies from an instance that does not have
  201. # the emoji or has it under different shortcode. This is an attempt
  202. # to mitigate this by copying emoji from inReplyTo if they are present
  203. # in the subject.
  204. summary_emoji =
  205. with %Activity{} <- draft.in_reply_to,
  206. %Object{data: %{"tag" => [_ | _] = tag}} <- Object.normalize(draft.in_reply_to) do
  207. Enum.reduce(tag, %{}, fn
  208. %{"type" => "Emoji", "name" => name, "icon" => %{"url" => url}}, acc ->
  209. if String.contains?(draft.summary, name) do
  210. Map.put(acc, name, url)
  211. else
  212. acc
  213. end
  214. _, acc ->
  215. acc
  216. end)
  217. else
  218. _ -> %{}
  219. end
  220. emoji = Map.merge(emoji, summary_emoji)
  221. {:ok, note_data, _meta} = Builder.note(draft)
  222. object =
  223. note_data
  224. |> Map.put("emoji", emoji)
  225. |> Map.put("source", %{
  226. "content" => draft.status,
  227. "mediaType" => Utils.get_content_type(draft.params[:content_type])
  228. })
  229. |> Map.put("generator", draft.params[:generator])
  230. %__MODULE__{draft | object: object}
  231. end
  232. defp preview?(draft) do
  233. preview? = Pleroma.Web.Utils.Params.truthy_param?(draft.params[:preview])
  234. %__MODULE__{draft | preview?: preview?}
  235. end
  236. defp changes(draft) do
  237. direct? = draft.visibility == "direct"
  238. additional = %{"cc" => draft.cc, "directMessage" => direct?}
  239. additional =
  240. case draft.expires_at do
  241. %DateTime{} = expires_at -> Map.put(additional, "expires_at", expires_at)
  242. _ -> additional
  243. end
  244. changes =
  245. %{
  246. to: draft.to,
  247. actor: draft.user,
  248. context: draft.context,
  249. object: draft.object,
  250. additional: additional
  251. }
  252. |> Utils.maybe_add_list_data(draft.user, draft.visibility)
  253. %__MODULE__{draft | changes: changes}
  254. end
  255. defp with_valid(%{valid?: true} = draft, func), do: func.(draft)
  256. defp with_valid(draft, _func), do: draft
  257. defp add_error(draft, message) do
  258. %__MODULE__{draft | valid?: false, errors: [message | draft.errors]}
  259. end
  260. defp validate(%{valid?: true} = draft), do: {:ok, draft}
  261. defp validate(%{errors: [message | _]}), do: {:error, message}
  262. end