# 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.ActivityExpiration
  alias Pleroma.Conversation.Participation
  alias Pleroma.FollowingRelationship
  alias Pleroma.Formatter
  alias Pleroma.Object
  alias Pleroma.Repo
  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 post_chat_message(%User{} = user, %User{} = recipient, content) do
    transaction =
      Repo.transaction(fn ->
        with {_, true} <-
               {:content_length,
                String.length(content) <= Pleroma.Config.get([:instance, :chat_limit])},
             {_, {:ok, chat_message_data, _meta}} <-
               {:build_object,
                Builder.chat_message(
                  user,
                  recipient.ap_id,
                  content |> Formatter.html_escape("text/plain")
                )},
             {_, {:ok, chat_message_object}} <-
               {:create_object, Object.create(chat_message_data)},
             {_, {:ok, create_activity_data, _meta}} <-
               {:build_create_activity,
                Builder.create(user, chat_message_object.data["id"], [recipient.ap_id])},
             {_, {:ok, %Activity{} = activity, _meta}} <-
               {:common_pipeline, Pipeline.common_pipeline(create_activity_data, local: true)} do
          {:ok, activity}
        else
          {:content_length, false} -> {:error, :content_too_long}
          e -> e
        end
      end)

    case transaction do
      {:ok, value} -> value
      error -> error
    end
  end

  def follow(follower, followed) do
    timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])

    with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
         {:ok, activity} <- ActivityPub.follow(follower, followed),
         {:ok, follower, followed} <- User.wait_and_refresh(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),
         {:ok, _subscription} <- User.unsubscribe(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, _relationship} <- FollowingRelationship.update(follower, followed, :follow_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, _relationship} <- FollowingRelationship.update(follower, followed, :follow_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} <-
           {:find_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
      {:find_activity, _} -> {:error, :not_found}
      _ -> {:error, dgettext("errors", "Could not delete")}
    end
  end

  def repeat(id_or_ap_id, user, params \\ %{}) do
    with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)},
         object <- Object.normalize(activity),
         announce_activity <- Utils.get_existing_announce(user.ap_id, object),
         public <- public_announce?(object, params) do
      if announce_activity do
        {:ok, announce_activity, object}
      else
        ActivityPub.announce(user, object, nil, true, public)
      end
    else
      {:find_activity, _} -> {:error, :not_found}
      _ -> {:error, dgettext("errors", "Could not repeat")}
    end
  end

  def unrepeat(id_or_ap_id, user) do
    with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)} do
      object = Object.normalize(activity)
      ActivityPub.unannounce(user, object)
    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_or_ap_id, user) do
    with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)} do
      object = Object.normalize(activity)
      ActivityPub.unlike(user, object)
    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) do
      ActivityPub.react_with_emoji(user, object, emoji)
    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) do
      ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
    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 ->
          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}
    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}}), do: {any_of, Enum.count(any_of)}
  defp get_options_and_max_count(%{data: %{"oneOf" => 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 = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)

    if ActivityExpiration.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, %{"title" => _} = data) do
    with visibility <- data["visibility"] || "public",
         {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
         listen_data <-
           Map.take(data, ["album", "artist", "title", "length"])
           |> 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
      draft.changes
      |> ActivityPub.create(draft.preview?)
      |> maybe_create_activity_expiration(draft.expires_at)
    end
  end

  defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
    with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
      {:ok, activity}
    end
  end

  defp maybe_create_activity_expiration(result, _), do: result

  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" => object_type}}
         } = activity <- get_by_id_or_ap_id(id_or_ap_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_or_ap_id, user) do
    with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_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"]) 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
    ThreadMute.exists?(user.id, activity.data["context"])
  end

  def report(user, %{"account_id" => account_id} = data) do
    with {:ok, account} <- get_reported_account(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: data["forward"] || false
      })
    end
  end

  def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}

  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
end