1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.CommonAPI do
7 alias Pleroma.ActivityExpiration
8 alias Pleroma.Conversation.Participation
9 alias Pleroma.FollowingRelationship
10 alias Pleroma.Notification
12 alias Pleroma.ThreadMute
14 alias Pleroma.UserRelationship
15 alias Pleroma.Web.ActivityPub.ActivityPub
16 alias Pleroma.Web.ActivityPub.Builder
17 alias Pleroma.Web.ActivityPub.Pipeline
18 alias Pleroma.Web.ActivityPub.Utils
19 alias Pleroma.Web.ActivityPub.Visibility
21 import Pleroma.Web.Gettext
22 import Pleroma.Web.CommonAPI.Utils
24 require Pleroma.Constants
27 def unblock(blocker, blocked) do
28 with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
29 {:ok, unblock_data, _} <- Builder.undo(blocker, block),
30 {:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
33 {:fetch_block, nil} ->
34 if User.blocks?(blocker, blocked) do
35 User.unblock(blocker, blocked)
38 {:error, :not_blocking}
46 def follow(follower, followed) do
47 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
49 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
50 {:ok, activity} <- ActivityPub.follow(follower, followed),
51 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
52 {:ok, follower, followed, activity}
56 def unfollow(follower, unfollowed) do
57 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
58 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
59 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
64 def accept_follow_request(follower, followed) do
65 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
66 {:ok, follower} <- User.follow(follower, followed),
67 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
68 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
73 object: follow_activity.data["id"],
80 def reject_follow_request(follower, followed) do
81 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
82 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
83 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
84 {:ok, _notifications} <- Notification.dismiss(follow_activity),
89 object: follow_activity.data["id"],
96 def delete(activity_id, user) do
97 with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
98 {:find_activity, Activity.get_by_id(activity_id)},
99 {_, %Object{} = object, _} <-
100 {:find_object, Object.normalize(activity, false), activity},
101 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
102 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
103 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
106 {:find_activity, _} ->
109 {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
110 # We have the create activity, but not the object, it was probably pruned.
111 # Insert a tombstone and try again
112 with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
113 {:ok, _tombstone} <- Object.create(tombstone_data) do
114 delete(activity_id, user)
118 "Could not insert tombstone for missing object on deletion. Object is #{object}."
121 {:error, dgettext("errors", "Could not delete")}
125 {:error, dgettext("errors", "Could not delete")}
129 def repeat(id, user, params \\ %{}) do
130 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id) do
131 object = Object.normalize(activity)
132 announce_activity = Utils.get_existing_announce(user.ap_id, object)
133 public = public_announce?(object, params)
135 if announce_activity do
136 {:ok, announce_activity, object}
138 ActivityPub.announce(user, object, nil, true, public)
141 _ -> {:error, :not_found}
145 def unrepeat(id, user) do
146 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
147 {:find_activity, Activity.get_by_id(id)},
148 %Object{} = note <- Object.normalize(activity, false),
149 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
150 {:ok, undo, _} <- Builder.undo(user, announce),
151 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
154 {:find_activity, _} -> {:error, :not_found}
155 _ -> {:error, dgettext("errors", "Could not unrepeat")}
159 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
160 def favorite(%User{} = user, id) do
161 case favorite_helper(user, id) do
165 {:error, :not_found} = res ->
169 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
170 {:error, dgettext("errors", "Could not favorite")}
174 def favorite_helper(user, id) do
175 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
176 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
177 {_, {:ok, %Activity{} = activity, _meta}} <-
179 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
196 if {:object, {"already liked by this actor", []}} in changeset.errors do
197 {:ok, :already_liked}
207 def unfavorite(id, user) do
208 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
209 {:find_activity, Activity.get_by_id(id)},
210 %Object{} = note <- Object.normalize(activity, false),
211 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
212 {:ok, undo, _} <- Builder.undo(user, like),
213 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
216 {:find_activity, _} -> {:error, :not_found}
217 _ -> {:error, dgettext("errors", "Could not unfavorite")}
221 def react_with_emoji(id, user, emoji) do
222 with %Activity{} = activity <- Activity.get_by_id(id),
223 object <- Object.normalize(activity),
224 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
225 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
229 {:error, dgettext("errors", "Could not add reaction emoji")}
233 def unreact_with_emoji(id, user, emoji) do
234 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
235 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
236 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
240 {:error, dgettext("errors", "Could not remove reaction emoji")}
244 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
245 with :ok <- validate_not_author(object, user),
246 :ok <- validate_existing_votes(user, object),
247 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
249 Enum.map(choices, fn index ->
250 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
253 ActivityPub.create(%{
254 to: answer_data["to"],
256 context: object.data["context"],
258 additional: %{"cc" => answer_data["cc"]}
264 object = Object.get_cached_by_ap_id(object.data["id"])
265 {:ok, answer_activities, object}
269 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
270 do: {:error, dgettext("errors", "Poll's author can't vote")}
272 defp validate_not_author(_, _), do: :ok
274 defp validate_existing_votes(%{ap_id: ap_id}, object) do
275 if Utils.get_existing_votes(ap_id, object) == [] do
278 {:error, dgettext("errors", "Already voted")}
282 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
283 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
285 defp normalize_and_validate_choices(choices, object) do
286 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
287 {options, max_count} = get_options_and_max_count(object)
288 count = Enum.count(options)
290 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
291 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
292 {:ok, options, choices}
294 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
295 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
299 def public_announce?(_, %{visibility: visibility})
300 when visibility in ~w{public unlisted private direct},
301 do: visibility in ~w(public unlisted)
303 def public_announce?(object, _) do
304 Visibility.is_public?(object)
307 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
309 def get_visibility(%{visibility: visibility}, in_reply_to, _)
310 when visibility in ~w{public unlisted private direct},
311 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)}
318 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
319 visibility = get_replied_to_visibility(in_reply_to)
320 {visibility, visibility}
323 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
325 def get_replied_to_visibility(nil), do: nil
327 def get_replied_to_visibility(activity) do
328 with %Object{} = object <- Object.normalize(activity) do
329 Visibility.get_visibility(object)
333 def check_expiry_date({:ok, nil} = res), do: res
335 def check_expiry_date({:ok, in_seconds}) do
336 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
338 if ActivityExpiration.expires_late_enough?(expiry) do
341 {:error, "Expiry date is too soon"}
345 def check_expiry_date(expiry_str) do
346 Ecto.Type.cast(:integer, expiry_str)
347 |> check_expiry_date()
350 def listen(user, data) do
351 visibility = Map.get(data, :visibility, "public")
353 with {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
356 |> Map.take([:album, :artist, :title, :length])
357 |> Map.new(fn {key, value} -> {to_string(key), value} end)
358 |> Map.put("type", "Audio")
361 |> Map.put("actor", user.ap_id),
363 ActivityPub.listen(%{
367 context: Utils.generate_context_id(),
368 additional: %{"cc" => cc}
374 def post(user, %{status: _} = data) do
375 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
377 |> ActivityPub.create(draft.preview?)
378 |> maybe_create_activity_expiration(draft.expires_at)
382 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
383 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
388 defp maybe_create_activity_expiration(result, _), do: result
390 def pin(id, %{ap_id: user_ap_id} = user) do
393 data: %{"type" => "Create"},
394 object: %Object{data: %{"type" => object_type}}
395 } = activity <- Activity.get_by_id_with_object(id),
396 true <- object_type in ["Note", "Article", "Question"],
397 true <- Visibility.is_public?(activity),
398 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
401 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
402 _ -> {:error, dgettext("errors", "Could not pin")}
406 def unpin(id, user) do
407 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
408 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
411 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
412 _ -> {:error, dgettext("errors", "Could not unpin")}
416 def add_mute(user, activity) do
417 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
420 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
424 def remove_mute(user, activity) do
425 ThreadMute.remove_mute(user.id, activity.data["context"])
429 def thread_muted?(%{id: nil} = _user, _activity), do: false
431 def thread_muted?(user, activity) do
432 ThreadMute.exists?(user.id, activity.data["context"])
435 def report(user, data) do
436 with {:ok, account} <- get_reported_account(data.account_id),
437 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
438 {:ok, statuses} <- get_report_statuses(account, data) do
440 context: Utils.generate_context_id(),
444 content: content_html,
445 forward: Map.get(data, :forward, false)
450 defp get_reported_account(account_id) do
451 case User.get_cached_by_id(account_id) do
452 %User{} = account -> {:ok, account}
453 _ -> {:error, dgettext("errors", "Account not found")}
457 def update_report_state(activity_ids, state) when is_list(activity_ids) do
458 case Utils.update_report_state(activity_ids, state) do
459 :ok -> {:ok, activity_ids}
460 _ -> {:error, dgettext("errors", "Could not update state")}
464 def update_report_state(activity_id, state) do
465 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
466 Utils.update_report_state(activity, state)
468 nil -> {:error, :not_found}
469 _ -> {:error, dgettext("errors", "Could not update state")}
473 def update_activity_scope(activity_id, opts \\ %{}) do
474 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
475 {:ok, activity} <- toggle_sensitive(activity, opts) do
476 set_visibility(activity, opts)
478 nil -> {:error, :not_found}
479 {:error, reason} -> {:error, reason}
483 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
484 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
487 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
488 when is_boolean(sensitive) do
489 new_data = Map.put(object.data, "sensitive", sensitive)
493 |> Object.change(%{data: new_data})
494 |> Object.update_and_set_cache()
496 {:ok, Map.put(activity, :object, object)}
499 defp toggle_sensitive(activity, _), do: {:ok, activity}
501 defp set_visibility(activity, %{visibility: visibility}) do
502 Utils.update_activity_visibility(activity, visibility)
505 defp set_visibility(activity, _), do: {:ok, activity}
507 def hide_reblogs(%User{} = user, %User{} = target) do
508 UserRelationship.create_reblog_mute(user, target)
511 def show_reblogs(%User{} = user, %User{} = target) do
512 UserRelationship.delete_reblog_mute(user, target)