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.Formatter
11 alias Pleroma.Notification
13 alias Pleroma.ThreadMute
15 alias Pleroma.UserRelationship
16 alias Pleroma.Web.ActivityPub.ActivityPub
17 alias Pleroma.Web.ActivityPub.Builder
18 alias Pleroma.Web.ActivityPub.Pipeline
19 alias Pleroma.Web.ActivityPub.Utils
20 alias Pleroma.Web.ActivityPub.Visibility
22 import Pleroma.Web.Gettext
23 import Pleroma.Web.CommonAPI.Utils
25 require Pleroma.Constants
28 def post_chat_message(%User{} = user, %User{} = recipient, content) do
29 with :ok <- validate_chat_content_length(content),
30 {_, {:ok, chat_message_data, _meta}} <-
35 content |> Formatter.html_escape("text/plain")
37 {_, {:ok, create_activity_data, _meta}} <-
38 {:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])},
39 {_, {:ok, %Activity{} = activity, _meta}} <-
41 Pipeline.common_pipeline(create_activity_data,
48 defp validate_chat_content_length(content) do
49 if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do
52 {:error, :content_too_long}
56 def follow(follower, followed) do
57 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
59 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
60 {:ok, activity} <- ActivityPub.follow(follower, followed),
61 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
62 {:ok, follower, followed, activity}
66 def unfollow(follower, unfollowed) do
67 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
68 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
69 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
74 def accept_follow_request(follower, followed) do
75 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
76 {:ok, follower} <- User.follow(follower, followed),
77 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
78 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
83 object: follow_activity.data["id"],
90 def reject_follow_request(follower, followed) do
91 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
92 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
93 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
94 {:ok, _notifications} <- Notification.dismiss(follow_activity),
99 object: follow_activity.data["id"],
106 def delete(activity_id, user) do
107 with {_, %Activity{data: %{"object" => _}} = activity} <-
108 {:find_activity, Activity.get_by_id_with_object(activity_id)},
109 %Object{} = object <- Object.normalize(activity),
110 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
111 {:ok, _} <- unpin(activity_id, user),
112 {:ok, delete} <- ActivityPub.delete(object) do
115 {:find_activity, _} -> {:error, :not_found}
116 _ -> {:error, dgettext("errors", "Could not delete")}
120 def repeat(id, user, params \\ %{}) do
121 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
122 {:find_activity, Activity.get_by_id(id)},
123 object <- Object.normalize(activity),
124 announce_activity <- Utils.get_existing_announce(user.ap_id, object),
125 public <- public_announce?(object, params) do
126 if announce_activity do
127 {:ok, announce_activity, object}
129 ActivityPub.announce(user, object, nil, true, public)
132 {:find_activity, _} -> {:error, :not_found}
133 _ -> {:error, dgettext("errors", "Could not repeat")}
137 def unrepeat(id, user) do
138 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
139 {:find_activity, Activity.get_by_id(id)} do
140 object = Object.normalize(activity)
141 ActivityPub.unannounce(user, object)
143 {:find_activity, _} -> {:error, :not_found}
144 _ -> {:error, dgettext("errors", "Could not unrepeat")}
148 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
149 def favorite(%User{} = user, id) do
150 case favorite_helper(user, id) do
154 {:error, :not_found} = res ->
158 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
159 {:error, dgettext("errors", "Could not favorite")}
163 def favorite_helper(user, id) do
164 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
165 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
166 {_, {:ok, %Activity{} = activity, _meta}} <-
168 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
185 if {:object, {"already liked by this actor", []}} in changeset.errors do
186 {:ok, :already_liked}
196 def unfavorite(id, user) do
197 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
198 {:find_activity, Activity.get_by_id(id)} do
199 object = Object.normalize(activity)
200 ActivityPub.unlike(user, object)
202 {:find_activity, _} -> {:error, :not_found}
203 _ -> {:error, dgettext("errors", "Could not unfavorite")}
207 def react_with_emoji(id, user, emoji) do
208 with %Activity{} = activity <- Activity.get_by_id(id),
209 object <- Object.normalize(activity) do
210 ActivityPub.react_with_emoji(user, object, emoji)
213 {:error, dgettext("errors", "Could not add reaction emoji")}
217 def unreact_with_emoji(id, user, emoji) do
218 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do
219 ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
222 {:error, dgettext("errors", "Could not remove reaction emoji")}
226 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
227 with :ok <- validate_not_author(object, user),
228 :ok <- validate_existing_votes(user, object),
229 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
231 Enum.map(choices, fn index ->
232 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
235 ActivityPub.create(%{
236 to: answer_data["to"],
238 context: object.data["context"],
240 additional: %{"cc" => answer_data["cc"]}
246 object = Object.get_cached_by_ap_id(object.data["id"])
247 {:ok, answer_activities, object}
251 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
252 do: {:error, dgettext("errors", "Poll's author can't vote")}
254 defp validate_not_author(_, _), do: :ok
256 defp validate_existing_votes(%{ap_id: ap_id}, object) do
257 if Utils.get_existing_votes(ap_id, object) == [] do
260 {:error, dgettext("errors", "Already voted")}
264 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
265 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
267 defp normalize_and_validate_choices(choices, object) do
268 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
269 {options, max_count} = get_options_and_max_count(object)
270 count = Enum.count(options)
272 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
273 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
274 {:ok, options, choices}
276 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
277 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
281 def public_announce?(_, %{"visibility" => visibility})
282 when visibility in ~w{public unlisted private direct},
283 do: visibility in ~w(public unlisted)
285 def public_announce?(object, _) do
286 Visibility.is_public?(object)
289 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
291 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
292 when visibility in ~w{public unlisted private direct},
293 do: {visibility, get_replied_to_visibility(in_reply_to)}
295 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
296 visibility = {:list, String.to_integer(list_id)}
297 {visibility, get_replied_to_visibility(in_reply_to)}
300 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
301 visibility = get_replied_to_visibility(in_reply_to)
302 {visibility, visibility}
305 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
307 def get_replied_to_visibility(nil), do: nil
309 def get_replied_to_visibility(activity) do
310 with %Object{} = object <- Object.normalize(activity) do
311 Visibility.get_visibility(object)
315 def check_expiry_date({:ok, nil} = res), do: res
317 def check_expiry_date({:ok, in_seconds}) do
318 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
320 if ActivityExpiration.expires_late_enough?(expiry) do
323 {:error, "Expiry date is too soon"}
327 def check_expiry_date(expiry_str) do
328 Ecto.Type.cast(:integer, expiry_str)
329 |> check_expiry_date()
332 def listen(user, %{"title" => _} = data) do
333 with visibility <- data["visibility"] || "public",
334 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
336 Map.take(data, ["album", "artist", "title", "length"])
337 |> Map.put("type", "Audio")
340 |> Map.put("actor", user.ap_id),
342 ActivityPub.listen(%{
346 context: Utils.generate_context_id(),
347 additional: %{"cc" => cc}
353 def post(user, %{"status" => _} = data) do
354 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
356 |> ActivityPub.create(draft.preview?)
357 |> maybe_create_activity_expiration(draft.expires_at)
361 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
362 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
367 defp maybe_create_activity_expiration(result, _), do: result
369 def pin(id, %{ap_id: user_ap_id} = user) do
372 data: %{"type" => "Create"},
373 object: %Object{data: %{"type" => object_type}}
374 } = activity <- Activity.get_by_id_with_object(id),
375 true <- object_type in ["Note", "Article", "Question"],
376 true <- Visibility.is_public?(activity),
377 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
380 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
381 _ -> {:error, dgettext("errors", "Could not pin")}
385 def unpin(id, user) do
386 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
387 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
390 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
391 _ -> {:error, dgettext("errors", "Could not unpin")}
395 def add_mute(user, activity) do
396 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
399 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
403 def remove_mute(user, activity) do
404 ThreadMute.remove_mute(user.id, activity.data["context"])
408 def thread_muted?(%{id: nil} = _user, _activity), do: false
410 def thread_muted?(user, activity) do
411 ThreadMute.exists?(user.id, activity.data["context"])
414 def report(user, data) do
415 with {:ok, account} <- get_reported_account(data.account_id),
416 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
417 {:ok, statuses} <- get_report_statuses(account, data) do
419 context: Utils.generate_context_id(),
423 content: content_html,
424 forward: Map.get(data, :forward, false)
429 defp get_reported_account(account_id) do
430 case User.get_cached_by_id(account_id) do
431 %User{} = account -> {:ok, account}
432 _ -> {:error, dgettext("errors", "Account not found")}
436 def update_report_state(activity_ids, state) when is_list(activity_ids) do
437 case Utils.update_report_state(activity_ids, state) do
438 :ok -> {:ok, activity_ids}
439 _ -> {:error, dgettext("errors", "Could not update state")}
443 def update_report_state(activity_id, state) do
444 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
445 Utils.update_report_state(activity, state)
447 nil -> {:error, :not_found}
448 _ -> {:error, dgettext("errors", "Could not update state")}
452 def update_activity_scope(activity_id, opts \\ %{}) do
453 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
454 {:ok, activity} <- toggle_sensitive(activity, opts) do
455 set_visibility(activity, opts)
457 nil -> {:error, :not_found}
458 {:error, reason} -> {:error, reason}
462 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
463 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
466 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
467 when is_boolean(sensitive) do
468 new_data = Map.put(object.data, "sensitive", sensitive)
472 |> Object.change(%{data: new_data})
473 |> Object.update_and_set_cache()
475 {:ok, Map.put(activity, :object, object)}
478 defp toggle_sensitive(activity, _), do: {:ok, activity}
480 defp set_visibility(activity, %{"visibility" => visibility}) do
481 Utils.update_activity_visibility(activity, visibility)
484 defp set_visibility(activity, _), do: {:ok, activity}
486 def hide_reblogs(%User{} = user, %User{} = target) do
487 UserRelationship.create_reblog_mute(user, target)
490 def show_reblogs(%User{} = user, %User{} = target) do
491 UserRelationship.delete_reblog_mute(user, target)