logo

pleroma

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

common_api.ex (25978B)


  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 do
  5. alias Pleroma.Activity
  6. alias Pleroma.Conversation.Participation
  7. alias Pleroma.Formatter
  8. alias Pleroma.ModerationLog
  9. alias Pleroma.Object
  10. alias Pleroma.Rule
  11. alias Pleroma.ThreadMute
  12. alias Pleroma.User
  13. alias Pleroma.UserRelationship
  14. alias Pleroma.Web.ActivityPub.ActivityPub
  15. alias Pleroma.Web.ActivityPub.Builder
  16. alias Pleroma.Web.ActivityPub.Pipeline
  17. alias Pleroma.Web.ActivityPub.Utils
  18. alias Pleroma.Web.ActivityPub.Visibility
  19. alias Pleroma.Web.CommonAPI.ActivityDraft
  20. import Ecto.Query, only: [where: 3]
  21. import Pleroma.Web.Gettext
  22. import Pleroma.Web.CommonAPI.Utils
  23. require Pleroma.Constants
  24. require Logger
  25. @spec block(User.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors()
  26. def block(blocked, blocker) do
  27. with {:ok, block_data, _} <- Builder.block(blocker, blocked),
  28. {:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do
  29. {:ok, block}
  30. end
  31. end
  32. @spec post_chat_message(User.t(), User.t(), String.t(), list()) ::
  33. {:ok, Activity.t()} | Pipeline.errors()
  34. def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do
  35. with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
  36. :ok <- validate_chat_attachment_attribution(maybe_attachment, user),
  37. :ok <- validate_chat_content_length(content, !!maybe_attachment),
  38. {_, {:ok, chat_message_data, _meta}} <-
  39. {:build_object,
  40. Builder.chat_message(
  41. user,
  42. recipient.ap_id,
  43. content |> format_chat_content,
  44. attachment: maybe_attachment
  45. )},
  46. {_, {:ok, create_activity_data, _meta}} <-
  47. {:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])},
  48. {_, {:ok, %Activity{} = activity, _meta}} <-
  49. {:common_pipeline,
  50. Pipeline.common_pipeline(create_activity_data,
  51. local: true,
  52. idempotency_key: opts[:idempotency_key]
  53. )} do
  54. {:ok, activity}
  55. else
  56. {:common_pipeline, e} -> e
  57. e -> e
  58. end
  59. end
  60. defp format_chat_content(nil), do: nil
  61. defp format_chat_content(content) do
  62. {text, _, _} =
  63. content
  64. |> Formatter.html_escape("text/plain")
  65. |> Formatter.linkify()
  66. |> (fn {text, mentions, tags} ->
  67. {String.replace(text, ~r/\r?\n/, "<br>"), mentions, tags}
  68. end).()
  69. text
  70. end
  71. defp validate_chat_attachment_attribution(nil, _), do: :ok
  72. defp validate_chat_attachment_attribution(attachment, user) do
  73. with :ok <- Object.authorize_access(attachment, user) do
  74. :ok
  75. else
  76. e ->
  77. e
  78. end
  79. end
  80. defp validate_chat_content_length(_, true), do: :ok
  81. defp validate_chat_content_length(nil, false), do: {:error, :no_content}
  82. defp validate_chat_content_length(content, _) do
  83. if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do
  84. :ok
  85. else
  86. {:error, :content_too_long}
  87. end
  88. end
  89. @spec unblock(User.t(), User.t()) ::
  90. {:ok, Activity.t()} | {:ok, :no_activity} | Pipeline.errors() | {:error, :not_blocking}
  91. def unblock(blocked, blocker) do
  92. with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
  93. {:ok, unblock_data, _} <- Builder.undo(blocker, block),
  94. {:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
  95. {:ok, unblock}
  96. else
  97. {:fetch_block, nil} ->
  98. if User.blocks?(blocker, blocked) do
  99. User.unblock(blocker, blocked)
  100. {:ok, :no_activity}
  101. else
  102. {:error, :not_blocking}
  103. end
  104. e ->
  105. e
  106. end
  107. end
  108. @spec follow(User.t(), User.t()) ::
  109. {:ok, User.t(), User.t(), Activity.t() | Object.t()}
  110. | {:error, :rejected}
  111. | Pipeline.errors()
  112. def follow(followed, follower) do
  113. timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
  114. with {:ok, follow_data, _} <- Builder.follow(follower, followed),
  115. {:ok, activity, _} <- Pipeline.common_pipeline(follow_data, local: true),
  116. {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
  117. if activity.data["state"] == "reject" do
  118. {:error, :rejected}
  119. else
  120. {:ok, followed, follower, activity}
  121. end
  122. end
  123. end
  124. @spec unfollow(User.t(), User.t()) :: {:ok, User.t()} | {:error, any()}
  125. def unfollow(unfollowed, follower) do
  126. with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
  127. {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
  128. {:ok, _subscription} <- User.unsubscribe(follower, unfollowed),
  129. {:ok, _endorsement} <- User.unendorse(follower, unfollowed) do
  130. {:ok, follower}
  131. end
  132. end
  133. @spec accept_follow_request(User.t(), User.t()) :: {:ok, User.t()} | Pipeline.errors()
  134. def accept_follow_request(follower, followed) do
  135. with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
  136. {:ok, accept_data, _} <- Builder.accept(followed, follow_activity),
  137. {:ok, _activity, _} <- Pipeline.common_pipeline(accept_data, local: true) do
  138. {:ok, follower}
  139. end
  140. end
  141. @spec reject_follow_request(User.t(), User.t()) :: {:ok, User.t()} | Pipeline.errors() | nil
  142. def reject_follow_request(follower, followed) do
  143. with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
  144. {:ok, reject_data, _} <- Builder.reject(followed, follow_activity),
  145. {:ok, _activity, _} <- Pipeline.common_pipeline(reject_data, local: true) do
  146. {:ok, follower}
  147. end
  148. end
  149. @spec delete(String.t(), User.t()) ::
  150. {:ok, Activity.t()} | Pipeline.errors() | {:error, :not_found | String.t()}
  151. def delete(activity_id, user) do
  152. with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
  153. {:find_activity, Activity.get_by_id(activity_id, filter: [])},
  154. {_, {:ok, _}} <- {:cancel_jobs, maybe_cancel_jobs(activity)},
  155. {_, %Object{} = object, _} <-
  156. {:find_object, Object.normalize(activity, fetch: false), activity},
  157. true <- User.privileged?(user, :messages_delete) || user.ap_id == object.data["actor"],
  158. {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
  159. {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
  160. if User.privileged?(user, :messages_delete) and user.ap_id != object.data["actor"] do
  161. action =
  162. if object.data["type"] == "ChatMessage" do
  163. "chat_message_delete"
  164. else
  165. "status_delete"
  166. end
  167. ModerationLog.insert_log(%{
  168. action: action,
  169. actor: user,
  170. subject_id: activity_id
  171. })
  172. end
  173. {:ok, delete}
  174. else
  175. {:find_activity, _} ->
  176. {:error, :not_found}
  177. {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
  178. # We have the create activity, but not the object, it was probably pruned.
  179. # Insert a tombstone and try again
  180. with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
  181. {:ok, _tombstone} <- Object.create(tombstone_data) do
  182. delete(activity_id, user)
  183. else
  184. _ ->
  185. Logger.error(
  186. "Could not insert tombstone for missing object on deletion. Object is #{object}."
  187. )
  188. {:error, dgettext("errors", "Could not delete")}
  189. end
  190. _ ->
  191. {:error, dgettext("errors", "Could not delete")}
  192. end
  193. end
  194. @spec repeat(String.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, :not_found}
  195. def repeat(id, user, params \\ %{}) do
  196. with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
  197. object = %Object{} <- Object.normalize(activity, fetch: false),
  198. {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
  199. public = public_announce?(object, params),
  200. {:ok, announce, _} <- Builder.announce(user, object, public: public),
  201. {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
  202. {:ok, activity}
  203. else
  204. {:existing_announce, %Activity{} = announce} ->
  205. {:ok, announce}
  206. _ ->
  207. {:error, :not_found}
  208. end
  209. end
  210. @spec unrepeat(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, :not_found | String.t()}
  211. def unrepeat(id, user) do
  212. with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
  213. {:find_activity, Activity.get_by_id(id)},
  214. %Object{} = note <- Object.normalize(activity, fetch: false),
  215. %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
  216. {_, {:ok, _}} <- {:cancel_jobs, maybe_cancel_jobs(announce)},
  217. {:ok, undo, _} <- Builder.undo(user, announce),
  218. {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
  219. {:ok, activity}
  220. else
  221. {:find_activity, _} -> {:error, :not_found}
  222. _ -> {:error, dgettext("errors", "Could not unrepeat")}
  223. end
  224. end
  225. @spec favorite(String.t(), User.t()) ::
  226. {:ok, Activity.t()} | {:ok, :already_liked} | {:error, :not_found | String.t()}
  227. def favorite(id, %User{} = user) do
  228. case favorite_helper(user, id) do
  229. {:ok, _} = res ->
  230. res
  231. {:error, :not_found} = res ->
  232. res
  233. {:error, e} ->
  234. Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
  235. {:error, dgettext("errors", "Could not favorite")}
  236. end
  237. end
  238. defp favorite_helper(user, id) do
  239. with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
  240. {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
  241. {_, {:ok, %Activity{} = activity, _meta}} <-
  242. {:common_pipeline,
  243. Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
  244. {:ok, activity}
  245. else
  246. {:find_object, _} ->
  247. {:error, :not_found}
  248. {:common_pipeline, {:error, {:validate, {:error, changeset}}}} = e ->
  249. if {:object, {"already liked by this actor", []}} in changeset.errors do
  250. {:ok, :already_liked}
  251. else
  252. {:error, e}
  253. end
  254. e ->
  255. {:error, e}
  256. end
  257. end
  258. @spec unfavorite(String.t(), User.t()) ::
  259. {:ok, Activity.t()} | {:error, :not_found | String.t()}
  260. def unfavorite(id, user) do
  261. with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
  262. {:find_activity, Activity.get_by_id(id)},
  263. %Object{} = note <- Object.normalize(activity, fetch: false),
  264. %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
  265. {_, {:ok, _}} <- {:cancel_jobs, maybe_cancel_jobs(like)},
  266. {:ok, undo, _} <- Builder.undo(user, like),
  267. {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
  268. {:ok, activity}
  269. else
  270. {:find_activity, _} -> {:error, :not_found}
  271. _ -> {:error, dgettext("errors", "Could not unfavorite")}
  272. end
  273. end
  274. @spec react_with_emoji(String.t(), User.t(), String.t()) ::
  275. {:ok, Activity.t()} | {:error, String.t()}
  276. def react_with_emoji(id, user, emoji) do
  277. with %Activity{} = activity <- Activity.get_by_id(id),
  278. object <- Object.normalize(activity, fetch: false),
  279. {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
  280. {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
  281. {:ok, activity}
  282. else
  283. _ ->
  284. {:error, dgettext("errors", "Could not add reaction emoji")}
  285. end
  286. end
  287. @spec unreact_with_emoji(String.t(), User.t(), String.t()) ::
  288. {:ok, Activity.t()} | {:error, String.t()}
  289. def unreact_with_emoji(id, user, emoji) do
  290. with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
  291. {_, {:ok, _}} <- {:cancel_jobs, maybe_cancel_jobs(reaction_activity)},
  292. {:ok, undo, _} <- Builder.undo(user, reaction_activity),
  293. {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
  294. {:ok, activity}
  295. else
  296. _ ->
  297. {:error, dgettext("errors", "Could not remove reaction emoji")}
  298. end
  299. end
  300. @spec vote(Object.t(), User.t(), list()) :: {:ok, list(), Object.t()} | Pipeline.errors()
  301. def vote(%Object{data: %{"type" => "Question"}} = object, %User{} = user, choices) do
  302. with :ok <- validate_not_author(object, user),
  303. :ok <- validate_existing_votes(user, object),
  304. {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
  305. answer_activities =
  306. Enum.map(choices, fn index ->
  307. {:ok, answer_object, _meta} =
  308. Builder.answer(user, object, Enum.at(options, index)["name"])
  309. {:ok, activity_data, _meta} = Builder.create(user, answer_object, [])
  310. {:ok, activity, _meta} =
  311. activity_data
  312. |> Map.put("cc", answer_object["cc"])
  313. |> Map.put("context", answer_object["context"])
  314. |> Pipeline.common_pipeline(local: true)
  315. # TODO: Do preload of Pleroma.Object in Pipeline
  316. Activity.normalize(activity.data)
  317. end)
  318. object = Object.get_cached_by_ap_id(object.data["id"])
  319. {:ok, answer_activities, object}
  320. end
  321. end
  322. defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
  323. do: {:error, dgettext("errors", "Poll's author can't vote")}
  324. defp validate_not_author(_, _), do: :ok
  325. defp validate_existing_votes(%{ap_id: ap_id}, object) do
  326. if Utils.get_existing_votes(ap_id, object) == [] do
  327. :ok
  328. else
  329. {:error, dgettext("errors", "Already voted")}
  330. end
  331. end
  332. defp get_options_and_max_count(%{data: %{"anyOf" => any_of}})
  333. when is_list(any_of) and any_of != [],
  334. do: {any_of, Enum.count(any_of)}
  335. defp get_options_and_max_count(%{data: %{"oneOf" => one_of}})
  336. when is_list(one_of) and one_of != [],
  337. do: {one_of, 1}
  338. defp normalize_and_validate_choices(choices, object) do
  339. choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
  340. {options, max_count} = get_options_and_max_count(object)
  341. count = Enum.count(options)
  342. with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
  343. {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
  344. {:ok, options, choices}
  345. else
  346. {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
  347. {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
  348. end
  349. end
  350. defp public_announce?(_, %{visibility: visibility})
  351. when visibility in ~w{public unlisted private direct},
  352. do: visibility in ~w(public unlisted)
  353. defp public_announce?(object, _) do
  354. Visibility.public?(object)
  355. end
  356. @spec get_visibility(map(), map() | nil, Participation.t() | nil) ::
  357. {String.t() | nil, String.t() | nil}
  358. def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
  359. def get_visibility(%{visibility: visibility}, in_reply_to, _)
  360. when visibility in ~w{public local unlisted private direct},
  361. do: {visibility, get_replied_to_visibility(in_reply_to)}
  362. def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
  363. visibility = {:list, String.to_integer(list_id)}
  364. {visibility, get_replied_to_visibility(in_reply_to)}
  365. end
  366. def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
  367. visibility = get_replied_to_visibility(in_reply_to)
  368. {visibility, visibility}
  369. end
  370. def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
  371. @spec get_replied_to_visibility(Activity.t() | nil) :: String.t() | nil
  372. def get_replied_to_visibility(nil), do: nil
  373. def get_replied_to_visibility(activity) do
  374. with %Object{} = object <- Object.normalize(activity, fetch: false) do
  375. Visibility.get_visibility(object)
  376. end
  377. end
  378. @spec check_expiry_date({:ok, nil | integer()} | String.t()) ::
  379. {:ok, boolean() | nil} | {:error, String.t()}
  380. def check_expiry_date({:ok, nil} = res), do: res
  381. def check_expiry_date({:ok, in_seconds}) do
  382. expiry = DateTime.add(DateTime.utc_now(), in_seconds)
  383. if Pleroma.Workers.PurgeExpiredActivity.expires_late_enough?(expiry) do
  384. {:ok, expiry}
  385. else
  386. {:error, "Expiry date is too soon"}
  387. end
  388. end
  389. def check_expiry_date(expiry_str) do
  390. Ecto.Type.cast(:integer, expiry_str)
  391. |> check_expiry_date()
  392. end
  393. @spec listen(User.t(), map()) :: {:ok, Activity.t()} | {:error, any()}
  394. def listen(user, data) do
  395. with {:ok, draft} <- ActivityDraft.listen(user, data) do
  396. ActivityPub.listen(draft.changes)
  397. end
  398. end
  399. @spec post(User.t(), map()) :: {:ok, Activity.t()} | {:error, any()}
  400. def post(user, %{status: _} = data) do
  401. with {:ok, draft} <- ActivityDraft.create(user, data) do
  402. ActivityPub.create(draft.changes, draft.preview?)
  403. end
  404. end
  405. @spec update(Activity.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, nil}
  406. def update(orig_activity, %User{} = user, changes) do
  407. with orig_object <- Object.normalize(orig_activity),
  408. {:ok, new_object} <- make_update_data(user, orig_object, changes),
  409. {:ok, update_data, _} <- Builder.update(user, new_object),
  410. {:ok, update, _} <- Pipeline.common_pipeline(update_data, local: true) do
  411. {:ok, update}
  412. else
  413. _ -> {:error, nil}
  414. end
  415. end
  416. defp make_update_data(user, orig_object, changes) do
  417. kept_params = %{
  418. visibility: Visibility.get_visibility(orig_object),
  419. in_reply_to_id:
  420. with replied_id when is_binary(replied_id) <- orig_object.data["inReplyTo"],
  421. %Activity{id: activity_id} <- Activity.get_create_by_object_ap_id(replied_id) do
  422. activity_id
  423. else
  424. _ -> nil
  425. end
  426. }
  427. params = Map.merge(changes, kept_params)
  428. with {:ok, draft} <- ActivityDraft.create(user, params) do
  429. change =
  430. Object.Updater.make_update_object_data(orig_object.data, draft.object, Utils.make_date())
  431. {:ok, change}
  432. else
  433. _ -> {:error, nil}
  434. end
  435. end
  436. @spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors()
  437. def pin(id, %User{} = user) do
  438. with %Activity{} = activity <- create_activity_by_id(id),
  439. true <- activity_belongs_to_actor(activity, user.ap_id),
  440. true <- object_type_is_allowed_for_pin(activity.object),
  441. true <- activity_is_public(activity),
  442. {:ok, pin_data, _} <- Builder.pin(user, activity.object),
  443. {:ok, _pin, _} <-
  444. Pipeline.common_pipeline(pin_data,
  445. local: true,
  446. activity_id: id
  447. ) do
  448. {:ok, activity}
  449. else
  450. {:error, {:side_effects, error}} -> error
  451. error -> error
  452. end
  453. end
  454. defp create_activity_by_id(id) do
  455. with nil <- Activity.create_by_id_with_object(id) do
  456. {:error, :not_found}
  457. end
  458. end
  459. defp activity_belongs_to_actor(%{actor: actor}, actor), do: true
  460. defp activity_belongs_to_actor(_, _), do: {:error, :ownership_error}
  461. defp object_type_is_allowed_for_pin(%{data: %{"type" => type}}) do
  462. with false <- type in ["Note", "Article", "Question"] do
  463. {:error, :not_allowed}
  464. end
  465. end
  466. defp activity_is_public(activity) do
  467. with false <- Visibility.public?(activity) do
  468. {:error, :visibility_error}
  469. end
  470. end
  471. @spec unpin(String.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors()
  472. def unpin(id, user) do
  473. with %Activity{} = activity <- create_activity_by_id(id),
  474. {:ok, unpin_data, _} <- Builder.unpin(user, activity.object),
  475. {:ok, _unpin, _} <-
  476. Pipeline.common_pipeline(unpin_data,
  477. local: true,
  478. activity_id: activity.id,
  479. expires_at: activity.data["expires_at"],
  480. featured_address: user.featured_address
  481. ) do
  482. {:ok, activity}
  483. end
  484. end
  485. @spec add_mute(Activity.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, String.t()}
  486. def add_mute(activity, user, params \\ %{}) do
  487. expires_in = Map.get(params, :expires_in, 0)
  488. with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
  489. _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
  490. if expires_in > 0 do
  491. Pleroma.Workers.MuteExpireWorker.new(
  492. %{"op" => "unmute_conversation", "user_id" => user.id, "activity_id" => activity.id},
  493. schedule_in: expires_in
  494. )
  495. |> Oban.insert()
  496. end
  497. {:ok, activity}
  498. else
  499. {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
  500. end
  501. end
  502. @spec remove_mute(Activity.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()}
  503. def remove_mute(%Activity{} = activity, %User{} = user) do
  504. ThreadMute.remove_mute(user.id, activity.data["context"])
  505. {:ok, activity}
  506. end
  507. @spec remove_mute(String.t(), String.t()) :: {:ok, Activity.t()} | {:error, any()}
  508. def remove_mute(activity_id, user_id) do
  509. with {:user, %User{} = user} <- {:user, User.get_by_id(user_id)},
  510. {:activity, %Activity{} = activity} <- {:activity, Activity.get_by_id(activity_id)} do
  511. remove_mute(activity, user)
  512. else
  513. {what, result} = error ->
  514. Logger.warning(
  515. "CommonAPI.remove_mute/2 failed. #{what}: #{result}, user_id: #{user_id}, activity_id: #{activity_id}"
  516. )
  517. {:error, error}
  518. end
  519. end
  520. @spec thread_muted?(Activity.t(), User.t()) :: boolean()
  521. def thread_muted?(%{data: %{"context" => context}}, %User{id: user_id})
  522. when is_binary(context) do
  523. ThreadMute.exists?(user_id, context)
  524. end
  525. def thread_muted?(_, _), do: false
  526. @spec report(User.t(), map()) :: {:ok, Activity.t()} | {:error, any()}
  527. def report(user, data) do
  528. with {:ok, account} <- get_reported_account(data.account_id),
  529. {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
  530. {:ok, statuses} <- get_report_statuses(account, data),
  531. rules <- get_report_rules(Map.get(data, :rule_ids, nil)) do
  532. ActivityPub.flag(%{
  533. context: Utils.generate_context_id(),
  534. actor: user,
  535. account: account,
  536. statuses: statuses,
  537. content: content_html,
  538. forward: Map.get(data, :forward, false),
  539. rules: rules
  540. })
  541. end
  542. end
  543. defp get_reported_account(account_id) do
  544. case User.get_cached_by_id(account_id) do
  545. %User{} = account -> {:ok, account}
  546. _ -> {:error, dgettext("errors", "Account not found")}
  547. end
  548. end
  549. defp get_report_rules(nil) do
  550. nil
  551. end
  552. defp get_report_rules(rule_ids) do
  553. rule_ids
  554. |> Enum.filter(&Rule.exists?/1)
  555. end
  556. @spec update_report_state(String.t() | [String.t()], String.t()) ::
  557. {:ok, any()} | {:error, any()}
  558. def update_report_state(activity_ids, state) when is_list(activity_ids) do
  559. case Utils.update_report_state(activity_ids, state) do
  560. :ok -> {:ok, activity_ids}
  561. _ -> {:error, dgettext("errors", "Could not update state")}
  562. end
  563. end
  564. def update_report_state(activity_id, state) do
  565. with %Activity{} = activity <- Activity.get_by_id(activity_id, filter: []) do
  566. Utils.update_report_state(activity, state)
  567. else
  568. nil -> {:error, :not_found}
  569. end
  570. end
  571. @spec update_activity_scope(String.t(), map()) :: {:ok, any()} | {:error, any()}
  572. def update_activity_scope(activity_id, opts \\ %{}) do
  573. with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
  574. {:ok, activity} <- toggle_sensitive(activity, opts) do
  575. set_visibility(activity, opts)
  576. else
  577. nil -> {:error, :not_found}
  578. end
  579. end
  580. defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
  581. toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
  582. end
  583. defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
  584. when is_boolean(sensitive) do
  585. new_data = Map.put(object.data, "sensitive", sensitive)
  586. {:ok, object} =
  587. object
  588. |> Object.change(%{data: new_data})
  589. |> Object.update_and_set_cache()
  590. {:ok, Map.put(activity, :object, object)}
  591. end
  592. defp toggle_sensitive(activity, _), do: {:ok, activity}
  593. defp set_visibility(activity, %{visibility: visibility}) do
  594. Utils.update_activity_visibility(activity, visibility)
  595. end
  596. defp set_visibility(activity, _), do: {:ok, activity}
  597. @spec hide_reblogs(User.t(), User.t()) :: {:ok, any()} | {:error, any()}
  598. def hide_reblogs(%User{} = target, %User{} = user) do
  599. UserRelationship.create_reblog_mute(user, target)
  600. end
  601. @spec show_reblogs(User.t(), User.t()) :: {:ok, any()} | {:error, any()}
  602. def show_reblogs(%User{} = target, %User{} = user) do
  603. UserRelationship.delete_reblog_mute(user, target)
  604. end
  605. @spec get_user(String.t(), boolean()) :: User.t() | nil
  606. def get_user(ap_id, fake_record_fallback \\ true) do
  607. cond do
  608. user = User.get_cached_by_ap_id(ap_id) ->
  609. user
  610. user = User.get_by_guessed_nickname(ap_id) ->
  611. user
  612. fake_record_fallback ->
  613. # TODO: refactor (fake records is never a good idea)
  614. User.error_user(ap_id)
  615. true ->
  616. nil
  617. end
  618. end
  619. defp maybe_cancel_jobs(%Activity{id: activity_id}) do
  620. Oban.Job
  621. |> where([j], j.worker == "Pleroma.Workers.PublisherWorker")
  622. |> where([j], j.args["op"] == "publish_one")
  623. |> where([j], j.args["params"]["activity_id"] == ^activity_id)
  624. |> Oban.cancel_all_jobs()
  625. end
  626. defp maybe_cancel_jobs(_), do: {:ok, 0}
  627. end