logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma

common_api.ex (19283B)


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