# Three-in-one policy for block, report and unfollow notifications.
# Credits for the individual parts:
# Yukkuri for the combined block/report MRF
# https://gitlab.eientei.org/eientei/pleroma/-/blob/eientei/lib/pleroma/web/activity_pub/mrf/block_bot_policy.ex
# NEETzsche for the block MRF with configurable message
# https://gitlab.com/soapbox-pub/rebased/-/blob/develop/lib/pleroma/web/activity_pub/mrf/block_notification_policy.ex
# Nekobit for the unfollow MRF (nigga nuked his account at the time, linking cached version)
# https://eientei.org/notice/AL6nnjih8H6Lco8QoS
# Pete for the example of Cachex-based rate limiting
# https://freespeechextremist.com/objects/9f24a3e4-2e34-4fcb-a0d1-42229e27da3e

defmodule Pleroma.Web.ActivityPub.MRF.HighRollerPolicy do
  alias Pleroma.User
  alias Pleroma.Web.CommonAPI
  alias Pleroma.Config

  require Logger

  @moduledoc "Notify local users upon the block, report or unfollow."
  @behaviour Pleroma.Web.ActivityPub.MRF.Policy

  defp is_block_or_unblock(%{"type" => "Block", "object" => object}),
    do: {true, "blocked", object}

  defp is_block_or_unblock(%{
         "type" => "Undo",
         "object" => %{"type" => "Block", "object" => object}
       }),
       do: {true, "unblocked", object}

  defp is_block_or_unblock(_), do: {false, nil, nil}

  defp is_report(%{"type" => "Flag", "object" => objects}) do
    case objects do
      [head | tail] when is_binary(head) -> {true, tail, head}
      s when is_binary(s) -> {true, [], s}
      _ -> {true, [], nil}
    end
  end

  defp is_report(_), do: {false, [], nil}

  defp extract_reported_post(post) do
    case post do
      %{"id" => id} -> id
      s when is_binary(s) -> s
      _ -> nil
    end
  end

  defp is_unfollow(%{
         "type" => "Undo",
         "object" => %{"type" => "Follow", "object" => object}
       }),
       do: {true, object}

  defp is_unfollow(_), do: {false, nil, nil}

  @impl true
  def filter(message) do
    with {:error, _} <- Cachex.stats(:highroller), do: Cachex.start(:highroller, [ stats: true ])
    systime = :os.system_time(:seconds)

    with {true, action, object} <- is_block_or_unblock(message),
         %User{} = actor <- User.get_cached_by_ap_id(message["actor"]),
         %User{} = recipient <- User.get_cached_by_ap_id(object),
         false <- Enum.member?(Config.get([:mrf_high_roller, :actor_blacklist]), message["actor"]),
         false <- Enum.member?(Config.get([:mrf_high_roller, :domain_blacklist]), URI.parse(message["actor"]).host),
         true <- recipient.local do

      {_, actiontime} = Cachex.fetch(:highroller, actor.nickname<>","<>recipient.nickname<>","<>action, fn(_i) -> {:commit, :os.system_time(:seconds)-1} end)
      {_, globalcount} = Cachex.fetch(:highroller, "global:"<>actor.nickname, fn(_i) -> {:commit, 0} end)

      blocker = if(Config.get([:mrf_high_roller, :tag_blocking_actor]) && !Enum.member?(Config.get([:mrf_high_roller, :domain_greylist]), URI.parse(message["actor"]).host)) do
        "@" <> actor.nickname
      else
        actor.nickname
      end

      replacements = %{
        "actor" => blocker,
        "target" => "@" <> recipient.nickname,
        "action" => action
      }

      msg =
        Regex.replace(
          ~r/{([a-z]+)?}/,
          Config.get([:mrf_high_roller, :block_message]),
          fn _, match ->
            replacements[match]
          end
        )

      if (systime > actiontime && globalcount < Config.get([:mrf_high_roller, :global_threshold])) do
        Cachex.incr(:highroller, "global:"<>actor.nickname, globalcount+1)
        Cachex.put(:highroller, actor.nickname<>","<>recipient.nickname<>","<>action, systime+Config.get([:mrf_high_roller, :timeout]))
        CommonAPI.post(User.get_cached_by_nickname(Config.get([:mrf_high_roller, :user])), %{
            status: msg,
            visibility: Config.get([:mrf_high_roller, :block_visibility])
        })
      else
        Logger.warn("Rate-limited incoming block notif!  #{inspect(message)}")
        Cachex.incr(:highroller, "global:"<>actor.nickname, globalcount+1)
        Cachex.incr(:highroller, actor.nickname<>","<>recipient.nickname<>","<>action, 30*(1+(systime-actiontime)))
      end
    end

    with {true, objects, to} <- is_report(message),
         %User{} = actor <- User.get_cached_by_ap_id(message["actor"]),
         %User{} = recipient <- User.get_cached_by_ap_id(to),
         false <- Enum.member?(Config.get([:mrf_high_roller, :actor_blacklist]), message["actor"]),
         false <- Enum.member?(Config.get([:mrf_high_roller, :domain_blacklist]), URI.parse(message["actor"]).host),
         true <- recipient.local do

      {_, actiontime} = Cachex.fetch(:highroller, actor.nickname<>","<>recipient.nickname<>",report", fn(_i) -> {:commit, :os.system_time(:seconds)-1} end)
      {_, globalcount} = Cachex.fetch(:highroller, "global:"<>actor.nickname, fn(_i) -> {:commit, 0} end)

      reporter = if(Config.get([:mrf_high_roller, :tag_reporting_actor]) && !Enum.member?(Config.get([:mrf_high_roller, :domain_greylist]), URI.parse(message["actor"]).host)) do
        "@" <> actor.nickname
      else
        actor.nickname
      end

      replacements = %{
        "actor" => reporter,
        "target" => "@" <> recipient.nickname
      }

      msg =
        Regex.replace(
          ~r/{([a-z]+)?}/,
          Pleroma.Config.get([:mrf_high_roller, :report_message]),
          fn _, match ->
            replacements[match]
          end
        )

      posts =
        objects
        |> Enum.map(&extract_reported_post/1)
        |> Enum.reject(&is_nil/1)
        |> Enum.map(fn s -> "- " <> s end)
        |> Enum.join("\n")
        |> (fn s ->
              case s do
                "" -> ""
                s -> "\n\nReported objects:\n" <> s
              end
            end).()

      comment =
        case message["content"] do
          "" -> ""
          s when is_binary(s) -> "\n\nReport message:\n" <> s
          _ -> ""
        end

      if (systime > actiontime && globalcount < Config.get([:mrf_high_roller, :global_threshold])) do
        Cachex.incr(:highroller, "global:"<>actor.nickname, globalcount+1)
        Cachex.put(:highroller, actor.nickname<>","<>recipient.nickname<>",report", systime+Config.get([:mrf_high_roller, :timeout]))
        CommonAPI.post(User.get_cached_by_nickname(Config.get([:mrf_high_roller, :user])), %{
            status: msg <> comment <> posts,
            visibility: Config.get([:mrf_high_roller, :report_visibility])
        })
      else
        Logger.warn("Rate-limited incoming report notif!  #{inspect(message)}")
        Cachex.incr(:highroller, "global:"<>actor.nickname, globalcount+1)
        Cachex.incr(:highroller, actor.nickname<>","<>recipient.nickname<>",report", 30*(1+(systime-actiontime)))
      end
    end

    with {true, object} <- is_unfollow(message),
         %User{} = actor <- User.get_cached_by_ap_id(message["actor"]),
         %User{} = recipient <- User.get_cached_by_ap_id(object),
         false <- Enum.member?(Config.get([:mrf_high_roller, :actor_blacklist]), message["actor"]),
         false <- Enum.member?(Config.get([:mrf_high_roller, :domain_blacklist]), URI.parse(message["actor"]).host),
         true <- recipient.local do

      {_, actiontime} = Cachex.fetch(:highroller, actor.nickname<>","<>recipient.nickname<>",unfollow", fn(_i) -> {:commit, :os.system_time(:seconds)-1} end)
      {_, globalcount} = Cachex.fetch(:highroller, "global:"<>actor.nickname, fn(_i) -> {:commit, 0} end)

      unfollower = if(Config.get([:mrf_high_roller, :tag_unfollowing_actor]) && !Enum.member?(Config.get([:mrf_high_roller, :domain_greylist]), URI.parse(message["actor"]).host)) do
        "@" <> actor.nickname
      else
        actor.nickname
      end

      replacements = %{
        "actor" => unfollower,
        "target" => "@" <> recipient.nickname
      }

      msg =
        Regex.replace(
          ~r/{([a-z]+)?}/,
          Pleroma.Config.get([:mrf_high_roller, :unfollow_message]),
          fn _, match ->
            replacements[match]
          end
        )

      if (systime > actiontime && globalcount < Config.get([:mrf_high_roller, :global_threshold])) do
        Cachex.incr(:highroller, "global:"<>actor.nickname, globalcount+1)
        Cachex.put(:highroller, actor.nickname<>","<>recipient.nickname<>",unfollow", systime+Config.get([:mrf_high_roller, :timeout]))
        CommonAPI.post(User.get_cached_by_nickname(Config.get([:mrf_high_roller, :user])), %{
            status: msg,
            visibility: Config.get([:mrf_high_roller, :unfollow_visibility])
        })
      else
        Logger.warn("Rate-limited incoming unfollow notif!  #{inspect(message)}")
        Cachex.incr(:highroller, "global:"<>actor.nickname, globalcount+1)
        Cachex.incr(:highroller, actor.nickname<>","<>recipient.nickname<>",unfollow", 30*(1+(systime-actiontime)))
      end
    end

    {:ok, message}
  end

  @impl true
  def config_description do
    %{
      key: :mrf_high_roller,
      related_policy: "Pleroma.Web.ActivityPub.MRF.HighRollerPolicy",
      label: "High Roller Policy",
      description: "Three-in-one policy for notifying users upon being blocked, unfollowed or reported",
      children: [
        %{
          key: :user,
          type: :string,
          label: "Account",
          description: "Account from which notifications would be sent",
          suggestions: ["blockbot"]
        },
        %{
          key: :timeout,
          type: :integer,
          label: "Timeout",
          description: "Timeout (in seconds) between which no new notifications of the same type can be sent",
          suggestions: [60]
        },
        %{
          key: :global_threshold,
          type: :integer,
          label: "Global threshold",
          description: "Global threshold of the actions for the actor",
          suggestions: [5]
        },
        %{
          key: :actor_blacklist,
          type: {:list, :string},
          label: "Actor blacklist",
          description: "List of actors to skip sending a notification about",
          suggestions: ["https://freespeechextremist.com/users/p"]
        },
        %{
          key: :domain_greylist,
          type: {:list, :string},
          label: "Domain greylist",
          description: "List of domains to exclude their users from being tagged",
          suggestions: ["freespeechextremist.com"]
        },
        %{
          key: :domain_blacklist,
          type: {:list, :string},
          label: "Domain blacklist",
          description: "List of domains to skip sending a notification about",
          suggestions: ["freespeechextremist.com"]
        },
        %{
          key: :block_message,
          type: :string,
          label: "Block message",
          description:
            "The message to send when someone is blocked or unblocked; use {actor}, {target}, and {action} variables",
          suggestions: ["{target} you have been {action} by {actor}"]
        },
        %{
          key: :tag_blocking_actor,
          type: :boolean,
          label: "Tag blocking actor",
          description: "Whether to tag the blocking actor or not"
        },
        %{
          key: :block_visibility,
          type: :string,
          label: "Block visibility",
          description: "Visibility of the block notification",
          suggestions: ["public", "unlisted", "private", "direct"]
        },
        %{
          key: :report_message,
          type: :string,
          label: "Report message",
          description:
            "The message to send when someone is reported; use {actor} and {target} variables",
          suggestions: ["{target} you have been reported by {actor}"]
        },
        %{
          key: :tag_reporting_actor,
          type: :boolean,
          label: "Tag reporting actor",
          description: "Whether to tag the reporting actor or not"
        },
        %{
          key: :report_visibility,
          type: :string,
          label: "Report visibility",
          description: "Visibility of the report notification",
          suggestions: ["public", "unlisted", "private", "direct"]
        },
        %{
          key: :unfollow_message,
          type: :string,
          label: "Unfollow message",
          description:
            "The message to send when someone is unfollowed; use {actor} and {target} variables",
          suggestions: ["{target} you have been unfollowed by {actor}"]
        },
        %{
          key: :tag_unfollowing_actor,
          type: :boolean,
          label: "Tag unfollowing actor",
          description: "Whether to tag the unfollowing actor or not"
        },
        %{
          key: :unfollow_visibility,
          type: :string,
          label: "Unfollow visibility",
          description: "Visibility of the unfollow notification",
          suggestions: ["public", "unlisted", "private", "direct"]
        }
      ]
    }
  end

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