# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.MastodonAPI.StatusView do
  use Pleroma.Web, :view

  require Pleroma.Constants

  alias Pleroma.Activity
  alias Pleroma.ActivityExpiration
  alias Pleroma.Conversation
  alias Pleroma.Conversation.Participation
  alias Pleroma.HTML
  alias Pleroma.Object
  alias Pleroma.Repo
  alias Pleroma.User
  alias Pleroma.Web.CommonAPI
  alias Pleroma.Web.CommonAPI.Utils
  alias Pleroma.Web.MastodonAPI.AccountView
  alias Pleroma.Web.MastodonAPI.StatusView
  alias Pleroma.Web.MediaProxy

  import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1]

  # TODO: Add cached version.
  defp get_replied_to_activities([]), do: %{}

  defp get_replied_to_activities(activities) do
    activities
    |> Enum.map(fn
      %{data: %{"type" => "Create"}} = activity ->
        object = Object.normalize(activity)
        object && object.data["inReplyTo"] != "" && object.data["inReplyTo"]

      _ ->
        nil
    end)
    |> Enum.filter(& &1)
    |> Activity.create_by_object_ap_id_with_object()
    |> Repo.all()
    |> Enum.reduce(%{}, fn activity, acc ->
      object = Object.normalize(activity)
      if object, do: Map.put(acc, object.data["id"], activity), else: acc
    end)
  end

  defp get_user(ap_id) do
    cond do
      user = User.get_cached_by_ap_id(ap_id) ->
        user

      user = User.get_by_guessed_nickname(ap_id) ->
        user

      true ->
        User.error_user(ap_id)
    end
  end

  defp get_context_id(%{data: %{"context_id" => context_id}}) when not is_nil(context_id),
    do: context_id

  defp get_context_id(%{data: %{"context" => context}}) when is_binary(context),
    do: Utils.context_to_conversation_id(context)

  defp get_context_id(_), do: nil

  defp reblogged?(activity, user) do
    object = Object.normalize(activity) || %{}
    present?(user && user.ap_id in (object.data["announcements"] || []))
  end

  def render("index.json", opts) do
    replied_to_activities = get_replied_to_activities(opts.activities)
    parallel = unless is_nil(opts[:parallel]), do: opts[:parallel], else: true

    opts.activities
    |> safe_render_many(
      StatusView,
      "status.json",
      Map.put(opts, :replied_to_activities, replied_to_activities),
      parallel
    )
  end

  def render(
        "status.json",
        %{activity: %{data: %{"type" => "Announce", "object" => _object}} = activity} = opts
      ) do
    user = get_user(activity.data["actor"])
    created_at = Utils.to_masto_date(activity.data["published"])
    activity_object = Object.normalize(activity)

    reblogged_activity =
      Activity.create_by_object_ap_id(activity_object.data["id"])
      |> Activity.with_preloaded_bookmark(opts[:for])
      |> Activity.with_set_thread_muted_field(opts[:for])
      |> Repo.one()

    reblogged = render("status.json", Map.put(opts, :activity, reblogged_activity))

    favorited = opts[:for] && opts[:for].ap_id in (activity_object.data["likes"] || [])

    bookmarked = Activity.get_bookmark(reblogged_activity, opts[:for]) != nil

    mentions =
      activity.recipients
      |> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)
      |> Enum.filter(& &1)
      |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)

    %{
      id: to_string(activity.id),
      uri: activity_object.data["id"],
      url: activity_object.data["id"],
      account: AccountView.render("account.json", %{user: user, for: opts[:for]}),
      in_reply_to_id: nil,
      in_reply_to_account_id: nil,
      reblog: reblogged,
      content: reblogged[:content] || "",
      created_at: created_at,
      reblogs_count: 0,
      replies_count: 0,
      favourites_count: 0,
      reblogged: reblogged?(reblogged_activity, opts[:for]),
      favourited: present?(favorited),
      bookmarked: present?(bookmarked),
      muted: false,
      pinned: pinned?(activity, user),
      sensitive: false,
      spoiler_text: "",
      visibility: "public",
      media_attachments: reblogged[:media_attachments] || [],
      mentions: mentions,
      tags: reblogged[:tags] || [],
      application: %{
        name: "Web",
        website: nil
      },
      language: nil,
      emojis: [],
      pleroma: %{
        local: activity.local
      }
    }
  end

  def render("status.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
    object = Object.normalize(activity)

    user = get_user(activity.data["actor"])
    user_follower_address = user.follower_address

    like_count = object.data["like_count"] || 0
    announcement_count = object.data["announcement_count"] || 0

    tags = object.data["tag"] || []
    sensitive = object.data["sensitive"] || Enum.member?(tags, "nsfw")

    tag_mentions =
      tags
      |> Enum.filter(fn tag -> is_map(tag) and tag["type"] == "Mention" end)
      |> Enum.map(fn tag -> tag["href"] end)

    mentions =
      (object.data["to"] ++ tag_mentions)
      |> Enum.uniq()
      |> Enum.map(fn
        Pleroma.Constants.as_public() -> nil
        ^user_follower_address -> nil
        ap_id -> User.get_cached_by_ap_id(ap_id)
      end)
      |> Enum.filter(& &1)
      |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)

    favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])

    bookmarked = Activity.get_bookmark(activity, opts[:for]) != nil

    client_posted_this_activity = opts[:for] && user.id == opts[:for].id

    expires_at =
      with true <- client_posted_this_activity,
           expiration when not is_nil(expiration) <-
             ActivityExpiration.get_by_activity_id(activity.id) do
        expiration.scheduled_at
      end

    thread_muted? =
      case activity.thread_muted? do
        thread_muted? when is_boolean(thread_muted?) -> thread_muted?
        nil -> (opts[:for] && CommonAPI.thread_muted?(opts[:for], activity)) || false
      end

    attachment_data = object.data["attachment"] || []
    attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)

    created_at = Utils.to_masto_date(object.data["published"])

    reply_to = get_reply_to(activity, opts)

    reply_to_user = reply_to && get_user(reply_to.data["actor"])

    content =
      object
      |> render_content()

    content_html =
      content
      |> HTML.get_cached_scrubbed_html_for_activity(
        User.html_filter_policy(opts[:for]),
        activity,
        "mastoapi:content"
      )

    content_plaintext =
      content
      |> HTML.get_cached_stripped_html_for_activity(
        activity,
        "mastoapi:content"
      )

    summary = object.data["summary"] || ""

    summary_html =
      summary
      |> HTML.get_cached_scrubbed_html_for_activity(
        User.html_filter_policy(opts[:for]),
        activity,
        "mastoapi:summary"
      )

    summary_plaintext =
      summary
      |> HTML.get_cached_stripped_html_for_activity(
        activity,
        "mastoapi:summary"
      )

    card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity))

    url =
      if user.local do
        Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
      else
        object.data["url"] || object.data["external_url"] || object.data["id"]
      end

    direct_conversation_id =
      with {_, true} <- {:include_id, opts[:with_direct_conversation_id]},
           {_, %User{} = for_user} <- {:for_user, opts[:for]},
           %{data: %{"context" => context}} when is_binary(context) <- activity,
           %Conversation{} = conversation <- Conversation.get_for_ap_id(context),
           %Participation{id: participation_id} <-
             Participation.for_user_and_conversation(for_user, conversation) do
        participation_id
      else
        _e ->
          nil
      end

    %{
      id: to_string(activity.id),
      uri: object.data["id"],
      url: url,
      account: AccountView.render("account.json", %{user: user, for: opts[:for]}),
      in_reply_to_id: reply_to && to_string(reply_to.id),
      in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
      reblog: nil,
      card: card,
      content: content_html,
      created_at: created_at,
      reblogs_count: announcement_count,
      replies_count: object.data["repliesCount"] || 0,
      favourites_count: like_count,
      reblogged: reblogged?(activity, opts[:for]),
      favourited: present?(favorited),
      bookmarked: present?(bookmarked),
      muted: thread_muted? || User.mutes?(opts[:for], user),
      pinned: pinned?(activity, user),
      sensitive: sensitive,
      spoiler_text: summary_html,
      visibility: get_visibility(object),
      media_attachments: attachments,
      poll: render("poll.json", %{object: object, for: opts[:for]}),
      mentions: mentions,
      tags: build_tags(tags),
      application: %{
        name: "Web",
        website: nil
      },
      language: nil,
      emojis: build_emojis(object.data["emoji"]),
      pleroma: %{
        local: activity.local,
        conversation_id: get_context_id(activity),
        in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
        content: %{"text/plain" => content_plaintext},
        spoiler_text: %{"text/plain" => summary_plaintext},
        expires_at: expires_at,
        direct_conversation_id: direct_conversation_id,
        thread_muted: thread_muted?
      }
    }
  end

  def render("status.json", _) do
    nil
  end

  def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
    page_url_data = URI.parse(page_url)

    page_url_data =
      if rich_media[:url] != nil do
        URI.merge(page_url_data, URI.parse(rich_media[:url]))
      else
        page_url_data
      end

    page_url = page_url_data |> to_string

    image_url =
      if rich_media[:image] != nil do
        URI.merge(page_url_data, URI.parse(rich_media[:image]))
        |> to_string
      else
        nil
      end

    site_name = rich_media[:site_name] || page_url_data.host

    %{
      type: "link",
      provider_name: site_name,
      provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
      url: page_url,
      image: image_url |> MediaProxy.url(),
      title: rich_media[:title] || "",
      description: rich_media[:description] || "",
      pleroma: %{
        opengraph: rich_media
      }
    }
  end

  def render("card.json", _) do
    nil
  end

  def render("attachment.json", %{attachment: attachment}) do
    [attachment_url | _] = attachment["url"]
    media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
    href = attachment_url["href"] |> MediaProxy.url()

    type =
      cond do
        String.contains?(media_type, "image") -> "image"
        String.contains?(media_type, "video") -> "video"
        String.contains?(media_type, "audio") -> "audio"
        true -> "unknown"
      end

    <<hash_id::signed-32, _rest::binary>> = :crypto.hash(:md5, href)

    %{
      id: to_string(attachment["id"] || hash_id),
      url: href,
      remote_url: href,
      preview_url: href,
      text_url: href,
      type: type,
      description: attachment["name"],
      pleroma: %{mime_type: media_type}
    }
  end

  def render("poll.json", %{object: object} = opts) do
    {multiple, options} =
      case object.data do
        %{"anyOf" => options} when is_list(options) -> {true, options}
        %{"oneOf" => options} when is_list(options) -> {false, options}
        _ -> {nil, nil}
      end

    if options do
      {end_time, expired} =
        case object.data["closed"] || object.data["endTime"] do
          end_time when is_binary(end_time) ->
            end_time =
              (object.data["closed"] || object.data["endTime"])
              |> NaiveDateTime.from_iso8601!()

            expired =
              end_time
              |> NaiveDateTime.compare(NaiveDateTime.utc_now())
              |> case do
                :lt -> true
                _ -> false
              end

            end_time = Utils.to_masto_date(end_time)

            {end_time, expired}

          _ ->
            {nil, false}
        end

      voted =
        if opts[:for] do
          existing_votes =
            Pleroma.Web.ActivityPub.Utils.get_existing_votes(opts[:for].ap_id, object)

          existing_votes != [] or opts[:for].ap_id == object.data["actor"]
        else
          false
        end

      {options, votes_count} =
        Enum.map_reduce(options, 0, fn %{"name" => name} = option, count ->
          current_count = option["replies"]["totalItems"] || 0

          {%{
             title: HTML.strip_tags(name),
             votes_count: current_count
           }, current_count + count}
        end)

      %{
        # Mastodon uses separate ids for polls, but an object can't have
        # more than one poll embedded so object id is fine
        id: to_string(object.id),
        expires_at: end_time,
        expired: expired,
        multiple: multiple,
        votes_count: votes_count,
        options: options,
        voted: voted,
        emojis: build_emojis(object.data["emoji"])
      }
    else
      nil
    end
  end

  def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
    object = Object.normalize(activity)

    with nil <- replied_to_activities[object.data["inReplyTo"]] do
      # If user didn't participate in the thread
      Activity.get_in_reply_to_activity(activity)
    end
  end

  def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
    object = Object.normalize(activity)

    if object.data["inReplyTo"] && object.data["inReplyTo"] != "" do
      Activity.get_create_by_object_ap_id(object.data["inReplyTo"])
    else
      nil
    end
  end

  def render_content(%{data: %{"type" => "Video"}} = object) do
    with name when not is_nil(name) and name != "" <- object.data["name"] do
      "<p><a href=\"#{object.data["id"]}\">#{name}</a></p>#{object.data["content"]}"
    else
      _ -> object.data["content"] || ""
    end
  end

  def render_content(%{data: %{"type" => object_type}} = object)
      when object_type in ["Article", "Page"] do
    with summary when not is_nil(summary) and summary != "" <- object.data["name"],
         url when is_bitstring(url) <- object.data["url"] do
      "<p><a href=\"#{url}\">#{summary}</a></p>#{object.data["content"]}"
    else
      _ -> object.data["content"] || ""
    end
  end

  def render_content(object), do: object.data["content"] || ""

  @doc """
  Builds a dictionary tags.

  ## Examples

  iex> Pleroma.Web.MastodonAPI.StatusView.build_tags(["fediverse", "nextcloud"])
  [{"name": "fediverse", "url": "/tag/fediverse"},
   {"name": "nextcloud", "url": "/tag/nextcloud"}]

  """
  @spec build_tags(list(any())) :: list(map())
  def build_tags(object_tags) when is_list(object_tags) do
    object_tags = for tag when is_binary(tag) <- object_tags, do: tag

    Enum.reduce(object_tags, [], fn tag, tags ->
      tags ++ [%{name: tag, url: "/tag/#{tag}"}]
    end)
  end

  def build_tags(_), do: []

  @doc """
  Builds list emojis.

  Arguments: `nil` or list tuple of name and url.

  Returns list emojis.

  ## Examples

  iex> Pleroma.Web.MastodonAPI.StatusView.build_emojis([{"2hu", "corndog.png"}])
  [%{shortcode: "2hu", static_url: "corndog.png", url: "corndog.png", visible_in_picker: false}]

  """
  @spec build_emojis(nil | list(tuple())) :: list(map())
  def build_emojis(nil), do: []

  def build_emojis(emojis) do
    emojis
    |> Enum.map(fn {name, url} ->
      name = HTML.strip_tags(name)

      url =
        url
        |> HTML.strip_tags()
        |> MediaProxy.url()

      %{shortcode: name, url: url, static_url: url, visible_in_picker: false}
    end)
  end

  defp present?(nil), do: false
  defp present?(false), do: false
  defp present?(_), do: true

  defp pinned?(%Activity{id: id}, %User{info: %{pinned_activities: pinned_activities}}),
    do: id in pinned_activities
end