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
55 |> Formatter.html_escape("text/plain")
56 |> Formatter.linkify()
57 |> (fn {text, mentions, tags} ->
58 {String.replace(text, ~r/\r?\n/, "<br>"), mentions, tags}
64 defp validate_chat_content_length(_, true), do: :ok
65 defp validate_chat_content_length(nil, false), do: {:error, :no_content}
67 defp validate_chat_content_length(content, _) do
68 if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do
71 {:error, :content_too_long}
75 def unblock(blocker, blocked) do
76 with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
77 {:ok, unblock_data, _} <- Builder.undo(blocker, block),
78 {:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
81 {:fetch_block, nil} ->
82 if User.blocks?(blocker, blocked) do
83 User.unblock(blocker, blocked)
86 {:error, :not_blocking}
94 def follow(follower, followed) do
95 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
97 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
98 {:ok, activity} <- ActivityPub.follow(follower, followed),
99 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
100 {:ok, follower, followed, activity}
104 def unfollow(follower, unfollowed) do
105 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
106 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
107 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
112 def accept_follow_request(follower, followed) do
113 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
114 {:ok, follower} <- User.follow(follower, followed),
115 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
116 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
118 ActivityPub.accept(%{
119 to: [follower.ap_id],
121 object: follow_activity.data["id"],
128 def reject_follow_request(follower, followed) do
129 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
130 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
131 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
132 {:ok, _notifications} <- Notification.dismiss(follow_activity),
134 ActivityPub.reject(%{
135 to: [follower.ap_id],
137 object: follow_activity.data["id"],
144 def delete(activity_id, user) do
145 with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
146 {:find_activity, Activity.get_by_id(activity_id)},
147 {_, %Object{} = object, _} <-
148 {:find_object, Object.normalize(activity, false), activity},
149 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
150 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
151 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
154 {:find_activity, _} ->
157 {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
158 # We have the create activity, but not the object, it was probably pruned.
159 # Insert a tombstone and try again
160 with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
161 {:ok, _tombstone} <- Object.create(tombstone_data) do
162 delete(activity_id, user)
166 "Could not insert tombstone for missing object on deletion. Object is #{object}."
169 {:error, dgettext("errors", "Could not delete")}
173 {:error, dgettext("errors", "Could not delete")}
177 def repeat(id, user, params \\ %{}) do
178 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
179 object = %Object{} <- Object.normalize(activity, false),
180 {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
181 public = public_announce?(object, params),
182 {:ok, announce, _} <- Builder.announce(user, object, public: public),
183 {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
186 {:existing_announce, %Activity{} = announce} ->
194 def unrepeat(id, user) do
195 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
196 {:find_activity, Activity.get_by_id(id)},
197 %Object{} = note <- Object.normalize(activity, false),
198 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
199 {:ok, undo, _} <- Builder.undo(user, announce),
200 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
203 {:find_activity, _} -> {:error, :not_found}
204 _ -> {:error, dgettext("errors", "Could not unrepeat")}
208 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
209 def favorite(%User{} = user, id) do
210 case favorite_helper(user, id) do
214 {:error, :not_found} = res ->
218 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
219 {:error, dgettext("errors", "Could not favorite")}
223 def favorite_helper(user, id) do
224 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
225 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
226 {_, {:ok, %Activity{} = activity, _meta}} <-
228 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
245 if {:object, {"already liked by this actor", []}} in changeset.errors do
246 {:ok, :already_liked}
256 def unfavorite(id, user) do
257 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
258 {:find_activity, Activity.get_by_id(id)},
259 %Object{} = note <- Object.normalize(activity, false),
260 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
261 {:ok, undo, _} <- Builder.undo(user, like),
262 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
265 {:find_activity, _} -> {:error, :not_found}
266 _ -> {:error, dgettext("errors", "Could not unfavorite")}
270 def react_with_emoji(id, user, emoji) do
271 with %Activity{} = activity <- Activity.get_by_id(id),
272 object <- Object.normalize(activity),
273 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
274 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
278 {:error, dgettext("errors", "Could not add reaction emoji")}
282 def unreact_with_emoji(id, user, emoji) do
283 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
284 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
285 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
289 {:error, dgettext("errors", "Could not remove reaction emoji")}
293 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
294 with :ok <- validate_not_author(object, user),
295 :ok <- validate_existing_votes(user, object),
296 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
298 Enum.map(choices, fn index ->
299 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
302 ActivityPub.create(%{
303 to: answer_data["to"],
305 context: object.data["context"],
307 additional: %{"cc" => answer_data["cc"]}
313 object = Object.get_cached_by_ap_id(object.data["id"])
314 {:ok, answer_activities, object}
318 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
319 do: {:error, dgettext("errors", "Poll's author can't vote")}
321 defp validate_not_author(_, _), do: :ok
323 defp validate_existing_votes(%{ap_id: ap_id}, object) do
324 if Utils.get_existing_votes(ap_id, object) == [] do
327 {:error, dgettext("errors", "Already voted")}
331 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
332 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
334 defp normalize_and_validate_choices(choices, object) do
335 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
336 {options, max_count} = get_options_and_max_count(object)
337 count = Enum.count(options)
339 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
340 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
341 {:ok, options, choices}
343 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
344 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
348 def public_announce?(_, %{visibility: visibility})
349 when visibility in ~w{public unlisted private direct},
350 do: visibility in ~w(public unlisted)
352 def public_announce?(object, _) do
353 Visibility.is_public?(object)
356 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
358 def get_visibility(%{visibility: visibility}, in_reply_to, _)
359 when visibility in ~w{public unlisted private direct},
360 do: {visibility, get_replied_to_visibility(in_reply_to)}
362 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
363 visibility = {:list, String.to_integer(list_id)}
364 {visibility, get_replied_to_visibility(in_reply_to)}
367 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
368 visibility = get_replied_to_visibility(in_reply_to)
369 {visibility, visibility}
372 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
374 def get_replied_to_visibility(nil), do: nil
376 def get_replied_to_visibility(activity) do
377 with %Object{} = object <- Object.normalize(activity) do
378 Visibility.get_visibility(object)
382 def check_expiry_date({:ok, nil} = res), do: res
384 def check_expiry_date({:ok, in_seconds}) do
385 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
387 if ActivityExpiration.expires_late_enough?(expiry) do
390 {:error, "Expiry date is too soon"}
394 def check_expiry_date(expiry_str) do
395 Ecto.Type.cast(:integer, expiry_str)
396 |> check_expiry_date()
399 def listen(user, data) do
400 visibility = Map.get(data, :visibility, "public")
402 with {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
405 |> Map.take([:album, :artist, :title, :length])
406 |> Map.new(fn {key, value} -> {to_string(key), value} end)
407 |> Map.put("type", "Audio")
410 |> Map.put("actor", user.ap_id),
412 ActivityPub.listen(%{
416 context: Utils.generate_context_id(),
417 additional: %{"cc" => cc}
423 def post(user, %{status: _} = data) do
424 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
426 |> ActivityPub.create(draft.preview?)
427 |> maybe_create_activity_expiration(draft.expires_at)
431 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
432 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
437 defp maybe_create_activity_expiration(result, _), do: result
439 def pin(id, %{ap_id: user_ap_id} = user) do
442 data: %{"type" => "Create"},
443 object: %Object{data: %{"type" => object_type}}
444 } = activity <- Activity.get_by_id_with_object(id),
445 true <- object_type in ["Note", "Article", "Question"],
446 true <- Visibility.is_public?(activity),
447 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
450 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
451 _ -> {:error, dgettext("errors", "Could not pin")}
455 def unpin(id, user) do
456 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
457 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
460 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
461 _ -> {:error, dgettext("errors", "Could not unpin")}
465 def add_mute(user, activity) do
466 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
469 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
473 def remove_mute(user, activity) do
474 ThreadMute.remove_mute(user.id, activity.data["context"])
478 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
479 when is_binary("context") do
480 ThreadMute.exists?(user_id, context)
483 def thread_muted?(_, _), do: false
485 def report(user, data) do
486 with {:ok, account} <- get_reported_account(data.account_id),
487 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
488 {:ok, statuses} <- get_report_statuses(account, data) do
490 context: Utils.generate_context_id(),
494 content: content_html,
495 forward: Map.get(data, :forward, false)
500 defp get_reported_account(account_id) do
501 case User.get_cached_by_id(account_id) do
502 %User{} = account -> {:ok, account}
503 _ -> {:error, dgettext("errors", "Account not found")}
507 def update_report_state(activity_ids, state) when is_list(activity_ids) do
508 case Utils.update_report_state(activity_ids, state) do
509 :ok -> {:ok, activity_ids}
510 _ -> {:error, dgettext("errors", "Could not update state")}
514 def update_report_state(activity_id, state) do
515 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
516 Utils.update_report_state(activity, state)
518 nil -> {:error, :not_found}
519 _ -> {:error, dgettext("errors", "Could not update state")}
523 def update_activity_scope(activity_id, opts \\ %{}) do
524 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
525 {:ok, activity} <- toggle_sensitive(activity, opts) do
526 set_visibility(activity, opts)
528 nil -> {:error, :not_found}
529 {:error, reason} -> {:error, reason}
533 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
534 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
537 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
538 when is_boolean(sensitive) do
539 new_data = Map.put(object.data, "sensitive", sensitive)
543 |> Object.change(%{data: new_data})
544 |> Object.update_and_set_cache()
546 {:ok, Map.put(activity, :object, object)}
549 defp toggle_sensitive(activity, _), do: {:ok, activity}
551 defp set_visibility(activity, %{visibility: visibility}) do
552 Utils.update_activity_visibility(activity, visibility)
555 defp set_visibility(activity, _), do: {:ok, activity}
557 def hide_reblogs(%User{} = user, %User{} = target) do
558 UserRelationship.create_reblog_mute(user, target)
561 def show_reblogs(%User{} = user, %User{} = target) do
562 UserRelationship.delete_reblog_mute(user, target)