# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors 
# 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.HTML
  alias Pleroma.Object
  alias Pleroma.Repo
  alias Pleroma.User
  alias Pleroma.UserRelationship
  alias Pleroma.Web.CommonAPI
  alias Pleroma.Web.CommonAPI.Utils
  alias Pleroma.Web.MastodonAPI.AccountView
  alias Pleroma.Web.MastodonAPI.PollView
  alias Pleroma.Web.MastodonAPI.StatusView
  alias Pleroma.Web.MediaProxy
  import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1, visible_for_user?: 2]
  # 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
  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
  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
    reading_user = opts[:for]
    # To do: check AdminAPIControllerTest on the reasons behind nil activities in the list
    activities = Enum.filter(opts.activities, & &1)
    replied_to_activities = get_replied_to_activities(activities)
    parent_activities =
      activities
      |> Enum.filter(&(&1.data["type"] == "Announce" && &1.data["object"]))
      |> Enum.map(&Object.normalize(&1).data["id"])
      |> Activity.create_by_object_ap_id()
      |> Activity.with_preloaded_object(:left)
      |> Activity.with_preloaded_bookmark(reading_user)
      |> Activity.with_set_thread_muted_field(reading_user)
      |> Repo.all()
    relationships_opt =
      cond do
        Map.has_key?(opts, :relationships) ->
          opts[:relationships]
        is_nil(reading_user) ->
          UserRelationship.view_relationships_option(nil, [])
        true ->
          # Note: unresolved users are filtered out
          actors =
            (activities ++ parent_activities)
            |> Enum.map(&get_user(&1.data["actor"], false))
            |> Enum.filter(& &1)
          UserRelationship.view_relationships_option(reading_user, actors, subset: :source_mutes)
      end
    opts =
      opts
      |> Map.put(:replied_to_activities, replied_to_activities)
      |> Map.put(:parent_activities, parent_activities)
      |> Map.put(:relationships, relationships_opt)
    safe_render_many(activities, StatusView, "show.json", opts)
  end
  def render(
        "show.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_parent_activity =
      if opts[:parent_activities] do
        Activity.Queries.find_by_object_ap_id(
          opts[:parent_activities],
          activity_object.data["id"]
        )
      else
        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()
      end
    reblog_rendering_opts = Map.put(opts, :activity, reblogged_parent_activity)
    reblogged = render("show.json", reblog_rendering_opts)
    favorited = opts[:for] && opts[:for].ap_id in (activity_object.data["likes"] || [])
    bookmarked = Activity.get_bookmark(reblogged_parent_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("show.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_parent_activity, opts[:for]),
      favourited: present?(favorited),
      bookmarked: present?(bookmarked),
      muted: false,
      pinned: pinned?(activity, user),
      sensitive: false,
      spoiler_text: "",
      visibility: get_visibility(activity),
      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("show.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,
           %ActivityExpiration{scheduled_at: scheduled_at} <-
             ActivityExpiration.get_by_activity_id(activity.id) do
        scheduled_at
      else
        _ -> nil
      end
    thread_muted? =
      cond do
        is_nil(opts[:for]) -> false
        is_boolean(activity.thread_muted?) -> activity.thread_muted?
        true -> CommonAPI.thread_muted?(opts[:for], activity)
      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"] || ""
    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 {_, nil} <- {:direct_conversation_id, opts[:direct_conversation_id]},
           {_, true} <- {:include_id, opts[:with_direct_conversation_id]},
           {_, %User{} = for_user} <- {:for_user, opts[:for]} do
        Activity.direct_conversation_id(activity, for_user)
      else
        {:direct_conversation_id, participation_id} when is_integer(participation_id) ->
          participation_id
        _e ->
          nil
      end
    emoji_reactions =
      with %{data: %{"reactions" => emoji_reactions}} <- object do
        Enum.map(emoji_reactions, fn [emoji, users] ->
          %{
            name: emoji,
            count: length(users),
            me: !!(opts[:for] && opts[:for].ap_id in users)
          }
        end)
      else
        _ -> []
      end
    # Status muted state (would do 1 request per status unless user mutes are preloaded)
    muted =
      thread_muted? ||
        UserRelationship.exists?(
          get_in(opts, [:relationships, :user_relationships]),
          :mute,
          opts[:for],
          user,
          fn for_user, user -> User.mutes?(for_user, user) end
        )
    %{
      id: to_string(activity.id),
      uri: object.data["id"],
      url: url,
      account:
        AccountView.render("show.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: muted,
      pinned: pinned?(activity, user),
      sensitive: sensitive,
      spoiler_text: summary,
      visibility: get_visibility(object),
      media_attachments: attachments,
      poll: render(PollView, "show.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},
        expires_at: expires_at,
        direct_conversation_id: direct_conversation_id,
        thread_muted: thread_muted?,
        emoji_reactions: emoji_reactions,
        parent_visible: visible_for_user?(reply_to, opts[:for])
      }
    }
  end
  def render("show.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 is_binary(rich_media["url"]) 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 is_binary(rich_media["image"]) do
        URI.merge(page_url_data, URI.parse(rich_media["image"]))
        |> to_string
      end
    %{
      type: "link",
      provider_name: page_url_data.host,
      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
  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
    <> = :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("context.json", %{activity: activity, activities: activities, user: user}) do
    %{ancestors: ancestors, descendants: descendants} =
      activities
      |> Enum.reverse()
      |> Enum.group_by(fn %{id: id} -> if id < activity.id, do: :ancestors, else: :descendants end)
      |> Map.put_new(:ancestors, [])
      |> Map.put_new(:descendants, [])
    %{
      ancestors: render("index.json", for: user, activities: ancestors, as: :activity),
      descendants: render("index.json", for: user, activities: descendants, as: :activity)
    }
  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" => object_type}} = object)
      when object_type in ["Video", "Event", "Audio"] do
    with name when not is_nil(name) and name != "" <- object.data["name"] do
      "#{name}
#{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
      "#{summary}
#{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
    |> Enum.filter(&is_binary/1)
    |> Enum.map(&%{name: &1, url: "/tag/#{URI.encode(&1)}"})
  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{pinned_activities: pinned_activities}),
    do: id in pinned_activities
end