# 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.SimplePolicyTest do
  use Pleroma.DataCase
  import Pleroma.Factory
  alias Pleroma.Web.ActivityPub.MRF.SimplePolicy
  alias Pleroma.Web.CommonAPI

  setup do:
          clear_config(:mrf_simple,
            media_removal: [],
            media_nsfw: [],
            federated_timeline_removal: [],
            report_removal: [],
            reject: [],
            followers_only: [],
            accept: [],
            avatar_removal: [],
            banner_removal: [],
            reject_deletes: []
          )

  describe "when :media_removal" do
    test "is empty" do
      clear_config([:mrf_simple, :media_removal], [])
      media_message = build_media_message()
      media_update_message = build_media_update_message()
      local_message = build_local_message()

      assert SimplePolicy.filter(media_message) == {:ok, media_message}
      assert SimplePolicy.filter(media_update_message) == {:ok, media_update_message}
      assert SimplePolicy.filter(local_message) == {:ok, local_message}
    end

    test "has a matching host" do
      clear_config([:mrf_simple, :media_removal], [{"remote.instance", "Some reason"}])
      media_message = build_media_message()
      media_update_message = build_media_update_message()
      local_message = build_local_message()

      assert SimplePolicy.filter(media_message) ==
               {:ok,
                media_message
                |> Map.put("object", Map.delete(media_message["object"], "attachment"))}

      assert SimplePolicy.filter(media_update_message) ==
               {:ok,
                media_update_message
                |> Map.put("object", Map.delete(media_update_message["object"], "attachment"))}

      assert SimplePolicy.filter(local_message) == {:ok, local_message}
    end

    test "match with wildcard domain" do
      clear_config([:mrf_simple, :media_removal], [{"*.remote.instance", "Whatever reason"}])
      media_message = build_media_message()
      media_update_message = build_media_update_message()
      local_message = build_local_message()

      assert SimplePolicy.filter(media_message) ==
               {:ok,
                media_message
                |> Map.put("object", Map.delete(media_message["object"], "attachment"))}

      assert SimplePolicy.filter(media_update_message) ==
               {:ok,
                media_update_message
                |> Map.put("object", Map.delete(media_update_message["object"], "attachment"))}

      assert SimplePolicy.filter(local_message) == {:ok, local_message}
    end
  end

  describe "when :media_nsfw" do
    test "is empty" do
      clear_config([:mrf_simple, :media_nsfw], [])
      media_message = build_media_message()
      media_update_message = build_media_update_message()
      local_message = build_local_message()

      assert SimplePolicy.filter(media_message) == {:ok, media_message}
      assert SimplePolicy.filter(media_update_message) == {:ok, media_update_message}
      assert SimplePolicy.filter(local_message) == {:ok, local_message}
    end

    test "has a matching host" do
      clear_config([:mrf_simple, :media_nsfw], [{"remote.instance", "Whetever"}])
      media_message = build_media_message()
      media_update_message = build_media_update_message()
      local_message = build_local_message()

      assert SimplePolicy.filter(media_message) ==
               {:ok, put_in(media_message, ["object", "sensitive"], true)}

      assert SimplePolicy.filter(media_update_message) ==
               {:ok, put_in(media_update_message, ["object", "sensitive"], true)}

      assert SimplePolicy.filter(local_message) == {:ok, local_message}
    end

    test "match with wildcard domain" do
      clear_config([:mrf_simple, :media_nsfw], [{"*.remote.instance", "yeah yeah"}])
      media_message = build_media_message()
      media_update_message = build_media_update_message()
      local_message = build_local_message()

      assert SimplePolicy.filter(media_message) ==
               {:ok, put_in(media_message, ["object", "sensitive"], true)}

      assert SimplePolicy.filter(media_update_message) ==
               {:ok, put_in(media_update_message, ["object", "sensitive"], true)}

      assert SimplePolicy.filter(local_message) == {:ok, local_message}
    end
  end

  defp build_media_message do
    %{
      "actor" => "https://remote.instance/users/bob",
      "type" => "Create",
      "object" => %{
        "attachment" => [%{}],
        "tag" => ["foo"],
        "sensitive" => false
      }
    }
  end

  defp build_media_update_message do
    %{
      "actor" => "https://remote.instance/users/bob",
      "type" => "Update",
      "object" => %{
        "attachment" => [%{}],
        "tag" => ["foo"],
        "sensitive" => false
      }
    }
  end

  describe "when :report_removal" do
    test "is empty" do
      clear_config([:mrf_simple, :report_removal], [])
      report_message = build_report_message()
      local_message = build_local_message()

      assert SimplePolicy.filter(report_message) == {:ok, report_message}
      assert SimplePolicy.filter(local_message) == {:ok, local_message}
    end

    test "has a matching host" do
      clear_config([:mrf_simple, :report_removal], [{"remote.instance", "muh"}])
      report_message = build_report_message()
      local_message = build_local_message()

      assert {:reject, _} = SimplePolicy.filter(report_message)
      assert SimplePolicy.filter(local_message) == {:ok, local_message}
    end

    test "match with wildcard domain" do
      clear_config([:mrf_simple, :report_removal], [{"*.remote.instance", "suya"}])
      report_message = build_report_message()
      local_message = build_local_message()

      assert {:reject, _} = SimplePolicy.filter(report_message)
      assert SimplePolicy.filter(local_message) == {:ok, local_message}
    end
  end

  defp build_report_message do
    %{
      "actor" => "https://remote.instance/users/bob",
      "type" => "Flag"
    }
  end

  describe "when :federated_timeline_removal" do
    test "is empty" do
      clear_config([:mrf_simple, :federated_timeline_removal], [])
      {_, ftl_message} = build_ftl_actor_and_message()
      local_message = build_local_message()

      assert SimplePolicy.filter(ftl_message) == {:ok, ftl_message}
      assert SimplePolicy.filter(local_message) == {:ok, local_message}
    end

    test "has a matching host" do
      {actor, ftl_message} = build_ftl_actor_and_message()

      ftl_message_actor_host =
        ftl_message
        |> Map.fetch!("actor")
        |> URI.parse()
        |> Map.fetch!(:host)

      clear_config([:mrf_simple, :federated_timeline_removal], [{ftl_message_actor_host, "uwu"}])
      local_message = build_local_message()

      assert {:ok, ftl_message} = SimplePolicy.filter(ftl_message)
      assert actor.follower_address in ftl_message["to"]
      refute actor.follower_address in ftl_message["cc"]
      refute "https://www.w3.org/ns/activitystreams#Public" in ftl_message["to"]
      assert "https://www.w3.org/ns/activitystreams#Public" in ftl_message["cc"]

      assert SimplePolicy.filter(local_message) == {:ok, local_message}
    end

    test "match with wildcard domain" do
      {actor, ftl_message} = build_ftl_actor_and_message()

      ftl_message_actor_host =
        ftl_message
        |> Map.fetch!("actor")
        |> URI.parse()
        |> Map.fetch!(:host)

      clear_config([:mrf_simple, :federated_timeline_removal], [
        {"*." <> ftl_message_actor_host, "owo"}
      ])

      local_message = build_local_message()

      assert {:ok, ftl_message} = SimplePolicy.filter(ftl_message)
      assert actor.follower_address in ftl_message["to"]
      refute actor.follower_address in ftl_message["cc"]
      refute "https://www.w3.org/ns/activitystreams#Public" in ftl_message["to"]
      assert "https://www.w3.org/ns/activitystreams#Public" in ftl_message["cc"]

      assert SimplePolicy.filter(local_message) == {:ok, local_message}
    end

    test "has a matching host but only as:Public in to" do
      {_actor, ftl_message} = build_ftl_actor_and_message()

      ftl_message_actor_host =
        ftl_message
        |> Map.fetch!("actor")
        |> URI.parse()
        |> Map.fetch!(:host)

      ftl_message = Map.put(ftl_message, "cc", [])

      clear_config([:mrf_simple, :federated_timeline_removal], [
        {ftl_message_actor_host, "spiderwaifu goes 88w88"}
      ])

      assert {:ok, ftl_message} = SimplePolicy.filter(ftl_message)
      refute "https://www.w3.org/ns/activitystreams#Public" in ftl_message["to"]
      assert "https://www.w3.org/ns/activitystreams#Public" in ftl_message["cc"]
    end
  end

  describe "describe/1" do
    test "returns a description of the policy" do
      clear_config([:mrf_simple, :reject], [
        {"remote.instance", "did not give my catboy a burg"}
      ])

      assert {:ok, %{mrf_simple: %{reject: ["remote.instance"]}}} = SimplePolicy.describe()
    end

    test "excludes domains listed in :transparency_exclusions" do
      clear_config([:mrf, :transparency_exclusions], [{"remote.instance", ":("}])

      clear_config([:mrf_simple, :reject], [
        {"remote.instance", "did not give my catboy a burg"}
      ])

      {:ok, description} = SimplePolicy.describe()
      assert %{mrf_simple: %{reject: []}} = description
      assert description[:mrf_simple_info][:reject] == nil
    end

    test "obfuscates domains listed in :transparency_obfuscate_domains" do
      clear_config([:mrf, :transparency_obfuscate_domains], ["remote.instance", "a.b"])

      clear_config([:mrf_simple, :reject], [
        {"remote.instance", "did not give my catboy a burg"},
        {"a.b", "spam-poked me on facebook in 2006"}
      ])

      assert {:ok,
              %{
                mrf_simple: %{reject: ["rem***.*****nce", "a.b"]},
                mrf_simple_info: %{reject: %{"rem***.*****nce" => %{}}}
              }} = SimplePolicy.describe()
    end
  end

  defp build_ftl_actor_and_message do
    actor = insert(:user)

    {actor,
     %{
       "actor" => actor.ap_id,
       "to" => ["https://www.w3.org/ns/activitystreams#Public", "http://foo.bar/baz"],
       "cc" => [actor.follower_address, "http://foo.bar/qux"]
     }}
  end

  describe "when :reject" do
    test "is empty" do
      clear_config([:mrf_simple, :reject], [])

      remote_message = build_remote_message()

      assert SimplePolicy.filter(remote_message) == {:ok, remote_message}
    end

    test "activity has a matching host" do
      clear_config([:mrf_simple, :reject], [{"remote.instance", ""}])

      remote_message = build_remote_message()

      assert {:reject, _} = SimplePolicy.filter(remote_message)
    end

    test "activity matches with wildcard domain" do
      clear_config([:mrf_simple, :reject], [{"*.remote.instance", ""}])

      remote_message = build_remote_message()

      assert {:reject, _} = SimplePolicy.filter(remote_message)
    end

    test "actor has a matching host" do
      clear_config([:mrf_simple, :reject], [{"remote.instance", ""}])

      remote_user = build_remote_user()

      assert {:reject, _} = SimplePolicy.filter(remote_user)
    end

    test "reject Announce when object would be rejected" do
      clear_config([:mrf_simple, :reject], [{"blocked.tld", ""}])

      announce = %{
        "type" => "Announce",
        "actor" => "https://okay.tld/users/alice",
        "object" => %{"type" => "Note", "actor" => "https://blocked.tld/users/bob"}
      }

      assert {:reject, _} = SimplePolicy.filter(announce)
    end

    test "reject by URI object" do
      clear_config([:mrf_simple, :reject], [{"blocked.tld", ""}])

      announce = %{
        "type" => "Announce",
        "actor" => "https://okay.tld/users/alice",
        "object" => "https://blocked.tld/activities/1"
      }

      assert {:reject, _} = SimplePolicy.filter(announce)
    end

    test "accept by matching context URI if :handle_threads is disabled" do
      clear_config([:mrf_simple, :reject], [{"blocked.tld", ""}])
      clear_config([:mrf_simple, :handle_threads], false)

      remote_message =
        build_remote_message()
        |> Map.put("context", "https://blocked.tld/contexts/abc")

      assert {:ok, _} = SimplePolicy.filter(remote_message)
    end

    test "accept by matching conversation field if :handle_threads is disabled" do
      clear_config([:mrf_simple, :reject], [{"blocked.tld", ""}])
      clear_config([:mrf_simple, :handle_threads], false)

      remote_message =
        build_remote_message()
        |> Map.put(
          "conversation",
          "tag:blocked.tld,1997-06-25:objectId=12345:objectType=Conversation"
        )

      assert {:ok, _} = SimplePolicy.filter(remote_message)
    end

    test "accept by matching reply ID if :handle_threads is disabled" do
      clear_config([:mrf_simple, :reject], [{"blocked.tld", ""}])
      clear_config([:mrf_simple, :handle_threads], false)

      remote_message =
        build_remote_message()
        |> Map.put("type", "Create")
        |> Map.put("object", %{
          "type" => "Note",
          "inReplyTo" => "https://blocked.tld/objects/1"
        })

      assert {:ok, _} = SimplePolicy.filter(remote_message)
    end

    test "reject by matching context URI if :handle_threads is enabled" do
      clear_config([:mrf_simple, :reject], [{"blocked.tld", ""}])
      clear_config([:mrf_simple, :handle_threads], true)

      remote_message =
        build_remote_message()
        |> Map.put("context", "https://blocked.tld/contexts/abc")

      assert {:reject, _} = SimplePolicy.filter(remote_message)
    end

    test "reject by matching conversation field if :handle_threads is enabled" do
      clear_config([:mrf_simple, :reject], [{"blocked.tld", ""}])
      clear_config([:mrf_simple, :handle_threads], true)

      remote_message =
        build_remote_message()
        |> Map.put(
          "conversation",
          "tag:blocked.tld,1997-06-25:objectId=12345:objectType=Conversation"
        )

      assert {:reject, _} = SimplePolicy.filter(remote_message)
    end

    test "reject by matching reply ID if :handle_threads is enabled" do
      clear_config([:mrf_simple, :reject], [{"blocked.tld", ""}])
      clear_config([:mrf_simple, :handle_threads], true)

      remote_message =
        build_remote_message()
        |> Map.put("type", "Create")
        |> Map.put("object", %{
          "type" => "Note",
          "inReplyTo" => "https://blocked.tld/objects/1"
        })

      assert {:reject, _} = SimplePolicy.filter(remote_message)
    end
  end

  describe "when :followers_only" do
    test "is empty" do
      clear_config([:mrf_simple, :followers_only], [])
      {_, ftl_message} = build_ftl_actor_and_message()
      local_message = build_local_message()

      assert SimplePolicy.filter(ftl_message) == {:ok, ftl_message}
      assert SimplePolicy.filter(local_message) == {:ok, local_message}
    end

    test "has a matching host" do
      actor = insert(:user)
      following_user = insert(:user)
      non_following_user = insert(:user)

      {:ok, _, _, _} = CommonAPI.follow(following_user, actor)

      activity = %{
        "actor" => actor.ap_id,
        "to" => [
          "https://www.w3.org/ns/activitystreams#Public",
          following_user.ap_id,
          non_following_user.ap_id
        ],
        "cc" => [actor.follower_address, "http://foo.bar/qux"]
      }

      dm_activity = %{
        "actor" => actor.ap_id,
        "to" => [
          following_user.ap_id,
          non_following_user.ap_id
        ],
        "cc" => []
      }

      actor_domain =
        activity
        |> Map.fetch!("actor")
        |> URI.parse()
        |> Map.fetch!(:host)

      clear_config([:mrf_simple, :followers_only], [{actor_domain, ""}])

      assert {:ok, new_activity} = SimplePolicy.filter(activity)
      assert actor.follower_address in new_activity["cc"]
      assert following_user.ap_id in new_activity["to"]
      refute "https://www.w3.org/ns/activitystreams#Public" in new_activity["to"]
      refute "https://www.w3.org/ns/activitystreams#Public" in new_activity["cc"]
      refute non_following_user.ap_id in new_activity["to"]
      refute non_following_user.ap_id in new_activity["cc"]

      assert {:ok, new_dm_activity} = SimplePolicy.filter(dm_activity)
      assert new_dm_activity["to"] == [following_user.ap_id]
      assert new_dm_activity["cc"] == []
    end
  end

  describe "when :accept" do
    test "is empty" do
      clear_config([:mrf_simple, :accept], [])

      local_message = build_local_message()
      remote_message = build_remote_message()

      assert SimplePolicy.filter(local_message) == {:ok, local_message}
      assert SimplePolicy.filter(remote_message) == {:ok, remote_message}
    end

    test "is not empty but activity doesn't have a matching host" do
      clear_config([:mrf_simple, :accept], [{"non.matching.remote", ""}])

      local_message = build_local_message()
      remote_message = build_remote_message()

      assert SimplePolicy.filter(local_message) == {:ok, local_message}
      assert {:reject, _} = SimplePolicy.filter(remote_message)
    end

    test "activity has a matching host" do
      clear_config([:mrf_simple, :accept], [{"remote.instance", ""}])

      local_message = build_local_message()
      remote_message = build_remote_message()

      assert SimplePolicy.filter(local_message) == {:ok, local_message}
      assert SimplePolicy.filter(remote_message) == {:ok, remote_message}
    end

    test "activity matches with wildcard domain" do
      clear_config([:mrf_simple, :accept], [{"*.remote.instance", ""}])

      local_message = build_local_message()
      remote_message = build_remote_message()

      assert SimplePolicy.filter(local_message) == {:ok, local_message}
      assert SimplePolicy.filter(remote_message) == {:ok, remote_message}
    end

    test "actor has a matching host" do
      clear_config([:mrf_simple, :accept], [{"remote.instance", ""}])

      remote_user = build_remote_user()

      assert SimplePolicy.filter(remote_user) == {:ok, remote_user}
    end
  end

  describe "when :avatar_removal" do
    test "is empty" do
      clear_config([:mrf_simple, :avatar_removal], [])

      remote_user = build_remote_user()

      assert SimplePolicy.filter(remote_user) == {:ok, remote_user}
    end

    test "is not empty but it doesn't have a matching host" do
      clear_config([:mrf_simple, :avatar_removal], [{"non.matching.remote", ""}])

      remote_user = build_remote_user()

      assert SimplePolicy.filter(remote_user) == {:ok, remote_user}
    end

    test "has a matching host" do
      clear_config([:mrf_simple, :avatar_removal], [{"remote.instance", ""}])

      remote_user = build_remote_user()
      {:ok, filtered} = SimplePolicy.filter(remote_user)

      refute filtered["icon"]
    end

    test "match with wildcard domain" do
      clear_config([:mrf_simple, :avatar_removal], [{"*.remote.instance", ""}])

      remote_user = build_remote_user()
      {:ok, filtered} = SimplePolicy.filter(remote_user)

      refute filtered["icon"]
    end
  end

  describe "when :banner_removal" do
    test "is empty" do
      clear_config([:mrf_simple, :banner_removal], [])

      remote_user = build_remote_user()

      assert SimplePolicy.filter(remote_user) == {:ok, remote_user}
    end

    test "is not empty but it doesn't have a matching host" do
      clear_config([:mrf_simple, :banner_removal], [{"non.matching.remote", ""}])

      remote_user = build_remote_user()

      assert SimplePolicy.filter(remote_user) == {:ok, remote_user}
    end

    test "has a matching host" do
      clear_config([:mrf_simple, :banner_removal], [{"remote.instance", ""}])

      remote_user = build_remote_user()
      {:ok, filtered} = SimplePolicy.filter(remote_user)

      refute filtered["image"]
    end

    test "match with wildcard domain" do
      clear_config([:mrf_simple, :banner_removal], [{"*.remote.instance", ""}])

      remote_user = build_remote_user()
      {:ok, filtered} = SimplePolicy.filter(remote_user)

      refute filtered["image"]
    end
  end

  describe "when :reject_deletes is empty" do
    setup do: clear_config([:mrf_simple, :reject_deletes], [])

    test "it accepts deletions even from rejected servers" do
      clear_config([:mrf_simple, :reject], [{"remote.instance", ""}])

      deletion_message = build_remote_deletion_message()

      assert SimplePolicy.filter(deletion_message) == {:ok, deletion_message}
    end

    test "it accepts deletions even from non-whitelisted servers" do
      clear_config([:mrf_simple, :accept], [{"non.matching.remote", ""}])

      deletion_message = build_remote_deletion_message()

      assert SimplePolicy.filter(deletion_message) == {:ok, deletion_message}
    end
  end

  describe "when :reject_deletes is not empty but it doesn't have a matching host" do
    setup do: clear_config([:mrf_simple, :reject_deletes], [{"non.matching.remote", ""}])

    test "it accepts deletions even from rejected servers" do
      clear_config([:mrf_simple, :reject], [{"remote.instance", ""}])

      deletion_message = build_remote_deletion_message()

      assert SimplePolicy.filter(deletion_message) == {:ok, deletion_message}
    end

    test "it accepts deletions even from non-whitelisted servers" do
      clear_config([:mrf_simple, :accept], [{"non.matching.remote", ""}])

      deletion_message = build_remote_deletion_message()

      assert SimplePolicy.filter(deletion_message) == {:ok, deletion_message}
    end
  end

  describe "when :reject_deletes has a matching host" do
    setup do: clear_config([:mrf_simple, :reject_deletes], [{"remote.instance", ""}])

    test "it rejects the deletion" do
      deletion_message = build_remote_deletion_message()

      assert {:reject, _} = SimplePolicy.filter(deletion_message)
    end
  end

  describe "when :reject_deletes match with wildcard domain" do
    setup do: clear_config([:mrf_simple, :reject_deletes], [{"*.remote.instance", ""}])

    test "it rejects the deletion" do
      deletion_message = build_remote_deletion_message()

      assert {:reject, _} = SimplePolicy.filter(deletion_message)
    end
  end

  defp build_local_message do
    %{
      "actor" => "#{Pleroma.Web.Endpoint.url()}/users/alice",
      "to" => [],
      "cc" => []
    }
  end

  defp build_remote_message do
    %{"actor" => "https://remote.instance/users/bob"}
  end

  defp build_remote_user do
    %{
      "id" => "https://remote.instance/users/bob",
      "icon" => %{
        "url" => "http://example.com/image.jpg",
        "type" => "Image"
      },
      "image" => %{
        "url" => "http://example.com/image.jpg",
        "type" => "Image"
      },
      "type" => "Person"
    }
  end

  defp build_remote_deletion_message do
    %{
      "type" => "Delete",
      "actor" => "https://remote.instance/users/bob"
    }
  end
end
