logo

pleroma

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

activity_draft.ex (11326B)


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