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
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
30 Repo.transaction(fn ->
33 String.length(content) <= Pleroma.Config.get([:instance, :chat_limit])},
34 {_, {:ok, chat_message_data, _meta}} <-
39 content |> Formatter.html_escape("text/plain")
41 {_, {:ok, create_activity_data, _meta}} <-
42 {:build_create_activity,
43 Builder.create(user, chat_message_data["id"], [recipient.ap_id])},
44 {_, {:ok, %Activity{} = activity, _meta}} <-
46 Pipeline.common_pipeline(create_activity_data,
48 object_data: chat_message_data
52 {:content_length, false} -> {:error, :content_too_long}
63 def follow(follower, followed) do
64 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
66 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
67 {:ok, activity} <- ActivityPub.follow(follower, followed),
68 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
69 {:ok, follower, followed, activity}
73 def unfollow(follower, unfollowed) do
74 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
75 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
76 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
81 def accept_follow_request(follower, followed) do
82 with {:ok, follower} <- User.follow(follower, followed),
83 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
84 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
85 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
90 object: follow_activity.data["id"],
97 def reject_follow_request(follower, followed) do
98 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
99 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
100 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
102 ActivityPub.reject(%{
103 to: [follower.ap_id],
105 object: follow_activity.data["id"],
112 def delete(activity_id, user) do
113 with {_, %Activity{data: %{"object" => _}} = activity} <-
114 {:find_activity, Activity.get_by_id_with_object(activity_id)},
115 %Object{} = object <- Object.normalize(activity),
116 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
117 {:ok, _} <- unpin(activity_id, user),
118 {:ok, delete} <- ActivityPub.delete(object) do
121 {:find_activity, _} -> {:error, :not_found}
122 _ -> {:error, dgettext("errors", "Could not delete")}
126 def repeat(id, user, params \\ %{}) do
127 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
128 {:find_activity, Activity.get_by_id(id)},
129 object <- Object.normalize(activity),
130 announce_activity <- Utils.get_existing_announce(user.ap_id, object),
131 public <- public_announce?(object, params) do
132 if announce_activity do
133 {:ok, announce_activity, object}
135 ActivityPub.announce(user, object, nil, true, public)
138 {:find_activity, _} -> {:error, :not_found}
139 _ -> {:error, dgettext("errors", "Could not repeat")}
143 def unrepeat(id, user) do
144 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
145 {:find_activity, Activity.get_by_id(id)} do
146 object = Object.normalize(activity)
147 ActivityPub.unannounce(user, object)
149 {:find_activity, _} -> {:error, :not_found}
150 _ -> {:error, dgettext("errors", "Could not unrepeat")}
154 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
155 def favorite(%User{} = user, id) do
156 case favorite_helper(user, id) do
160 {:error, :not_found} = res ->
164 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
165 {:error, dgettext("errors", "Could not favorite")}
169 def favorite_helper(user, id) do
170 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
171 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
172 {_, {:ok, %Activity{} = activity, _meta}} <-
174 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
191 if {:object, {"already liked by this actor", []}} in changeset.errors do
192 {:ok, :already_liked}
202 def unfavorite(id, user) do
203 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
204 {:find_activity, Activity.get_by_id(id)} do
205 object = Object.normalize(activity)
206 ActivityPub.unlike(user, object)
208 {:find_activity, _} -> {:error, :not_found}
209 _ -> {:error, dgettext("errors", "Could not unfavorite")}
213 def react_with_emoji(id, user, emoji) do
214 with %Activity{} = activity <- Activity.get_by_id(id),
215 object <- Object.normalize(activity) do
216 ActivityPub.react_with_emoji(user, object, emoji)
219 {:error, dgettext("errors", "Could not add reaction emoji")}
223 def unreact_with_emoji(id, user, emoji) do
224 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do
225 ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
228 {:error, dgettext("errors", "Could not remove reaction emoji")}
232 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
233 with :ok <- validate_not_author(object, user),
234 :ok <- validate_existing_votes(user, object),
235 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
237 Enum.map(choices, fn index ->
238 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
241 ActivityPub.create(%{
242 to: answer_data["to"],
244 context: object.data["context"],
246 additional: %{"cc" => answer_data["cc"]}
252 object = Object.get_cached_by_ap_id(object.data["id"])
253 {:ok, answer_activities, object}
257 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
258 do: {:error, dgettext("errors", "Poll's author can't vote")}
260 defp validate_not_author(_, _), do: :ok
262 defp validate_existing_votes(%{ap_id: ap_id}, object) do
263 if Utils.get_existing_votes(ap_id, object) == [] do
266 {:error, dgettext("errors", "Already voted")}
270 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
271 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
273 defp normalize_and_validate_choices(choices, object) do
274 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
275 {options, max_count} = get_options_and_max_count(object)
276 count = Enum.count(options)
278 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
279 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
280 {:ok, options, choices}
282 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
283 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
287 def public_announce?(_, %{"visibility" => visibility})
288 when visibility in ~w{public unlisted private direct},
289 do: visibility in ~w(public unlisted)
291 def public_announce?(object, _) do
292 Visibility.is_public?(object)
295 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
297 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
298 when visibility in ~w{public unlisted private direct},
299 do: {visibility, get_replied_to_visibility(in_reply_to)}
301 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
302 visibility = {:list, String.to_integer(list_id)}
303 {visibility, get_replied_to_visibility(in_reply_to)}
306 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
307 visibility = get_replied_to_visibility(in_reply_to)
308 {visibility, visibility}
311 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
313 def get_replied_to_visibility(nil), do: nil
315 def get_replied_to_visibility(activity) do
316 with %Object{} = object <- Object.normalize(activity) do
317 Visibility.get_visibility(object)
321 def check_expiry_date({:ok, nil} = res), do: res
323 def check_expiry_date({:ok, in_seconds}) do
324 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
326 if ActivityExpiration.expires_late_enough?(expiry) do
329 {:error, "Expiry date is too soon"}
333 def check_expiry_date(expiry_str) do
334 Ecto.Type.cast(:integer, expiry_str)
335 |> check_expiry_date()
338 def listen(user, %{"title" => _} = data) do
339 with visibility <- data["visibility"] || "public",
340 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
342 Map.take(data, ["album", "artist", "title", "length"])
343 |> Map.put("type", "Audio")
346 |> Map.put("actor", user.ap_id),
348 ActivityPub.listen(%{
352 context: Utils.generate_context_id(),
353 additional: %{"cc" => cc}
359 def post(user, %{"status" => _} = data) do
360 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
362 |> ActivityPub.create(draft.preview?)
363 |> maybe_create_activity_expiration(draft.expires_at)
367 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
368 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
373 defp maybe_create_activity_expiration(result, _), do: result
375 def pin(id, %{ap_id: user_ap_id} = user) do
378 data: %{"type" => "Create"},
379 object: %Object{data: %{"type" => object_type}}
380 } = activity <- Activity.get_by_id_with_object(id),
381 true <- object_type in ["Note", "Article", "Question"],
382 true <- Visibility.is_public?(activity),
383 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
386 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
387 _ -> {:error, dgettext("errors", "Could not pin")}
391 def unpin(id, user) do
392 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
393 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
396 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
397 _ -> {:error, dgettext("errors", "Could not unpin")}
401 def add_mute(user, activity) do
402 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
405 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
409 def remove_mute(user, activity) do
410 ThreadMute.remove_mute(user.id, activity.data["context"])
414 def thread_muted?(%{id: nil} = _user, _activity), do: false
416 def thread_muted?(user, activity) do
417 ThreadMute.exists?(user.id, activity.data["context"])
420 def report(user, %{"account_id" => account_id} = data) do
421 with {:ok, account} <- get_reported_account(account_id),
422 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
423 {:ok, statuses} <- get_report_statuses(account, data) do
425 context: Utils.generate_context_id(),
429 content: content_html,
430 forward: data["forward"] || false
435 def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
437 defp get_reported_account(account_id) do
438 case User.get_cached_by_id(account_id) do
439 %User{} = account -> {:ok, account}
440 _ -> {:error, dgettext("errors", "Account not found")}
444 def update_report_state(activity_ids, state) when is_list(activity_ids) do
445 case Utils.update_report_state(activity_ids, state) do
446 :ok -> {:ok, activity_ids}
447 _ -> {:error, dgettext("errors", "Could not update state")}
451 def update_report_state(activity_id, state) do
452 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
453 Utils.update_report_state(activity, state)
455 nil -> {:error, :not_found}
456 _ -> {:error, dgettext("errors", "Could not update state")}
460 def update_activity_scope(activity_id, opts \\ %{}) do
461 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
462 {:ok, activity} <- toggle_sensitive(activity, opts) do
463 set_visibility(activity, opts)
465 nil -> {:error, :not_found}
466 {:error, reason} -> {:error, reason}
470 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
471 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
474 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
475 when is_boolean(sensitive) do
476 new_data = Map.put(object.data, "sensitive", sensitive)
480 |> Object.change(%{data: new_data})
481 |> Object.update_and_set_cache()
483 {:ok, Map.put(activity, :object, object)}
486 defp toggle_sensitive(activity, _), do: {:ok, activity}
488 defp set_visibility(activity, %{"visibility" => visibility}) do
489 Utils.update_activity_visibility(activity, visibility)
492 defp set_visibility(activity, _), do: {:ok, activity}
494 def hide_reblogs(%User{} = user, %User{} = target) do
495 UserRelationship.create_reblog_mute(user, target)
498 def show_reblogs(%User{} = user, %User{} = target) do
499 UserRelationship.delete_reblog_mute(user, target)