# Pleroma: A lightweight social networking server # Copyright © 2017-2019 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.CommonAPI do alias Pleroma.Activity alias Pleroma.Bookmark alias Pleroma.Formatter alias Pleroma.Object alias Pleroma.ThreadMute alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility import Pleroma.Web.Gettext import Pleroma.Web.CommonAPI.Utils def follow(follower, followed) do with {:ok, follower} <- User.maybe_direct_follow(follower, followed), {:ok, activity} <- ActivityPub.follow(follower, followed), {:ok, follower, followed} <- User.wait_and_refresh( Pleroma.Config.get([:activitypub, :follow_handshake_timeout]), follower, followed ) do {:ok, follower, followed, activity} end end def unfollow(follower, unfollowed) do with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed), {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed) do {:ok, follower} end end def accept_follow_request(follower, followed) do with {:ok, follower} <- User.follow(follower, followed), %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"), {:ok, _activity} <- ActivityPub.accept(%{ to: [follower.ap_id], actor: followed, object: follow_activity.data["id"], type: "Accept" }) do {:ok, follower} end end def reject_follow_request(follower, followed) do with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"), {:ok, _activity} <- ActivityPub.reject(%{ to: [follower.ap_id], actor: followed, object: follow_activity.data["id"], type: "Reject" }) do {:ok, follower} end end def delete(activity_id, user) do with %Activity{data: %{"object" => _}} = activity <- Activity.get_by_id_with_object(activity_id), %Object{} = object <- Object.normalize(activity), true <- User.superuser?(user) || user.ap_id == object.data["actor"], {:ok, _} <- unpin(activity_id, user), {:ok, delete} <- ActivityPub.delete(object) do {:ok, delete} else _ -> {:error, dgettext("errors", "Could not delete")} end end def repeat(id_or_ap_id, user) do with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id), object <- Object.normalize(activity), nil <- Utils.get_existing_announce(user.ap_id, object) do ActivityPub.announce(user, object) else _ -> {:error, dgettext("errors", "Could not repeat")} end end def unrepeat(id_or_ap_id, user) do with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id), object <- Object.normalize(activity) do ActivityPub.unannounce(user, object) else _ -> {:error, dgettext("errors", "Could not unrepeat")} end end def favorite(id_or_ap_id, user) do with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id), object <- Object.normalize(activity), nil <- Utils.get_existing_like(user.ap_id, object) do ActivityPub.like(user, object) else _ -> {:error, dgettext("errors", "Could not favorite")} end end def unfavorite(id_or_ap_id, user) do with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id), object <- Object.normalize(activity) do ActivityPub.unlike(user, object) else _ -> {:error, dgettext("errors", "Could not unfavorite")} end end def vote(user, object, choices) do with "Question" <- object.data["type"], {:author, false} <- {:author, object.data["actor"] == user.ap_id}, {:existing_votes, []} <- {:existing_votes, Utils.get_existing_votes(user.ap_id, object)}, {options, max_count} <- get_options_and_max_count(object), option_count <- Enum.count(options), {:choice_check, {choices, true}} <- {:choice_check, normalize_and_validate_choice_indices(choices, option_count)}, {:count_check, true} <- {:count_check, Enum.count(choices) <= max_count} do answer_activities = Enum.map(choices, fn index -> answer_data = make_answer_data(user, object, Enum.at(options, index)["name"]) {:ok, activity} = ActivityPub.create(%{ to: answer_data["to"], actor: user, context: object.data["context"], object: answer_data, additional: %{"cc" => answer_data["cc"]} }) activity end) object = Object.get_cached_by_ap_id(object.data["id"]) {:ok, answer_activities, object} else {:author, _} -> {:error, dgettext("errors", "Poll's author can't vote")} {:existing_votes, _} -> {:error, dgettext("errors", "Already voted")} {:choice_check, {_, false}} -> {:error, dgettext("errors", "Invalid indices")} {:count_check, false} -> {:error, dgettext("errors", "Too many choices")} end end defp get_options_and_max_count(object) do if Map.has_key?(object.data, "anyOf") do {object.data["anyOf"], Enum.count(object.data["anyOf"])} else {object.data["oneOf"], 1} end end defp normalize_and_validate_choice_indices(choices, count) do Enum.map_reduce(choices, true, fn index, valid -> index = if is_binary(index), do: String.to_integer(index), else: index {index, if(valid, do: index < count, else: valid)} end) end 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(_, 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 Pleroma.Web.ActivityPub.Visibility.get_visibility(object) end end def post(user, %{"status" => status} = data) do limit = Pleroma.Config.get([:instance, :limit]) with status <- String.trim(status), attachments <- attachments_from_ids(data), in_reply_to <- get_replied_to_activity(data["in_reply_to_status_id"]), {visibility, in_reply_to_visibility} <- get_visibility(data, in_reply_to), {_, false} <- {:private_to_public, in_reply_to_visibility == "direct" && visibility != "direct"}, {content_html, mentions, tags} <- make_content_html( status, attachments, data, visibility ), mentioned_users <- for({_, mentioned_user} <- mentions, do: mentioned_user.ap_id), addressed_users <- get_addressed_users(mentioned_users, data["to"]), {poll, poll_emoji} <- make_poll_data(data), {to, cc} <- get_to_and_cc(user, addressed_users, in_reply_to, visibility), context <- make_context(in_reply_to), cw <- data["spoiler_text"] || "", sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}), full_payload <- String.trim(status <> cw), :ok <- validate_character_limit(full_payload, attachments, limit), object <- make_note_data( user.ap_id, to, context, content_html, attachments, in_reply_to, tags, cw, cc, sensitive, poll ), object <- Map.put( object, "emoji", Map.merge(Formatter.get_emoji_map(full_payload), poll_emoji) ) do res = ActivityPub.create( %{ to: to, actor: user, context: context, object: object, additional: %{"cc" => cc, "directMessage" => visibility == "direct"} }, Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false ) res else {:private_to_public, true} -> {:error, dgettext("errors", "The message visibility must be direct")} {:error, _} = e -> e e -> {:error, e} end end # Updates the emojis for a user based on their profile def update(user) do user = with emoji <- emoji_from_profile(user), source_data <- (user.info.source_data || %{}) |> Map.put("tag", emoji), info_cng <- User.Info.set_source_data(user.info, source_data), change <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), {:ok, user} <- User.update_and_set_cache(change) do user else _e -> user end ActivityPub.update(%{ local: true, to: [user.follower_address], cc: [], actor: user.ap_id, object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user}) }) end def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do with %Activity{ actor: ^user_ap_id, data: %{ "type" => "Create" }, object: %Object{ data: %{ "type" => "Note" } } } = activity <- get_by_id_or_ap_id(id_or_ap_id), true <- Visibility.is_public?(activity), %{valid?: true} = info_changeset <- User.Info.add_pinnned_activity(user.info, activity), changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset), {:ok, _user} <- User.update_and_set_cache(changeset) do {:ok, activity} else %{errors: [pinned_activities: {err, _}]} -> {:error, err} _ -> {:error, dgettext("errors", "Could not pin")} end end def unpin(id_or_ap_id, user) do with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id), %{valid?: true} = info_changeset <- User.Info.remove_pinnned_activity(user.info, activity), changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset), {:ok, _user} <- User.update_and_set_cache(changeset) do {:ok, activity} else %{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"]) 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?(%{id: nil} = _user, _activity), do: false def thread_muted?(user, activity) do with [] <- ThreadMute.check_muted(user.id, activity.data["context"]) do false else _ -> true end end def bookmarked?(user, activity) do with %Bookmark{} <- Bookmark.get(user.id, activity.id) do true else _ -> false end end def report(user, data) do with {:account_id, %{"account_id" => account_id}} <- {:account_id, data}, {:account, %User{} = account} <- {:account, User.get_cached_by_id(account_id)}, {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]), {:ok, statuses} <- get_report_statuses(account, data), {:ok, activity} <- ActivityPub.flag(%{ context: Utils.generate_context_id(), actor: user, account: account, statuses: statuses, content: content_html, forward: data["forward"] || false }) do {:ok, activity} else {:error, err} -> {:error, err} {:account_id, %{}} -> {:error, dgettext("errors", "Valid `account_id` required")} {:account, nil} -> {:error, dgettext("errors", "Account not found")} end end def update_report_state(activity_id, state) do with %Activity{} = activity <- Activity.get_by_id(activity_id), {:ok, activity} <- Utils.update_report_state(activity, state) do {:ok, activity} else nil -> {:error, :not_found} {:error, reason} -> {:error, reason} _ -> {: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), {:ok, activity} <- set_visibility(activity, opts) do {:ok, activity} 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, muted) do ap_id = muted.ap_id if ap_id not in user.info.muted_reblogs do info_changeset = User.Info.add_reblog_mute(user.info, ap_id) changeset = Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset) User.update_and_set_cache(changeset) end end def show_reblogs(user, muted) do ap_id = muted.ap_id if ap_id in user.info.muted_reblogs do info_changeset = User.Info.remove_reblog_mute(user.info, ap_id) changeset = Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset) User.update_and_set_cache(changeset) end end end