diff options
author | lain <lain@soykaf.club> | 2020-10-23 13:25:41 +0200 |
---|---|---|
committer | lain <lain@soykaf.club> | 2020-10-23 13:25:41 +0200 |
commit | 682f0aa2597bec4ef80691daddae1d87ddeeae0a (patch) | |
tree | c8ae5efb603e6b226ed3b5d39efb3912b63d17b9 /lib/pleroma/web/common_api.ex | |
parent | 346a59aee68d71af5d7ab6cfb9ff89008a032f2b (diff) | |
parent | 096e4518adf176c3f9cee0e38319340249083a48 (diff) | |
download | pleroma-682f0aa2597bec4ef80691daddae1d87ddeeae0a.tar.gz |
Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into 2061-chat-deletion
Diffstat (limited to 'lib/pleroma/web/common_api.ex')
-rw-r--r-- | lib/pleroma/web/common_api.ex | 573 |
1 files changed, 573 insertions, 0 deletions
diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex new file mode 100644 index 000000000..60a50b027 --- /dev/null +++ b/lib/pleroma/web/common_api.ex @@ -0,0 +1,573 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.CommonAPI do + alias Pleroma.Activity + alias Pleroma.Conversation.Participation + alias Pleroma.Formatter + alias Pleroma.Object + alias Pleroma.ThreadMute + alias Pleroma.User + alias Pleroma.UserRelationship + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.Pipeline + alias Pleroma.Web.ActivityPub.Utils + alias Pleroma.Web.ActivityPub.Visibility + + import Pleroma.Web.Gettext + import Pleroma.Web.CommonAPI.Utils + + require Pleroma.Constants + require Logger + + def block(blocker, blocked) do + with {:ok, block_data, _} <- Builder.block(blocker, blocked), + {:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do + {:ok, block} + end + end + + def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do + with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]), + :ok <- validate_chat_content_length(content, !!maybe_attachment), + {_, {:ok, chat_message_data, _meta}} <- + {:build_object, + Builder.chat_message( + user, + recipient.ap_id, + content |> format_chat_content, + attachment: maybe_attachment + )}, + {_, {:ok, create_activity_data, _meta}} <- + {:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])}, + {_, {:ok, %Activity{} = activity, _meta}} <- + {:common_pipeline, + Pipeline.common_pipeline(create_activity_data, + local: true + )} do + {:ok, activity} + else + {:common_pipeline, {:reject, _} = e} -> e + e -> e + end + end + + defp format_chat_content(nil), do: nil + + defp format_chat_content(content) do + {text, _, _} = + content + |> Formatter.html_escape("text/plain") + |> Formatter.linkify() + |> (fn {text, mentions, tags} -> + {String.replace(text, ~r/\r?\n/, "<br>"), mentions, tags} + end).() + + text + end + + defp validate_chat_content_length(_, true), do: :ok + defp validate_chat_content_length(nil, false), do: {:error, :no_content} + + defp validate_chat_content_length(content, _) do + if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do + :ok + else + {:error, :content_too_long} + end + end + + def unblock(blocker, blocked) do + with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)}, + {:ok, unblock_data, _} <- Builder.undo(blocker, block), + {:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do + {:ok, unblock} + else + {:fetch_block, nil} -> + if User.blocks?(blocker, blocked) do + User.unblock(blocker, blocked) + {:ok, :no_activity} + else + {:error, :not_blocking} + end + + e -> + e + end + end + + def follow(follower, followed) do + timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout]) + + with {:ok, follow_data, _} <- Builder.follow(follower, followed), + {:ok, activity, _} <- Pipeline.common_pipeline(follow_data, local: true), + {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do + if activity.data["state"] == "reject" do + {:error, :rejected} + else + {:ok, follower, followed, activity} + end + end + end + + def unfollow(follower, unfollowed) do + with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed), + {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed), + {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do + {:ok, follower} + end + end + + def accept_follow_request(follower, followed) do + with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), + {:ok, accept_data, _} <- Builder.accept(followed, follow_activity), + {:ok, _activity, _} <- Pipeline.common_pipeline(accept_data, local: true) do + {:ok, follower} + end + end + + def reject_follow_request(follower, followed) do + with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), + {:ok, reject_data, _} <- Builder.reject(followed, follow_activity), + {:ok, _activity, _} <- Pipeline.common_pipeline(reject_data, local: true) do + {:ok, follower} + end + end + + def delete(activity_id, user) do + with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <- + {:find_activity, Activity.get_by_id(activity_id)}, + {_, %Object{} = object, _} <- + {:find_object, Object.normalize(activity, false), activity}, + true <- User.superuser?(user) || user.ap_id == object.data["actor"], + {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]), + {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do + {:ok, delete} + else + {:find_activity, _} -> + {:error, :not_found} + + {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} -> + # We have the create activity, but not the object, it was probably pruned. + # Insert a tombstone and try again + with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object), + {:ok, _tombstone} <- Object.create(tombstone_data) do + delete(activity_id, user) + else + _ -> + Logger.error( + "Could not insert tombstone for missing object on deletion. Object is #{object}." + ) + + {:error, dgettext("errors", "Could not delete")} + end + + _ -> + {:error, dgettext("errors", "Could not delete")} + end + end + + def repeat(id, user, params \\ %{}) do + with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id), + object = %Object{} <- Object.normalize(activity, false), + {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)}, + public = public_announce?(object, params), + {:ok, announce, _} <- Builder.announce(user, object, public: public), + {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do + {:ok, activity} + else + {:existing_announce, %Activity{} = announce} -> + {:ok, announce} + + _ -> + {:error, :not_found} + end + end + + def unrepeat(id, user) do + with {_, %Activity{data: %{"type" => "Create"}} = activity} <- + {:find_activity, Activity.get_by_id(id)}, + %Object{} = note <- Object.normalize(activity, false), + %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note), + {:ok, undo, _} <- Builder.undo(user, announce), + {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do + {:ok, activity} + else + {:find_activity, _} -> {:error, :not_found} + _ -> {:error, dgettext("errors", "Could not unrepeat")} + end + end + + @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()} + def favorite(%User{} = user, id) do + case favorite_helper(user, id) do + {:ok, _} = res -> + res + + {:error, :not_found} = res -> + res + + {:error, e} -> + Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}") + {:error, dgettext("errors", "Could not favorite")} + end + end + + def favorite_helper(user, id) do + with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)}, + {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)}, + {_, {:ok, %Activity{} = activity, _meta}} <- + {:common_pipeline, + Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do + {:ok, activity} + else + {:find_object, _} -> + {:error, :not_found} + + {:common_pipeline, + { + :error, + { + :validate_object, + { + :error, + changeset + } + } + }} = e -> + if {:object, {"already liked by this actor", []}} in changeset.errors do + {:ok, :already_liked} + else + {:error, e} + end + + e -> + {:error, e} + end + end + + def unfavorite(id, user) do + with {_, %Activity{data: %{"type" => "Create"}} = activity} <- + {:find_activity, Activity.get_by_id(id)}, + %Object{} = note <- Object.normalize(activity, false), + %Activity{} = like <- Utils.get_existing_like(user.ap_id, note), + {:ok, undo, _} <- Builder.undo(user, like), + {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do + {:ok, activity} + else + {:find_activity, _} -> {:error, :not_found} + _ -> {:error, dgettext("errors", "Could not unfavorite")} + end + end + + def react_with_emoji(id, user, emoji) do + with %Activity{} = activity <- Activity.get_by_id(id), + object <- Object.normalize(activity), + {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji), + {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do + {:ok, activity} + else + _ -> + {:error, dgettext("errors", "Could not add reaction emoji")} + end + end + + def unreact_with_emoji(id, user, emoji) do + with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji), + {:ok, undo, _} <- Builder.undo(user, reaction_activity), + {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do + {:ok, activity} + else + _ -> + {:error, dgettext("errors", "Could not remove reaction emoji")} + end + end + + def vote(user, %{data: %{"type" => "Question"}} = object, choices) do + with :ok <- validate_not_author(object, user), + :ok <- validate_existing_votes(user, object), + {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do + answer_activities = + Enum.map(choices, fn index -> + {:ok, answer_object, _meta} = + Builder.answer(user, object, Enum.at(options, index)["name"]) + + {:ok, activity_data, _meta} = Builder.create(user, answer_object, []) + + {:ok, activity, _meta} = + activity_data + |> Map.put("cc", answer_object["cc"]) + |> Map.put("context", answer_object["context"]) + |> Pipeline.common_pipeline(local: true) + + # TODO: Do preload of Pleroma.Object in Pipeline + Activity.normalize(activity.data) + end) + + object = Object.get_cached_by_ap_id(object.data["id"]) + {:ok, answer_activities, object} + end + end + + defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}), + do: {:error, dgettext("errors", "Poll's author can't vote")} + + defp validate_not_author(_, _), do: :ok + + defp validate_existing_votes(%{ap_id: ap_id}, object) do + if Utils.get_existing_votes(ap_id, object) == [] do + :ok + else + {:error, dgettext("errors", "Already voted")} + end + end + + defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}) + when is_list(any_of) and any_of != [], + do: {any_of, Enum.count(any_of)} + + defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}) + when is_list(one_of) and one_of != [], + do: {one_of, 1} + + defp normalize_and_validate_choices(choices, object) do + choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end) + {options, max_count} = get_options_and_max_count(object) + count = Enum.count(options) + + with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))}, + {_, true} <- {:count_check, Enum.count(choices) <= max_count} do + {:ok, options, choices} + else + {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")} + {:count_check, _} -> {:error, dgettext("errors", "Too many choices")} + end + end + + def public_announce?(_, %{visibility: visibility}) + when visibility in ~w{public unlisted private direct}, + do: visibility in ~w(public unlisted) + + def public_announce?(object, _) do + Visibility.is_public?(object) + end + + def get_visibility(_, _, %Participation{}), do: {"direct", "direct"} + + def get_visibility(%{visibility: visibility}, in_reply_to, _) + when visibility in ~w{public unlisted private direct}, + do: {visibility, get_replied_to_visibility(in_reply_to)} + + def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do + visibility = {:list, String.to_integer(list_id)} + {visibility, get_replied_to_visibility(in_reply_to)} + end + + def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do + visibility = get_replied_to_visibility(in_reply_to) + {visibility, visibility} + end + + def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)} + + def get_replied_to_visibility(nil), do: nil + + def get_replied_to_visibility(activity) do + with %Object{} = object <- Object.normalize(activity) do + Visibility.get_visibility(object) + end + end + + def check_expiry_date({:ok, nil} = res), do: res + + def check_expiry_date({:ok, in_seconds}) do + expiry = DateTime.add(DateTime.utc_now(), in_seconds) + + if Pleroma.Workers.PurgeExpiredActivity.expires_late_enough?(expiry) do + {:ok, expiry} + else + {:error, "Expiry date is too soon"} + end + end + + def check_expiry_date(expiry_str) do + Ecto.Type.cast(:integer, expiry_str) + |> check_expiry_date() + end + + def listen(user, data) do + visibility = Map.get(data, :visibility, "public") + + with {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil), + listen_data <- + data + |> Map.take([:album, :artist, :title, :length]) + |> Map.new(fn {key, value} -> {to_string(key), value} end) + |> Map.put("type", "Audio") + |> Map.put("to", to) + |> Map.put("cc", cc) + |> Map.put("actor", user.ap_id), + {:ok, activity} <- + ActivityPub.listen(%{ + actor: user, + to: to, + object: listen_data, + context: Utils.generate_context_id(), + additional: %{"cc" => cc} + }) do + {:ok, activity} + end + end + + def post(user, %{status: _} = data) do + with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do + ActivityPub.create(draft.changes, draft.preview?) + end + end + + def pin(id, %{ap_id: user_ap_id} = user) do + with %Activity{ + actor: ^user_ap_id, + data: %{"type" => "Create"}, + object: %Object{data: %{"type" => object_type}} + } = activity <- Activity.get_by_id_with_object(id), + true <- object_type in ["Note", "Article", "Question"], + true <- Visibility.is_public?(activity), + {:ok, _user} <- User.add_pinnned_activity(user, activity) do + {:ok, activity} + else + {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err} + _ -> {:error, dgettext("errors", "Could not pin")} + end + end + + def unpin(id, user) do + with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id), + {:ok, _user} <- User.remove_pinnned_activity(user, activity) do + {:ok, activity} + else + {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err} + _ -> {:error, dgettext("errors", "Could not unpin")} + end + end + + def add_mute(user, activity) do + with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]), + _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do + {:ok, activity} + else + {:error, _} -> {:error, dgettext("errors", "conversation is already muted")} + end + end + + def remove_mute(user, activity) do + ThreadMute.remove_mute(user.id, activity.data["context"]) + {:ok, activity} + end + + def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}}) + when is_binary(context) do + ThreadMute.exists?(user_id, context) + end + + def thread_muted?(_, _), do: false + + def report(user, data) do + with {:ok, account} <- get_reported_account(data.account_id), + {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]), + {:ok, statuses} <- get_report_statuses(account, data) do + ActivityPub.flag(%{ + context: Utils.generate_context_id(), + actor: user, + account: account, + statuses: statuses, + content: content_html, + forward: Map.get(data, :forward, false) + }) + end + end + + defp get_reported_account(account_id) do + case User.get_cached_by_id(account_id) do + %User{} = account -> {:ok, account} + _ -> {:error, dgettext("errors", "Account not found")} + end + end + + def update_report_state(activity_ids, state) when is_list(activity_ids) do + case Utils.update_report_state(activity_ids, state) do + :ok -> {:ok, activity_ids} + _ -> {:error, dgettext("errors", "Could not update state")} + end + end + + def update_report_state(activity_id, state) do + with %Activity{} = activity <- Activity.get_by_id(activity_id) do + Utils.update_report_state(activity, state) + else + nil -> {:error, :not_found} + _ -> {:error, dgettext("errors", "Could not update state")} + end + end + + def update_activity_scope(activity_id, opts \\ %{}) do + with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id), + {:ok, activity} <- toggle_sensitive(activity, opts) do + set_visibility(activity, opts) + else + nil -> {:error, :not_found} + {:error, reason} -> {:error, reason} + end + end + + defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do + toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)}) + end + + defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive}) + when is_boolean(sensitive) do + new_data = Map.put(object.data, "sensitive", sensitive) + + {:ok, object} = + object + |> Object.change(%{data: new_data}) + |> Object.update_and_set_cache() + + {:ok, Map.put(activity, :object, object)} + end + + defp toggle_sensitive(activity, _), do: {:ok, activity} + + defp set_visibility(activity, %{visibility: visibility}) do + Utils.update_activity_visibility(activity, visibility) + end + + defp set_visibility(activity, _), do: {:ok, activity} + + def hide_reblogs(%User{} = user, %User{} = target) do + UserRelationship.create_reblog_mute(user, target) + end + + def show_reblogs(%User{} = user, %User{} = target) do + UserRelationship.delete_reblog_mute(user, target) + end + + def get_user(ap_id, fake_record_fallback \\ true) do + cond do + user = User.get_cached_by_ap_id(ap_id) -> + user + + user = User.get_by_guessed_nickname(ap_id) -> + user + + fake_record_fallback -> + # TODO: refactor (fake records is never a good idea) + User.error_user(ap_id) + + true -> + nil + end + end +end |