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

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

  alias Pleroma.Config
  alias Pleroma.User
  alias Pleroma.Web.ActivityPub.MRF
  alias Pleroma.Web.MastodonAPI

  @mastodon_api_level "2.7.2"

  @block_severities %{
    federated_timeline_removal: "silence",
    reject: "suspend"
  }

  def render("show.json", _) do
    instance = Config.get(:instance)

    %{
      uri: Pleroma.Web.WebFinger.domain(),
      title: Keyword.get(instance, :name),
      description: Keyword.get(instance, :description),
      short_description: Keyword.get(instance, :short_description),
      version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.compat_version()})",
      email: Keyword.get(instance, :email),
      urls: %{
        streaming_api: Pleroma.Web.Endpoint.websocket_url()
      },
      stats: Pleroma.Stats.get_stats(),
      thumbnail:
        URI.merge(Pleroma.Web.Endpoint.url(), Keyword.get(instance, :instance_thumbnail))
        |> to_string,
      languages: Keyword.get(instance, :languages, ["en"]),
      registrations: Keyword.get(instance, :registrations_open),
      approval_required: Keyword.get(instance, :account_approval_required),
      configuration: configuration(),
      contact_account: contact_account(Keyword.get(instance, :contact_username)),
      rules: render(__MODULE__, "rules.json"),
      # Extra (not present in Mastodon):
      max_toot_chars: Keyword.get(instance, :limit),
      max_media_attachments: Keyword.get(instance, :max_media_attachments),
      poll_limits: Keyword.get(instance, :poll_limits),
      upload_limit: Keyword.get(instance, :upload_limit),
      avatar_upload_limit: Keyword.get(instance, :avatar_upload_limit),
      background_upload_limit: Keyword.get(instance, :background_upload_limit),
      banner_upload_limit: Keyword.get(instance, :banner_upload_limit),
      background_image: Pleroma.Web.Endpoint.url() <> Keyword.get(instance, :background_image),
      description_limit: Keyword.get(instance, :description_limit),
      pleroma: pleroma_configuration(instance),
      soapbox: %{
        version: Soapbox.version()
      }
    }
  end

  def render("show2.json", _) do
    instance = Config.get(:instance)

    %{
      domain: Pleroma.Web.WebFinger.domain(),
      title: Keyword.get(instance, :name),
      version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.compat_version()})",
      source_url: Pleroma.Application.repository(),
      description: Keyword.get(instance, :short_description),
      usage: %{users: %{active_month: Pleroma.User.active_user_count()}},
      thumbnail: %{
        url:
          URI.merge(Pleroma.Web.Endpoint.url(), Keyword.get(instance, :instance_thumbnail))
          |> to_string
      },
      languages: Keyword.get(instance, :languages, ["en"]),
      configuration: configuration2(),
      registrations: %{
        enabled: Keyword.get(instance, :registrations_open),
        approval_required: Keyword.get(instance, :account_approval_required),
        message: nil
      },
      contact: %{
        email: Keyword.get(instance, :email),
        account: contact_account(Keyword.get(instance, :contact_username))
      },
      rules: render(__MODULE__, "rules.json"),
      # Extra (not present in Mastodon):
      pleroma: pleroma_configuration2(instance),
      soapbox: %{
        version: Soapbox.version()
      }
    }
  end

  def render("rules.json", _) do
    Pleroma.Rule.query()
    |> Pleroma.Repo.all()
    |> render_many(__MODULE__, "rule.json", as: :rule)
  end

  def render("rule.json", %{rule: rule}) do
    %{
      id: rule.id,
      text: rule.text
    }
  end

  def render("domain_blocks.json", _) do
    if Config.get([:mrf, :transparency]) do
      exclusions = Config.get([:mrf, :transparency_exclusions]) |> MRF.instance_list_from_tuples()

      domain_blocks =
        Config.get(:mrf_simple)
        |> Enum.map(fn {rule, instances} ->
          MRF.normalize_instance_list(instances)
          |> Enum.reject(fn {host, _} ->
            host in exclusions or not Map.has_key?(@block_severities, rule)
          end)
          |> Enum.map(fn {host, reason} ->
            %{
              domain: host,
              digest: :crypto.hash(:sha256, host) |> Base.encode16(case: :lower),
              severity: Map.get(@block_severities, rule),
              comment: reason
            }
          end)
        end)
        |> List.flatten()

      domain_blocks
    else
      []
    end
  end

  def render("translation_languages.json", _) do
    with true <- Pleroma.Language.Translation.configured?(),
         {:ok, languages} <- Pleroma.Language.Translation.languages_matrix() do
      languages
    else
      _ -> %{}
    end
  end

  def features do
    [
      "pleroma_api",
      "mastodon_api",
      "mastodon_api_streaming",
      "polls",
      "v2_suggestions",
      "pleroma_explicit_addressing",
      "shareable_emoji_packs",
      "multifetch",
      "pleroma:api/v1/notifications:include_types_filter",
      "quote_posting",
      "editing",
      if Config.get([:activitypub, :blockers_visible]) do
        "blockers_visible"
      end,
      if Config.get([:media_proxy, :enabled]) do
        "media_proxy"
      end,
      if Config.get([:gopher, :enabled]) do
        "gopher"
      end,
      if Config.get([:instance, :allow_relay]) do
        "relay"
      end,
      if Config.get([:instance, :safe_dm_mentions]) do
        "safe_dm_mentions"
      end,
      "pleroma_emoji_reactions",
      "pleroma_custom_emoji_reactions",
      "pleroma_chat_messages",
      "email_list",
      if Config.get([:instance, :show_reactions]) do
        "exposable_reactions"
      end,
      if Config.get([:instance, :profile_directory]) do
        "profile_directory"
      end,
      "pleroma:get:main/ostatus",
      if Pleroma.Language.Translation.configured?() do
        "translation"
      end,
      "events"
    ]
    |> Enum.filter(& &1)
  end

  def federation do
    quarantined = Config.get([:instance, :quarantined_instances], [])

    if Config.get([:mrf, :transparency]) do
      {:ok, data} = MRF.describe()

      data
      |> Map.put(
        :quarantined_instances,
        Enum.map(quarantined, fn {instance, _reason} -> instance end)
      )
      # This is for backwards compatibility. We originally didn't sent
      # extra info like a reason why an instance was rejected/quarantined/etc.
      # Because we didn't want to break backwards compatibility it was decided
      # to add an extra "info" key.
      |> Map.put(:quarantined_instances_info, %{
        "quarantined_instances" =>
          quarantined
          |> Enum.map(fn {instance, reason} -> {instance, %{"reason" => reason}} end)
          |> Map.new()
      })
    else
      %{}
    end
    |> Map.put(:enabled, Config.get([:instance, :federating]))
  end

  defp fields_limits do
    %{
      max_fields: Config.get([:instance, :max_account_fields]),
      max_remote_fields: Config.get([:instance, :max_remote_account_fields]),
      name_length: Config.get([:instance, :account_field_name_length]),
      value_length: Config.get([:instance, :account_field_value_length])
    }
  end

  defp configuration do
    %{
      statuses: %{
        max_characters: Config.get([:instance, :limit]),
        max_media_attachments: Config.get([:instance, :max_media_attachments])
      },
      media_attachments: %{
        image_size_limit: Config.get([:instance, :upload_limit]),
        video_size_limit: Config.get([:instance, :upload_limit])
      },
      polls: %{
        max_options: Config.get([:instance, :poll_limits, :max_options]),
        max_characters_per_option: Config.get([:instance, :poll_limits, :max_option_chars]),
        min_expiration: Config.get([:instance, :poll_limits, :min_expiration]),
        max_expiration: Config.get([:instance, :poll_limits, :max_expiration])
      }
    }
  end

  defp configuration2 do
    configuration()
    |> Map.merge(%{
      urls: %{streaming: Pleroma.Web.Endpoint.websocket_url()},
      translation: %{enabled: Pleroma.Language.Translation.configured?()},
      vapid: %{public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)}
    })
  end

  defp restrict_unauthenticated do
    Config.get([:restrict_unauthenticated])
    |> Enum.map(fn {category, features} ->
      features =
        Enum.map(features, fn
          {feature, is_enabled} when is_boolean(is_enabled) -> {feature, is_enabled}
          {feature, :if_instance_is_private} -> {feature, !Config.get!([:instance, :public])}
        end)
        |> Enum.into(%{})

      {category, features}
    end)
    |> Enum.into(%{})
  end

  defp pleroma_configuration(instance) do
    %{
      metadata: %{
        account_activation_required: Keyword.get(instance, :account_activation_required),
        features: features(),
        federation: federation(),
        fields_limits: fields_limits(),
        post_formats: Config.get([:instance, :allowed_post_formats]),
        privileged_staff: Config.get([:instance, :privileged_staff]),
        birthday_required: Config.get([:instance, :birthday_required]),
        birthday_min_age: Config.get([:instance, :birthday_min_age]),
        migration_cooldown_period: Config.get([:instance, :migration_cooldown_period]),
        restrict_unauthenticated: restrict_unauthenticated(),
        translation: translation_configuration(),
        markup: markup()
      },
      stats: %{mau: Pleroma.User.active_user_count()},
      vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key),
      oauth_consumer_strategies: Pleroma.Config.oauth_consumer_strategies(),
      favicon:
        URI.merge(Pleroma.Web.Endpoint.url(), Keyword.get(instance, :favicon))
        |> to_string
    }
  end

  defp pleroma_configuration2(instance) do
    configuration = pleroma_configuration(instance)

    configuration
    |> Map.merge(%{
      metadata:
        configuration.metadata
        |> Map.merge(%{
          avatar_upload_limit: Keyword.get(instance, :avatar_upload_limit),
          background_upload_limit: Keyword.get(instance, :background_upload_limit),
          banner_upload_limit: Keyword.get(instance, :banner_upload_limit),
          background_image:
            Pleroma.Web.Endpoint.url() <> Keyword.get(instance, :background_image),
          description_limit: Keyword.get(instance, :description_limit)
        })
    })
  end

  defp translation_configuration do
    enabled = Pleroma.Language.Translation.configured?()

    source_languages =
      with true <- enabled,
           {:ok, languages} <- Pleroma.Language.Translation.supported_languages(:source) do
        languages
      else
        _ -> nil
      end

    target_languages =
      with true <- enabled,
           {:ok, languages} <- Pleroma.Language.Translation.supported_languages(:target) do
        languages
      else
        _ -> nil
      end

    %{
      source_languages: source_languages,
      target_languages: target_languages,
      allow_unauthenticated: Config.get([Pleroma.Language.Translation, :allow_unauthenticated]),
      allow_remote: Config.get([Pleroma.Language.Translation, :allow_remote])
    }
  end

  defp contact_account(nil), do: nil

  defp contact_account("@" <> username) do
    contact_account(username)
  end

  defp contact_account(username) do
    user = User.get_cached_by_nickname(username)

    if user do
      MastodonAPI.AccountView.render("show.json", %{user: user, for: nil})
    else
      nil
    end
  end

  defp markup do
    %{
      allow_inline_images: Config.get([:markup, :allow_inline_images]),
      allow_headings: Config.get([:markup, :allow_headings]),
      allow_tables: Config.get([:markup, :allow_tables])
    }
  end
end
