Fork of Pleroma with site-specific changes and feature branches https://git.pleroma.social/pleroma/pleroma
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

603 lines
20KB

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