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, opts \\ []) do
29 with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
30 :ok <- validate_chat_content_length(content, !!maybe_attachment),
31 {_, {:ok, chat_message_data, _meta}} <-
36 content |> format_chat_content,
37 attachment: maybe_attachment
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,
50 defp format_chat_content(nil), do: nil
52 defp format_chat_content(content) do
53 content |> Formatter.html_escape("text/plain")
56 defp validate_chat_content_length(_, true), do: :ok
57 defp validate_chat_content_length(nil, false), do: {:error, :no_content}
59 defp validate_chat_content_length(content, _) do
60 if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do
63 {:error, :content_too_long}
67 def unblock(blocker, blocked) do
68 with %Activity{} = block <- Utils.fetch_latest_block(blocker, blocked),
69 {:ok, unblock_data, _} <- Builder.undo(blocker, block),
70 {:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
75 def follow(follower, followed) do
76 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
78 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
79 {:ok, activity} <- ActivityPub.follow(follower, followed),
80 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
81 {:ok, follower, followed, activity}
85 def unfollow(follower, unfollowed) do
86 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
87 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
88 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
93 def accept_follow_request(follower, followed) do
94 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
95 {:ok, follower} <- User.follow(follower, followed),
96 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
97 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
100 to: [follower.ap_id],
102 object: follow_activity.data["id"],
109 def reject_follow_request(follower, followed) do
110 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
111 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
112 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
113 {:ok, _notifications} <- Notification.dismiss(follow_activity),
115 ActivityPub.reject(%{
116 to: [follower.ap_id],
118 object: follow_activity.data["id"],
125 def delete(activity_id, user) do
126 with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
127 {:find_activity, Activity.get_by_id(activity_id)},
128 {_, %Object{} = object, _} <-
129 {:find_object, Object.normalize(activity, false), activity},
130 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
131 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
132 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
135 {:find_activity, _} ->
138 {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
139 # We have the create activity, but not the object, it was probably pruned.
140 # Insert a tombstone and try again
141 with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
142 {:ok, _tombstone} <- Object.create(tombstone_data) do
143 delete(activity_id, user)
147 "Could not insert tombstone for missing object on deletion. Object is #{object}."
150 {:error, dgettext("errors", "Could not delete")}
154 {:error, dgettext("errors", "Could not delete")}
158 def repeat(id, user, params \\ %{}) do
159 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id) do
160 object = Object.normalize(activity)
161 announce_activity = Utils.get_existing_announce(user.ap_id, object)
162 public = public_announce?(object, params)
164 if announce_activity do
165 {:ok, announce_activity, object}
167 ActivityPub.announce(user, object, nil, true, public)
170 _ -> {:error, :not_found}
174 def unrepeat(id, user) do
175 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
176 {:find_activity, Activity.get_by_id(id)},
177 %Object{} = note <- Object.normalize(activity, false),
178 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
179 {:ok, undo, _} <- Builder.undo(user, announce),
180 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
183 {:find_activity, _} -> {:error, :not_found}
184 _ -> {:error, dgettext("errors", "Could not unrepeat")}
188 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
189 def favorite(%User{} = user, id) do
190 case favorite_helper(user, id) do
194 {:error, :not_found} = res ->
198 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
199 {:error, dgettext("errors", "Could not favorite")}
203 def favorite_helper(user, id) do
204 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
205 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
206 {_, {:ok, %Activity{} = activity, _meta}} <-
208 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
225 if {:object, {"already liked by this actor", []}} in changeset.errors do
226 {:ok, :already_liked}
236 def unfavorite(id, user) do
237 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
238 {:find_activity, Activity.get_by_id(id)},
239 %Object{} = note <- Object.normalize(activity, false),
240 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
241 {:ok, undo, _} <- Builder.undo(user, like),
242 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
245 {:find_activity, _} -> {:error, :not_found}
246 _ -> {:error, dgettext("errors", "Could not unfavorite")}
250 def react_with_emoji(id, user, emoji) do
251 with %Activity{} = activity <- Activity.get_by_id(id),
252 object <- Object.normalize(activity),
253 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
254 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
258 {:error, dgettext("errors", "Could not add reaction emoji")}
262 def unreact_with_emoji(id, user, emoji) do
263 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
264 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
265 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
269 {:error, dgettext("errors", "Could not remove reaction emoji")}
273 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
274 with :ok <- validate_not_author(object, user),
275 :ok <- validate_existing_votes(user, object),
276 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
278 Enum.map(choices, fn index ->
279 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
282 ActivityPub.create(%{
283 to: answer_data["to"],
285 context: object.data["context"],
287 additional: %{"cc" => answer_data["cc"]}
293 object = Object.get_cached_by_ap_id(object.data["id"])
294 {:ok, answer_activities, object}
298 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
299 do: {:error, dgettext("errors", "Poll's author can't vote")}
301 defp validate_not_author(_, _), do: :ok
303 defp validate_existing_votes(%{ap_id: ap_id}, object) do
304 if Utils.get_existing_votes(ap_id, object) == [] do
307 {:error, dgettext("errors", "Already voted")}
311 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
312 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
314 defp normalize_and_validate_choices(choices, object) do
315 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
316 {options, max_count} = get_options_and_max_count(object)
317 count = Enum.count(options)
319 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
320 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
321 {:ok, options, choices}
323 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
324 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
328 def public_announce?(_, %{visibility: visibility})
329 when visibility in ~w{public unlisted private direct},
330 do: visibility in ~w(public unlisted)
332 def public_announce?(object, _) do
333 Visibility.is_public?(object)
336 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
338 def get_visibility(%{visibility: visibility}, in_reply_to, _)
339 when visibility in ~w{public unlisted private direct},
340 do: {visibility, get_replied_to_visibility(in_reply_to)}
342 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
343 visibility = {:list, String.to_integer(list_id)}
344 {visibility, get_replied_to_visibility(in_reply_to)}
347 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
348 visibility = get_replied_to_visibility(in_reply_to)
349 {visibility, visibility}
352 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
354 def get_replied_to_visibility(nil), do: nil
356 def get_replied_to_visibility(activity) do
357 with %Object{} = object <- Object.normalize(activity) do
358 Visibility.get_visibility(object)
362 def check_expiry_date({:ok, nil} = res), do: res
364 def check_expiry_date({:ok, in_seconds}) do
365 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
367 if ActivityExpiration.expires_late_enough?(expiry) do
370 {:error, "Expiry date is too soon"}
374 def check_expiry_date(expiry_str) do
375 Ecto.Type.cast(:integer, expiry_str)
376 |> check_expiry_date()
379 def listen(user, %{"title" => _} = data) do
380 with visibility <- data["visibility"] || "public",
381 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
383 Map.take(data, ["album", "artist", "title", "length"])
384 |> Map.put("type", "Audio")
387 |> Map.put("actor", user.ap_id),
389 ActivityPub.listen(%{
393 context: Utils.generate_context_id(),
394 additional: %{"cc" => cc}
400 def post(user, %{status: _} = data) do
401 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
403 |> ActivityPub.create(draft.preview?)
404 |> maybe_create_activity_expiration(draft.expires_at)
408 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
409 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
414 defp maybe_create_activity_expiration(result, _), do: result
416 def pin(id, %{ap_id: user_ap_id} = user) do
419 data: %{"type" => "Create"},
420 object: %Object{data: %{"type" => object_type}}
421 } = activity <- Activity.get_by_id_with_object(id),
422 true <- object_type in ["Note", "Article", "Question"],
423 true <- Visibility.is_public?(activity),
424 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
427 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
428 _ -> {:error, dgettext("errors", "Could not pin")}
432 def unpin(id, user) do
433 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
434 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
437 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
438 _ -> {:error, dgettext("errors", "Could not unpin")}
442 def add_mute(user, activity) do
443 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
446 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
450 def remove_mute(user, activity) do
451 ThreadMute.remove_mute(user.id, activity.data["context"])
455 def thread_muted?(%{id: nil} = _user, _activity), do: false
457 def thread_muted?(user, activity) do
458 ThreadMute.exists?(user.id, activity.data["context"])
461 def report(user, data) do
462 with {:ok, account} <- get_reported_account(data.account_id),
463 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
464 {:ok, statuses} <- get_report_statuses(account, data) do
466 context: Utils.generate_context_id(),
470 content: content_html,
471 forward: Map.get(data, :forward, false)
476 defp get_reported_account(account_id) do
477 case User.get_cached_by_id(account_id) do
478 %User{} = account -> {:ok, account}
479 _ -> {:error, dgettext("errors", "Account not found")}
483 def update_report_state(activity_ids, state) when is_list(activity_ids) do
484 case Utils.update_report_state(activity_ids, state) do
485 :ok -> {:ok, activity_ids}
486 _ -> {:error, dgettext("errors", "Could not update state")}
490 def update_report_state(activity_id, state) do
491 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
492 Utils.update_report_state(activity, state)
494 nil -> {:error, :not_found}
495 _ -> {:error, dgettext("errors", "Could not update state")}
499 def update_activity_scope(activity_id, opts \\ %{}) do
500 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
501 {:ok, activity} <- toggle_sensitive(activity, opts) do
502 set_visibility(activity, opts)
504 nil -> {:error, :not_found}
505 {:error, reason} -> {:error, reason}
509 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
510 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
513 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
514 when is_boolean(sensitive) do
515 new_data = Map.put(object.data, "sensitive", sensitive)
519 |> Object.change(%{data: new_data})
520 |> Object.update_and_set_cache()
522 {:ok, Map.put(activity, :object, object)}
525 defp toggle_sensitive(activity, _), do: {:ok, activity}
527 defp set_visibility(activity, %{visibility: visibility}) do
528 Utils.update_activity_visibility(activity, visibility)
531 defp set_visibility(activity, _), do: {:ok, activity}
533 def hide_reblogs(%User{} = user, %User{} = target) do
534 UserRelationship.create_reblog_mute(user, target)
537 def show_reblogs(%User{} = user, %User{} = target) do
538 UserRelationship.delete_reblog_mute(user, target)