logo

pleroma

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

activity_draft.ex (10149B)


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