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
31 String.length(content) <= Pleroma.Config.get([:instance, :chat_limit])},
32 {_, {:ok, chat_message_data, _meta}} <-
37 content |> Formatter.html_escape("text/plain")
39 {_, {:ok, create_activity_data, _meta}} <-
40 {:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])},
41 {_, {:ok, %Activity{} = activity, _meta}} <-
43 Pipeline.common_pipeline(create_activity_data,
48 {:content_length, false} -> {:error, :content_too_long}
53 def follow(follower, followed) do
54 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
56 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
57 {:ok, activity} <- ActivityPub.follow(follower, followed),
58 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
59 {:ok, follower, followed, activity}
63 def unfollow(follower, unfollowed) do
64 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
65 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
66 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
71 def accept_follow_request(follower, followed) do
72 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
73 {:ok, follower} <- User.follow(follower, followed),
74 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
75 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
80 object: follow_activity.data["id"],
87 def reject_follow_request(follower, followed) do
88 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
89 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
90 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
91 {:ok, _notifications} <- Notification.dismiss(follow_activity),
96 object: follow_activity.data["id"],
103 def delete(activity_id, user) do
104 with {_, %Activity{data: %{"object" => _}} = activity} <-
105 {:find_activity, Activity.get_by_id_with_object(activity_id)},
106 %Object{} = object <- Object.normalize(activity),
107 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
108 {:ok, _} <- unpin(activity_id, user),
109 {:ok, delete} <- ActivityPub.delete(object) do
112 {:find_activity, _} -> {:error, :not_found}
113 _ -> {:error, dgettext("errors", "Could not delete")}
117 def repeat(id, user, params \\ %{}) do
118 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
119 {:find_activity, Activity.get_by_id(id)},
120 object <- Object.normalize(activity),
121 announce_activity <- Utils.get_existing_announce(user.ap_id, object),
122 public <- public_announce?(object, params) do
123 if announce_activity do
124 {:ok, announce_activity, object}
126 ActivityPub.announce(user, object, nil, true, public)
129 {:find_activity, _} -> {:error, :not_found}
130 _ -> {:error, dgettext("errors", "Could not repeat")}
134 def unrepeat(id, user) do
135 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
136 {:find_activity, Activity.get_by_id(id)} do
137 object = Object.normalize(activity)
138 ActivityPub.unannounce(user, object)
140 {:find_activity, _} -> {:error, :not_found}
141 _ -> {:error, dgettext("errors", "Could not unrepeat")}
145 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
146 def favorite(%User{} = user, id) do
147 case favorite_helper(user, id) do
151 {:error, :not_found} = res ->
155 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
156 {:error, dgettext("errors", "Could not favorite")}
160 def favorite_helper(user, id) do
161 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
162 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
163 {_, {:ok, %Activity{} = activity, _meta}} <-
165 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
182 if {:object, {"already liked by this actor", []}} in changeset.errors do
183 {:ok, :already_liked}
193 def unfavorite(id, user) do
194 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
195 {:find_activity, Activity.get_by_id(id)} do
196 object = Object.normalize(activity)
197 ActivityPub.unlike(user, object)
199 {:find_activity, _} -> {:error, :not_found}
200 _ -> {:error, dgettext("errors", "Could not unfavorite")}
204 def react_with_emoji(id, user, emoji) do
205 with %Activity{} = activity <- Activity.get_by_id(id),
206 object <- Object.normalize(activity) do
207 ActivityPub.react_with_emoji(user, object, emoji)
210 {:error, dgettext("errors", "Could not add reaction emoji")}
214 def unreact_with_emoji(id, user, emoji) do
215 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do
216 ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
219 {:error, dgettext("errors", "Could not remove reaction emoji")}
223 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
224 with :ok <- validate_not_author(object, user),
225 :ok <- validate_existing_votes(user, object),
226 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
228 Enum.map(choices, fn index ->
229 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
232 ActivityPub.create(%{
233 to: answer_data["to"],
235 context: object.data["context"],
237 additional: %{"cc" => answer_data["cc"]}
243 object = Object.get_cached_by_ap_id(object.data["id"])
244 {:ok, answer_activities, object}
248 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
249 do: {:error, dgettext("errors", "Poll's author can't vote")}
251 defp validate_not_author(_, _), do: :ok
253 defp validate_existing_votes(%{ap_id: ap_id}, object) do
254 if Utils.get_existing_votes(ap_id, object) == [] do
257 {:error, dgettext("errors", "Already voted")}
261 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
262 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
264 defp normalize_and_validate_choices(choices, object) do
265 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
266 {options, max_count} = get_options_and_max_count(object)
267 count = Enum.count(options)
269 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
270 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
271 {:ok, options, choices}
273 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
274 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
278 def public_announce?(_, %{"visibility" => visibility})
279 when visibility in ~w{public unlisted private direct},
280 do: visibility in ~w(public unlisted)
282 def public_announce?(object, _) do
283 Visibility.is_public?(object)
286 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
288 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
289 when visibility in ~w{public unlisted private direct},
290 do: {visibility, get_replied_to_visibility(in_reply_to)}
292 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
293 visibility = {:list, String.to_integer(list_id)}
294 {visibility, get_replied_to_visibility(in_reply_to)}
297 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
298 visibility = get_replied_to_visibility(in_reply_to)
299 {visibility, visibility}
302 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
304 def get_replied_to_visibility(nil), do: nil
306 def get_replied_to_visibility(activity) do
307 with %Object{} = object <- Object.normalize(activity) do
308 Visibility.get_visibility(object)
312 def check_expiry_date({:ok, nil} = res), do: res
314 def check_expiry_date({:ok, in_seconds}) do
315 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
317 if ActivityExpiration.expires_late_enough?(expiry) do
320 {:error, "Expiry date is too soon"}
324 def check_expiry_date(expiry_str) do
325 Ecto.Type.cast(:integer, expiry_str)
326 |> check_expiry_date()
329 def listen(user, %{"title" => _} = data) do
330 with visibility <- data["visibility"] || "public",
331 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
333 Map.take(data, ["album", "artist", "title", "length"])
334 |> Map.put("type", "Audio")
337 |> Map.put("actor", user.ap_id),
339 ActivityPub.listen(%{
343 context: Utils.generate_context_id(),
344 additional: %{"cc" => cc}
350 def post(user, %{"status" => _} = data) do
351 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
353 |> ActivityPub.create(draft.preview?)
354 |> maybe_create_activity_expiration(draft.expires_at)
358 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
359 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
364 defp maybe_create_activity_expiration(result, _), do: result
366 def pin(id, %{ap_id: user_ap_id} = user) do
369 data: %{"type" => "Create"},
370 object: %Object{data: %{"type" => object_type}}
371 } = activity <- Activity.get_by_id_with_object(id),
372 true <- object_type in ["Note", "Article", "Question"],
373 true <- Visibility.is_public?(activity),
374 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
377 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
378 _ -> {:error, dgettext("errors", "Could not pin")}
382 def unpin(id, user) do
383 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
384 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
387 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
388 _ -> {:error, dgettext("errors", "Could not unpin")}
392 def add_mute(user, activity) do
393 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
396 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
400 def remove_mute(user, activity) do
401 ThreadMute.remove_mute(user.id, activity.data["context"])
405 def thread_muted?(%{id: nil} = _user, _activity), do: false
407 def thread_muted?(user, activity) do
408 ThreadMute.exists?(user.id, activity.data["context"])
411 def report(user, data) do
412 with {:ok, account} <- get_reported_account(data.account_id),
413 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
414 {:ok, statuses} <- get_report_statuses(account, data) do
416 context: Utils.generate_context_id(),
420 content: content_html,
421 forward: Map.get(data, :forward, false)
426 defp get_reported_account(account_id) do
427 case User.get_cached_by_id(account_id) do
428 %User{} = account -> {:ok, account}
429 _ -> {:error, dgettext("errors", "Account not found")}
433 def update_report_state(activity_ids, state) when is_list(activity_ids) do
434 case Utils.update_report_state(activity_ids, state) do
435 :ok -> {:ok, activity_ids}
436 _ -> {:error, dgettext("errors", "Could not update state")}
440 def update_report_state(activity_id, state) do
441 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
442 Utils.update_report_state(activity, state)
444 nil -> {:error, :not_found}
445 _ -> {:error, dgettext("errors", "Could not update state")}
449 def update_activity_scope(activity_id, opts \\ %{}) do
450 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
451 {:ok, activity} <- toggle_sensitive(activity, opts) do
452 set_visibility(activity, opts)
454 nil -> {:error, :not_found}
455 {:error, reason} -> {:error, reason}
459 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
460 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
463 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
464 when is_boolean(sensitive) do
465 new_data = Map.put(object.data, "sensitive", sensitive)
469 |> Object.change(%{data: new_data})
470 |> Object.update_and_set_cache()
472 {:ok, Map.put(activity, :object, object)}
475 defp toggle_sensitive(activity, _), do: {:ok, activity}
477 defp set_visibility(activity, %{"visibility" => visibility}) do
478 Utils.update_activity_visibility(activity, visibility)
481 defp set_visibility(activity, _), do: {:ok, activity}
483 def hide_reblogs(%User{} = user, %User{} = target) do
484 UserRelationship.create_reblog_mute(user, target)
487 def show_reblogs(%User{} = user, %User{} = target) do
488 UserRelationship.delete_reblog_mute(user, target)