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

defmodule Pleroma.Web.ActivityPub.MRF.EmojiBotPolicy do
  alias Pleroma.Activity
  alias Pleroma.User
  alias Pleroma.Web.CommonAPI

  require Logger

  @moduledoc "Upload attached images as emoji"

  @behaviour Pleroma.Web.ActivityPub.MRF.Policy

  defp trim_emoji_name(n), do: String.trim(String.trim(String.trim(n), ":"))

  defp extract_emoji_name({name, url}) do
    trimmed = trim_emoji_name(name)

    case trimmed do
      n when n == "" or n == nil ->
        case Regex.run(~r/\?name=([a-zA-Z0-9_-]*)/, url) do
          [_, nname] -> {trim_emoji_name(nname), url}
          _ -> {"", url}
        end

      _ ->
        case Regex.run(~r/([a-zA-Z0-9_-]*)/, name) do
          [_, nname] -> {trim_emoji_name(nname), url}
          _ -> {"", url}
        end
    end
  end

  defp add_emoji({code, url}, instance, dir_user, dir_all, dir_repo) do
    url = Pleroma.Web.MediaProxy.url(url)

    with {:ok, %{status: status} = response} when status in 200..299 <- Pleroma.HTTP.get(url) do
      extension =
        url
        |> URI.parse()
        |> Map.get(:path)
        |> Path.basename()
        |> Path.extname() ||
          ".png"

      file_name = Regex.replace(~r/\s+/, code, "") <> extension
      file_path = Path.join(dir_user, file_name)
      secs = DateTime.utc_now() |> DateTime.to_unix()
      sym_path = Path.join(dir_all, to_string(secs) <> "_" <> instance <> "_" <> file_name)
      hash = :crypto.hash(:md5, response.body) |> Base.encode16() |> String.downcase()
      store_path = Path.join(dir_repo, hash <> extension)

      case File.write(store_path, response.body) do
        :ok ->
          File.ln_s(store_path, file_path)
          File.ln_s(store_path, sym_path)

        e ->
          Logger.warn("MRF.StealEmojiPolicy: Failed to write to #{file_path}: #{inspect(e)}")
      end
    else
      e ->
        Logger.warn("MRF.StealEmojiPolicy: Failed to fetch #{url}: #{inspect(e)}")
    end
  end

  defp remove_emoji({_, file}, instance, dir_user, dir_all) do
    file_name = Path.basename(file)
    file_path = Path.join(dir_user, file_name)
    sym_path = Path.join(dir_all, "*_" <> instance <> "_" <> file_name)
    File.rm(file_path)
    Path.wildcard(sym_path) |> Enum.map(&File.rm/1)
  end

  @impl true
  def filter(
        %{
          "object" => %{
            "attachment" => attachments,
            "to" => to,
            "actor" => actor,
            "type" => "Note",
            "source" => source
          },
          "type" => "Create",
          "directMessage" => true
        } = message
      ) do
    bot_user = Pleroma.Config.get([:mrf_emojibotpolicy, :user])
    instance = Pleroma.Web.Endpoint.host()
    bot_suffix = instance <> "/users/" <> bot_user

    if URI.parse(actor).host == instance && Enum.any?(to, &String.ends_with?(&1, bot_suffix)) do
      installed = Pleroma.Emoji.get_all()
      installed_codes = installed |> Enum.map(fn {k, _} -> k end)

      actor_nick = User.get_cached_by_ap_id(actor).nickname

      static_dir = Pleroma.Config.get([:instance, :static_dir])

      dir_user = Path.join(static_dir, "emoji/user/#{actor_nick}")
      dir_all = Path.join(static_dir, "emoji_all")
      dir_repo = Path.join(static_dir, "emoji_repo")

      toadd =
        attachments
        |> Enum.map(fn %{"url" => [%{"href" => href}]} = att -> {att["name"], href} end)
        |> Enum.map(&extract_emoji_name/1)
        |> Enum.reject(fn {name, _} -> name == "" end)

      reject_add_duplicates =
        toadd
        |> Enum.filter(fn {code, _} -> code in installed_codes end)

      allowed_extensions =
        Pleroma.Config.get([:mrf_emojibotpolicy, :extensions], ["png", "gif"])
        |> Enum.map(&String.downcase/1)

      reject_add_mime =
        toadd
        |> Enum.reject(fn {_, url} ->
          String.downcase(
            String.trim_leading(Path.extname(Path.basename(URI.parse(url).path)), ".")
          ) in allowed_extensions
        end)

      reject_add_combined = reject_add_duplicates ++ reject_add_mime

      toadd_filtered =
        toadd
        |> Enum.reject(&Enum.member?(reject_add_combined, &1))

      toremove =
        Regex.scan(~r/!remove ([^\n]*)/, source)
        |> Enum.map(fn [_, terms] -> Regex.split(~r/[\s,]+/, terms) end)
        |> List.flatten()
        |> Enum.map(&trim_emoji_name/1)
        |> Enum.map(fn code ->
          case Enum.filter(installed, fn {ic, _} -> ic == code end) do
            [{code, %{file: file}} | _] -> {code, file}
            _ -> {code, nil}
          end
        end)

      reject_remove_unknown =
        toremove
        |> Enum.filter(fn {_, file} -> file == nil end)

      reject_remove_ownership =
        toremove
        |> Enum.filter(fn {_, file} ->
          file != nil && !File.exists?(Path.join(dir_user, Path.basename(file)))
        end)

      reject_remove_combined = reject_remove_unknown ++ reject_remove_ownership

      toremove_filtered =
        toremove
        |> Enum.reject(&Enum.member?(reject_remove_combined, &1))

      if !Enum.empty?(toadd_filtered) do
        File.mkdir_p(dir_user)
        File.mkdir_p(dir_all)
        File.mkdir_p(dir_repo)
      end

      toadd_filtered
      |> Enum.map(&add_emoji(&1, instance, dir_user, dir_all, dir_repo))

      toremove_filtered
      |> Enum.map(&remove_emoji(&1, instance, dir_user, dir_all))

      if !Enum.empty?(toadd_filtered) || !Enum.empty?(toremove_filtered) do
        Pleroma.Emoji.reload()
      end

      updated_codes = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end)

      reject_add_size =
        toadd_filtered
        |> Enum.reject(fn {code, _} -> code in updated_codes end)

      reject_add_size
      |> Enum.map(&extract_emoji_name/1)
      |> Enum.reject(fn {name, _} -> name == "" end)
      |> Enum.map(fn {name, url} ->
        {name, name <> Path.extname(Path.basename(URI.parse(url).path))}
      end)
      |> Enum.each(&remove_emoji(&1, instance, dir_user, dir_all))

      added =
        toadd_filtered
        |> Enum.reject(fn el -> el in reject_add_size end)
        |> Enum.map(fn {code, _} -> code end)

      removed =
        toremove_filtered
        |> Enum.map(fn {code, _} -> code end)

      reject_add =
        case (reject_add_duplicates
              |> Enum.map(fn {code, url} -> "- (duplicate code) :" <> code <> ": " <> url end)
              |> Enum.join("\n")) <>
               (reject_add_mime
                |> Enum.map(fn {code, url} -> "- (file type) :" <> code <> ": " <> url end)
                |> Enum.join("\n")) <>
               (reject_add_size
                |> Enum.map(fn {code, url} -> "- (file size) :" <> code <> ": " <> url end)
                |> Enum.join("\n")) do
          "" -> ""
          n -> "\n\nRejected adding:\n" <> n
        end

      reject_remove =
        case (reject_remove_unknown
              |> Enum.map(fn {code, _} -> "- (unknown code) :" <> code <> ":" end)
              |> Enum.join("\n")) <>
               (reject_remove_ownership
                |> Enum.map(fn {code, _} -> "- (not owned) :" <> code <> ":" end)
                |> Enum.join("\n")) do
          "" -> ""
          n -> "\n\nRejected removing:\n" <> n
        end

      accept_add =
        case added
             |> Enum.map(fn c -> "- :" <> c <> ":" end)
             |> Enum.join("\n") do
          "" -> ""
          n -> "\n\nAdded:\n" <> n
        end

      accept_remove =
        case removed
             |> Enum.map(fn c -> "- :" <> c <> ":" end)
             |> Enum.join("\n") do
          "" -> ""
          n -> "\n\nRemoved:\n" <> n
        end

      status = reject_add <> reject_remove <> accept_add <> accept_remove

      if status != "" do
        CommonAPI.post(User.get_by_nickname(bot_user), %{
          status: "@" <> actor_nick <> status,
          in_reply_to_status_id: %Activity{data: message},
          visibility: "direct"
        })
      end
    end

    {:ok, message}
  end

  def filter(message), do: {:ok, message}

  @impl true
  def config_description do
    %{
      key: :mrf_emojibotpolicy,
      related_policy: "Pleroma.Web.ActivityPub.MRF.EmojiBotPolicy",
      label: "EmojiBot",
      description: @moduledoc,
      children: [
        %{
          key: :user,
          type: :string,
          description: "A name of emojibot account",
          suggestions: ["emojibot"]
        },
        %{
          key: :extensions,
          type: {:list, :string},
          description: "A name of permitted emoji extensions. Default: png, gif",
          suggestions: ["png", "gif", "jpg", "jpeg"]
        }
      ]
    }
  end

  @impl true
  def describe do
    {:ok, %{}}
  end
end
