diff --git a/config/description.exs b/config/description.exs index f4b8768da..e43713444 100644 --- a/config/description.exs +++ b/config/description.exs @@ -758,12 +758,12 @@ }, %{ key: :quarantined_instances, - type: {:list, :string}, + type: {:list, :tuple}, description: - "List of ActivityPub instances where private (DMs, followers-only) activities will not be sent", + "List of ActivityPub instances where private (DMs, followers-only) activities will not be sent and the reason for doing so", suggestions: [ - "quarantined.com", - "*.quarantined.com" + {"quarantined.com", "Reason"}, + {"*.quarantined.com", "Reason"} ] }, %{ diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex index 59c6b0f58..692121d1a 100644 --- a/lib/pleroma/config/deprecation_warnings.ex +++ b/lib/pleroma/config/deprecation_warnings.ex @@ -20,6 +20,142 @@ defmodule Pleroma.Config.DeprecationWarnings do "\n* `config :pleroma, :instance, mrf_transparency_exclusions` is now `config :pleroma, :mrf, transparency_exclusions`"} ] + def check_simple_policy_tuples do + has_strings = + Config.get([:mrf_simple]) + |> Enum.any?(fn {_, v} -> Enum.any?(v, fn e -> is_binary(e) end) end) + + if has_strings do + Logger.warn(""" + !!!DEPRECATION WARNING!!! + Your config is using strings in the SimplePolicy configuration instead of tuples. They should work for now, but you are advised to change to the new configuration to prevent possible issues later: + + ``` + config :pleroma, :mrf_simple, + media_removal: ["instance.tld"], + media_nsfw: ["instance.tld"], + federated_timeline_removal: ["instance.tld"], + report_removal: ["instance.tld"], + reject: ["instance.tld"], + followers_only: ["instance.tld"], + accept: ["instance.tld"], + avatar_removal: ["instance.tld"], + banner_removal: ["instance.tld"], + reject_deletes: ["instance.tld"] + ``` + + Is now + + + ``` + config :pleroma, :mrf_simple, + media_removal: [{"instance.tld", "Reason for media removal"}], + media_nsfw: [{"instance.tld", "Reason for media nsfw"}], + federated_timeline_removal: [{"instance.tld", "Reason for federated timeline removal"}], + report_removal: [{"instance.tld", "Reason for report removal"}], + reject: [{"instance.tld", "Reason for reject"}], + followers_only: [{"instance.tld", "Reason for followers only"}], + accept: [{"instance.tld", "Reason for accept"}], + avatar_removal: [{"instance.tld", "Reason for avatar removal"}], + banner_removal: [{"instance.tld", "Reason for banner removal"}], + reject_deletes: [{"instance.tld", "Reason for reject deletes"}] + ``` + """) + + new_config = + Config.get([:mrf_simple]) + |> Enum.map(fn {k, v} -> + {k, + Enum.map(v, fn + {instance, reason} -> {instance, reason} + instance -> {instance, ""} + end)} + end) + + Config.put([:mrf_simple], new_config) + + :error + else + :ok + end + end + + def check_quarantined_instances_tuples do + has_strings = + Config.get([:instance, :quarantined_instances]) |> Enum.any?(fn e -> is_binary(e) end) + + if has_strings do + Logger.warn(""" + !!!DEPRECATION WARNING!!! + Your config is using strings in the quarantined_instances configuration instead of tuples. They should work for now, but you are advised to change to the new configuration to prevent possible issues later: + + ``` + config :pleroma, :instance, + quarantined_instances: ["instance.tld"] + ``` + + Is now + + + ``` + config :pleroma, :instance, + quarantined_instances: [{"instance.tld", "Reason for quarantine"}] + ``` + """) + + new_config = + Config.get([:instance, :quarantined_instances]) + |> Enum.map(fn + {instance, reason} -> {instance, reason} + instance -> {instance, ""} + end) + + Config.put([:instance, :quarantined_instances], new_config) + + :error + else + :ok + end + end + + def check_transparency_exclusions_tuples do + has_strings = + Config.get([:mrf, :transparency_exclusions]) |> Enum.any?(fn e -> is_binary(e) end) + + if has_strings do + Logger.warn(""" + !!!DEPRECATION WARNING!!! + Your config is using strings in the transparency_exclusions configuration instead of tuples. They should work for now, but you are advised to change to the new configuration to prevent possible issues later: + + ``` + config :pleroma, :mrf, + transparency_exclusions: ["instance.tld"] + ``` + + Is now + + + ``` + config :pleroma, :mrf, + transparency_exclusions: [{"instance.tld", "Reason to exlude transparency"}] + ``` + """) + + new_config = + Config.get([:mrf, :transparency_exclusions]) + |> Enum.map(fn + {instance, reason} -> {instance, reason} + instance -> {instance, ""} + end) + + Config.put([:mrf, :transparency_exclusions], new_config) + + :error + else + :ok + end + end + def check_hellthread_threshold do if Config.get([:mrf_hellthread, :threshold]) do Logger.warn(""" @@ -34,18 +170,22 @@ def check_hellthread_threshold do end def warn do - with :ok <- check_hellthread_threshold(), - :ok <- check_old_mrf_config(), - :ok <- check_media_proxy_whitelist_config(), - :ok <- check_welcome_message_config(), - :ok <- check_gun_pool_options(), - :ok <- check_activity_expiration_config(), - :ok <- check_remote_ip_plug_name() do - :ok - else - _ -> - :error - end + [ + check_hellthread_threshold(), + check_old_mrf_config(), + check_media_proxy_whitelist_config(), + check_welcome_message_config(), + check_gun_pool_options(), + check_activity_expiration_config(), + check_quarantined_instances_tuples(), + check_transparency_exclusions_tuples(), + check_simple_policy_tuples(), + check_remote_ip_plug_name() + ] + |> Enum.reduce(:ok, fn + :ok, :ok -> :ok + _, _ -> :error + end) end def check_welcome_message_config do diff --git a/lib/pleroma/web/activity_pub/mrf.ex b/lib/pleroma/web/activity_pub/mrf.ex index 6e73b2f22..13d592cec 100644 --- a/lib/pleroma/web/activity_pub/mrf.ex +++ b/lib/pleroma/web/activity_pub/mrf.ex @@ -106,6 +106,11 @@ def subdomain_match?(domains, host) do Enum.any?(domains, fn domain -> Regex.match?(domain, host) end) end + @spec instance_list_from_tuples([{String.t(), String.t()}]) :: [String.t()] + def instance_list_from_tuples(list) do + Enum.map(list, fn {instance, _} -> instance end) + end + def describe(policies) do {:ok, policy_configs} = policies diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex index 6cd91826d..322402bdf 100644 --- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex @@ -15,7 +15,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do defp check_accept(%{host: actor_host} = _actor_info, object) do accepts = - Config.get([:mrf_simple, :accept]) + instance_list(:accept) |> MRF.subdomains_regex() cond do @@ -28,7 +28,7 @@ defp check_accept(%{host: actor_host} = _actor_info, object) do defp check_reject(%{host: actor_host} = _actor_info, object) do rejects = - Config.get([:mrf_simple, :reject]) + instance_list(:reject) |> MRF.subdomains_regex() if MRF.subdomain_match?(rejects, actor_host) do @@ -44,7 +44,7 @@ defp check_media_removal( ) when length(child_attachment) > 0 do media_removal = - Config.get([:mrf_simple, :media_removal]) + instance_list(:media_removal) |> MRF.subdomains_regex() object = @@ -69,7 +69,7 @@ defp check_media_nsfw( ) when is_map(child_object) do media_nsfw = - Config.get([:mrf_simple, :media_nsfw]) + instance_list(:media_nsfw) |> MRF.subdomains_regex() object = @@ -89,7 +89,7 @@ defp check_media_nsfw(_actor_info, object), do: {:ok, object} defp check_ftl_removal(%{host: actor_host} = _actor_info, object) do timeline_removal = - Config.get([:mrf_simple, :federated_timeline_removal]) + instance_list(:federated_timeline_removal) |> MRF.subdomains_regex() object = @@ -116,7 +116,7 @@ defp intersection(list1, list2) do defp check_followers_only(%{host: actor_host} = _actor_info, object) do followers_only = - Config.get([:mrf_simple, :followers_only]) + instance_list(:followers_only) |> MRF.subdomains_regex() object = @@ -141,7 +141,7 @@ defp check_followers_only(%{host: actor_host} = _actor_info, object) do defp check_report_removal(%{host: actor_host} = _actor_info, %{"type" => "Flag"} = object) do report_removal = - Config.get([:mrf_simple, :report_removal]) + instance_list(:report_removal) |> MRF.subdomains_regex() if MRF.subdomain_match?(report_removal, actor_host) do @@ -155,7 +155,7 @@ defp check_report_removal(_actor_info, object), do: {:ok, object} defp check_avatar_removal(%{host: actor_host} = _actor_info, %{"icon" => _icon} = object) do avatar_removal = - Config.get([:mrf_simple, :avatar_removal]) + instance_list(:avatar_removal) |> MRF.subdomains_regex() if MRF.subdomain_match?(avatar_removal, actor_host) do @@ -169,7 +169,7 @@ defp check_avatar_removal(_actor_info, object), do: {:ok, object} defp check_banner_removal(%{host: actor_host} = _actor_info, %{"image" => _image} = object) do banner_removal = - Config.get([:mrf_simple, :banner_removal]) + instance_list(:banner_removal) |> MRF.subdomains_regex() if MRF.subdomain_match?(banner_removal, actor_host) do @@ -181,12 +181,17 @@ defp check_banner_removal(%{host: actor_host} = _actor_info, %{"image" => _image defp check_banner_removal(_actor_info, object), do: {:ok, object} + defp instance_list(config_key) do + Config.get([:mrf_simple, config_key]) + |> MRF.instance_list_from_tuples() + end + @impl true def filter(%{"type" => "Delete", "actor" => actor} = object) do %{host: actor_host} = URI.parse(actor) reject_deletes = - Config.get([:mrf_simple, :reject_deletes]) + instance_list(:reject_deletes) |> MRF.subdomains_regex() if MRF.subdomain_match?(reject_deletes, actor_host) do @@ -235,14 +240,29 @@ def filter(object), do: {:ok, object} @impl true def describe do - exclusions = Config.get([:mrf, :transparency_exclusions]) + exclusions = Config.get([:mrf, :transparency_exclusions]) |> MRF.instance_list_from_tuples() + + mrf_simple_excluded = + Config.get(:mrf_simple) + |> Enum.map(fn {k, v} -> {k, Enum.reject(v, fn {v, _} -> v in exclusions end)} end) mrf_simple = - Config.get(:mrf_simple) - |> Enum.map(fn {k, v} -> {k, Enum.reject(v, fn v -> v in exclusions end)} end) + mrf_simple_excluded + |> Enum.map(fn {k, v} -> + {k, Enum.map(v, fn {instance, _} -> instance end)} + end) |> Enum.into(%{}) - {:ok, %{mrf_simple: mrf_simple}} + mrf_simple_info = + mrf_simple_excluded + |> Enum.map(fn {k, v} -> {k, Enum.reject(v, fn {_, reason} -> reason == "" end)} end) + |> Enum.reject(fn {_, v} -> v == [] end) + |> Enum.map(fn {k, l} -> + {k, l |> Enum.map(fn {i, r} -> {i, %{"reason" => r}} end) |> Enum.into(%{})} + end) + |> Enum.into(%{}) + + {:ok, %{mrf_simple: mrf_simple, mrf_simple_info: mrf_simple_info}} end @impl true @@ -253,68 +273,73 @@ def config_description do label: "MRF Simple", description: "Simple ingress policies", children: [ - %{ - key: :media_removal, - type: {:list, :string}, - description: "List of instances to strip media attachments from", - suggestions: ["example.com", "*.example.com"] - }, - %{ - key: :media_nsfw, - label: "Media NSFW", - type: {:list, :string}, - description: "List of instances to tag all media as NSFW (sensitive) from", - suggestions: ["example.com", "*.example.com"] - }, - %{ - key: :federated_timeline_removal, - type: {:list, :string}, - description: - "List of instances to remove from the Federated (aka The Whole Known Network) Timeline", - suggestions: ["example.com", "*.example.com"] - }, - %{ - key: :reject, - type: {:list, :string}, - description: "List of instances to reject activities from (except deletes)", - suggestions: ["example.com", "*.example.com"] - }, - %{ - key: :accept, - type: {:list, :string}, - description: "List of instances to only accept activities from (except deletes)", - suggestions: ["example.com", "*.example.com"] - }, - %{ - key: :followers_only, - type: {:list, :string}, - description: "Force posts from the given instances to be visible by followers only", - suggestions: ["example.com", "*.example.com"] - }, - %{ - key: :report_removal, - type: {:list, :string}, - description: "List of instances to reject reports from", - suggestions: ["example.com", "*.example.com"] - }, - %{ - key: :avatar_removal, - type: {:list, :string}, - description: "List of instances to strip avatars from", - suggestions: ["example.com", "*.example.com"] - }, - %{ - key: :banner_removal, - type: {:list, :string}, - description: "List of instances to strip banners from", - suggestions: ["example.com", "*.example.com"] - }, - %{ - key: :reject_deletes, - type: {:list, :string}, - description: "List of instances to reject deletions from", - suggestions: ["example.com", "*.example.com"] - } + %{ + key: :media_removal, + type: {:list, :tuple}, + description: + "List of instances to strip media attachments from and the reason for doing so", + suggestions: [{"example.com", "Some reason"}, {"*.example.com", "Another reason"}] + }, + %{ + key: :media_nsfw, + label: "Media NSFW", + type: {:list, :tuple}, + description: + "List of instances to tag all media as NSFW (sensitive) from and the reason for doing so", + suggestions: [{"example.com", "Some reason"}, {"*.example.com", "Another reason"}] + }, + %{ + key: :federated_timeline_removal, + type: {:list, :tuple}, + description: + "List of instances to remove from the Federated Timeline (aka The Whole Known Network) and the reason for doing so", + suggestions: [{"example.com", "Some reason"}, {"*.example.com", "Another reason"}] + }, + %{ + key: :reject, + type: {:list, :tuple}, + description: + "List of instances to reject activities (except deletes) from and the reason for doing so", + suggestions: [{"example.com", "Some reason"}, {"*.example.com", "Another reason"}] + }, + %{ + key: :accept, + type: {:list, :tuple}, + description: + "List of instances to only accept activities (except deletes) from and the reason for doing so", + suggestions: [{"example.com", "Some reason"}, {"*.example.com", "Another reason"}] + }, + %{ + key: :followers_only, + type: {:list, :tuple}, + description: + "Force posts from the given instances to be visible by followers only and the reason for doing so", + suggestions: [{"example.com", "Some reason"}, {"*.example.com", "Another reason"}] + }, + %{ + key: :report_removal, + type: {:list, :tuple}, + description: "List of instances to reject reports from and the reason for doing so", + suggestions: [{"example.com", "Some reason"}, {"*.example.com", "Another reason"}] + }, + %{ + key: :avatar_removal, + type: {:list, :tuple}, + description: "List of instances to strip avatars from and the reason for doing so", + suggestions: [{"example.com", "Some reason"}, {"*.example.com", "Another reason"}] + }, + %{ + key: :banner_removal, + type: {:list, :tuple}, + description: "List of instances to strip banners from and the reason for doing so", + suggestions: [{"example.com", "Some reason"}, {"*.example.com", "Another reason"}] + }, + %{ + key: :reject_deletes, + type: {:list, :tuple}, + description: "List of instances to reject deletions from and the reason for doing so", + suggestions: [{"example.com", "Some reason"}, {"*.example.com", "Another reason"}] + } ] } end diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index 5ab3562bf..f700c80bc 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -112,6 +112,7 @@ defp should_federate?(inbox, public) do quarantined_instances = Config.get([:instance, :quarantined_instances], []) + |> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples() |> Pleroma.Web.ActivityPub.MRF.subdomains_regex() !Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host) diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index c5aca5506..b4c0ac6fa 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -88,7 +88,19 @@ def federation do {:ok, data} = MRF.describe() data - |> Map.merge(%{quarantined_instances: quarantined}) + |> Map.merge(%{ + quarantined_instances: + quarantined + |> Enum.map(fn {instance, _reason} -> instance end) + }) + |> Map.merge(%{ + quarantined_instances_info: %{ + "quarantined_instances" => + quarantined + |> Enum.map(fn {instance, reason} -> {instance, %{"reason" => reason}} end) + |> Enum.into(%{}) + } + }) else %{} end diff --git a/priv/repo/migrations/20201005123100_simple_policy_string_to_tuple.exs b/priv/repo/migrations/20201005123100_simple_policy_string_to_tuple.exs new file mode 100644 index 000000000..77a4a7311 --- /dev/null +++ b/priv/repo/migrations/20201005123100_simple_policy_string_to_tuple.exs @@ -0,0 +1,40 @@ +defmodule Pleroma.Repo.Migrations.SimplePolicyStringToTuple do + use Ecto.Migration + + alias Pleroma.ConfigDB + + def up, do: ConfigDB.get_by_params(%{group: :pleroma, key: :mrf_simple}) |> update_to_tuples + def down, do: ConfigDB.get_by_params(%{group: :pleroma, key: :mrf_simple}) |> update_to_strings + + defp update_to_tuples(%{value: value}) do + new_value = + value + |> Enum.map(fn {k, v} -> + {k, + Enum.map(v, fn + {instance, reason} -> {instance, reason} + instance -> {instance, ""} + end)} + end) + + ConfigDB.update_or_create(%{group: :pleroma, key: :mrf_simple, value: new_value}) + end + + defp update_to_tuples(nil), do: {:ok, nil} + + defp update_to_strings(%{value: value}) do + new_value = + value + |> Enum.map(fn {k, v} -> + {k, + Enum.map(v, fn + {instance, _} -> instance + instance -> instance + end)} + end) + + ConfigDB.update_or_create(%{group: :pleroma, key: :mrf_simple, value: new_value}) + end + + defp update_to_strings(nil), do: {:ok, nil} +end diff --git a/priv/repo/migrations/20201005124600_quarantained_policy_string_to_tuple.exs b/priv/repo/migrations/20201005124600_quarantained_policy_string_to_tuple.exs new file mode 100644 index 000000000..b924e4638 --- /dev/null +++ b/priv/repo/migrations/20201005124600_quarantained_policy_string_to_tuple.exs @@ -0,0 +1,61 @@ +defmodule Pleroma.Repo.Migrations.QuarantainedStringToTuple do + use Ecto.Migration + + alias Pleroma.ConfigDB + + def up, + do: + ConfigDB.get_by_params(%{group: :pleroma, key: :instance}) + |> update_quarantined_instances_to_tuples + + def down, + do: + ConfigDB.get_by_params(%{group: :pleroma, key: :instance}) + |> update_quarantined_instances_to_strings + + defp update_quarantined_instances_to_tuples(%{value: settings}) do + settings |> List.keyfind(:quarantined_instances, 0) |> update_to_tuples + end + + defp update_quarantined_instances_to_tuples(nil), do: {:ok, nil} + + defp update_to_tuples({:quarantined_instances, instance_list}) do + new_value = + instance_list + |> Enum.map(fn + {v, r} -> {v, r} + v -> {v, ""} + end) + + ConfigDB.update_or_create(%{ + group: :pleroma, + key: :instance, + value: [quarantined_instances: new_value] + }) + end + + defp update_to_tuples(nil), do: {:ok, nil} + + defp update_quarantined_instances_to_strings(%{value: settings}) do + settings |> List.keyfind(:quarantined_instances, 0) |> update_to_strings + end + + defp update_quarantined_instances_to_strings(nil), do: {:ok, nil} + + defp update_to_strings({:quarantined_instances, instance_list}) do + new_value = + instance_list + |> Enum.map(fn + {v, _} -> v + v -> v + end) + + ConfigDB.update_or_create(%{ + group: :pleroma, + key: :instance, + value: [quarantined_instances: new_value] + }) + end + + defp update_to_strings(nil), do: {:ok, nil} +end diff --git a/priv/repo/migrations/20201005132900_transparency_exclusions_string_to_tuple.exs b/priv/repo/migrations/20201005132900_transparency_exclusions_string_to_tuple.exs new file mode 100644 index 000000000..6516083a7 --- /dev/null +++ b/priv/repo/migrations/20201005132900_transparency_exclusions_string_to_tuple.exs @@ -0,0 +1,61 @@ +defmodule Pleroma.Repo.Migrations.TransparencyExclusionsStringToTuple do + use Ecto.Migration + + alias Pleroma.ConfigDB + + def up, + do: + ConfigDB.get_by_params(%{group: :pleroma, key: :mrf}) + |> update_transparency_exclusions_instances_to_tuples + + def down, + do: + ConfigDB.get_by_params(%{group: :pleroma, key: :mrf}) + |> update_transparency_exclusions_instances_to_strings + + defp update_transparency_exclusions_instances_to_tuples(%{value: settings}) do + settings |> List.keyfind(:transparency_exclusions, 0) |> update_to_tuples + end + + defp update_transparency_exclusions_instances_to_tuples(nil), do: {:ok, nil} + + defp update_to_tuples({:transparency_exclusions, instance_list}) do + new_value = + instance_list + |> Enum.map(fn + {v, r} -> {v, r} + v -> {v, ""} + end) + + ConfigDB.update_or_create(%{ + group: :pleroma, + key: :mrf, + value: [transparency_exclusions: new_value] + }) + end + + defp update_to_tuples(nil), do: {:ok, nil} + + defp update_transparency_exclusions_instances_to_strings(%{value: settings}) do + settings |> List.keyfind(:transparency_exclusions, 0) |> update_to_strings + end + + defp update_transparency_exclusions_instances_to_strings(nil), do: {:ok, nil} + + defp update_to_strings({:transparency_exclusions, instance_list}) do + new_value = + instance_list + |> Enum.map(fn + {v, _} -> v + v -> v + end) + + ConfigDB.update_or_create(%{ + group: :pleroma, + key: :mrf, + value: [transparency_exclusions: new_value] + }) + end + + defp update_to_strings(nil), do: {:ok, nil} +end diff --git a/test/pleroma/config/deprecation_warnings_test.exs b/test/pleroma/config/deprecation_warnings_test.exs index f52629f8a..2a674d7bf 100644 --- a/test/pleroma/config/deprecation_warnings_test.exs +++ b/test/pleroma/config/deprecation_warnings_test.exs @@ -11,6 +11,183 @@ defmodule Pleroma.Config.DeprecationWarningsTest do alias Pleroma.Config alias Pleroma.Config.DeprecationWarnings + describe "simple policy tuples" do + test "gives warning when there are still strings" do + clear_config([:mrf_simple], + media_removal: ["some.removal"], + media_nsfw: ["some.nsfw"], + federated_timeline_removal: ["some.tl.removal"], + report_removal: ["some.report.removal"], + reject: ["some.reject"], + followers_only: ["some.followers.only"], + accept: ["some.accept"], + avatar_removal: ["some.avatar.removal"], + banner_removal: ["some.banner.removal"], + reject_deletes: ["some.reject.deletes"] + ) + + assert capture_log(fn -> DeprecationWarnings.check_simple_policy_tuples() end) =~ + """ + !!!DEPRECATION WARNING!!! + Your config is using strings in the SimplePolicy configuration instead of tuples. They should work for now, but you are advised to change to the new configuration to prevent possible issues later: + + ``` + config :pleroma, :mrf_simple, + media_removal: ["instance.tld"], + media_nsfw: ["instance.tld"], + federated_timeline_removal: ["instance.tld"], + report_removal: ["instance.tld"], + reject: ["instance.tld"], + followers_only: ["instance.tld"], + accept: ["instance.tld"], + avatar_removal: ["instance.tld"], + banner_removal: ["instance.tld"], + reject_deletes: ["instance.tld"] + ``` + + Is now + + + ``` + config :pleroma, :mrf_simple, + media_removal: [{"instance.tld", "Reason for media removal"}], + media_nsfw: [{"instance.tld", "Reason for media nsfw"}], + federated_timeline_removal: [{"instance.tld", "Reason for federated timeline removal"}], + report_removal: [{"instance.tld", "Reason for report removal"}], + reject: [{"instance.tld", "Reason for reject"}], + followers_only: [{"instance.tld", "Reason for followers only"}], + accept: [{"instance.tld", "Reason for accept"}], + avatar_removal: [{"instance.tld", "Reason for avatar removal"}], + banner_removal: [{"instance.tld", "Reason for banner removal"}], + reject_deletes: [{"instance.tld", "Reason for reject deletes"}] + ``` + """ + end + + test "transforms config to tuples" do + clear_config([:mrf_simple], + media_removal: ["some.removal", {"some.other.instance", "Some reason"}] + ) + + expected_config = [ + {:media_removal, [{"some.removal", ""}, {"some.other.instance", "Some reason"}]} + ] + + capture_log(fn -> DeprecationWarnings.warn() end) + + assert Config.get([:mrf_simple]) == expected_config + end + + test "doesn't give a warning with correct config" do + clear_config([:mrf_simple], + media_removal: [{"some.removal", ""}, {"some.other.instance", "Some reason"}] + ) + + assert capture_log(fn -> DeprecationWarnings.check_simple_policy_tuples() end) == "" + end + end + + describe "quarantined_instances tuples" do + test "gives warning when there are still strings" do + clear_config([:instance, :quarantined_instances], [ + {"domain.com", "some reason"}, + "somedomain.tld" + ]) + + assert capture_log(fn -> DeprecationWarnings.check_quarantined_instances_tuples() end) =~ + """ + !!!DEPRECATION WARNING!!! + Your config is using strings in the quarantined_instances configuration instead of tuples. They should work for now, but you are advised to change to the new configuration to prevent possible issues later: + + ``` + config :pleroma, :instance, + quarantined_instances: ["instance.tld"] + ``` + + Is now + + + ``` + config :pleroma, :instance, + quarantined_instances: [{"instance.tld", "Reason for quarantine"}] + ``` + """ + end + + test "transforms config to tuples" do + clear_config([:instance, :quarantined_instances], [ + {"domain.com", "some reason"}, + "some.tld" + ]) + + expected_config = [{"domain.com", "some reason"}, {"some.tld", ""}] + + capture_log(fn -> DeprecationWarnings.warn() end) + + assert Config.get([:instance, :quarantined_instances]) == expected_config + end + + test "doesn't give a warning with correct config" do + clear_config([:instance, :quarantined_instances], [ + {"domain.com", "some reason"}, + {"some.tld", ""} + ]) + + assert capture_log(fn -> DeprecationWarnings.check_quarantined_instances_tuples() end) == "" + end + end + + describe "transparency_exclusions tuples" do + test "gives warning when there are still strings" do + clear_config([:mrf, :transparency_exclusions], [ + {"domain.com", "some reason"}, + "somedomain.tld" + ]) + + assert capture_log(fn -> DeprecationWarnings.check_transparency_exclusions_tuples() end) =~ + """ + !!!DEPRECATION WARNING!!! + Your config is using strings in the transparency_exclusions configuration instead of tuples. They should work for now, but you are advised to change to the new configuration to prevent possible issues later: + + ``` + config :pleroma, :mrf, + transparency_exclusions: ["instance.tld"] + ``` + + Is now + + + ``` + config :pleroma, :mrf, + transparency_exclusions: [{"instance.tld", "Reason to exlude transparency"}] + ``` + """ + end + + test "transforms config to tuples" do + clear_config([:mrf, :transparency_exclusions], [ + {"domain.com", "some reason"}, + "some.tld" + ]) + + expected_config = [{"domain.com", "some reason"}, {"some.tld", ""}] + + capture_log(fn -> DeprecationWarnings.warn() end) + + assert Config.get([:mrf, :transparency_exclusions]) == expected_config + end + + test "doesn't give a warning with correct config" do + clear_config([:mrf, :transparency_exclusions], [ + {"domain.com", "some reason"}, + {"some.tld", ""} + ]) + + assert capture_log(fn -> DeprecationWarnings.check_transparency_exclusions_tuples() end) == + "" + end + end + test "check_old_mrf_config/0" do clear_config([:instance, :rewrite_policy], []) clear_config([:instance, :mrf_transparency], true) diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs index 5d544fe7d..f6d19be5b 100644 --- a/test/pleroma/user_test.exs +++ b/test/pleroma/user_test.exs @@ -480,7 +480,7 @@ test "it sends a welcome chat message if it is set" do ) test "it sends a welcome chat message when Simple policy applied to local instance" do - Pleroma.Config.put([:mrf_simple, :media_nsfw], ["localhost"]) + Pleroma.Config.put([:mrf_simple, :media_nsfw], [{"localhost", ""}]) welcome_user = insert(:user) Pleroma.Config.put([:welcome, :chat_message, :enabled], true) diff --git a/test/pleroma/web/activity_pub/mrf/simple_policy_test.exs b/test/pleroma/web/activity_pub/mrf/simple_policy_test.exs index d7dde62c4..3e9da13b5 100644 --- a/test/pleroma/web/activity_pub/mrf/simple_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/simple_policy_test.exs @@ -34,7 +34,7 @@ test "is empty" do end test "has a matching host" do - Config.put([:mrf_simple, :media_removal], ["remote.instance"]) + Config.put([:mrf_simple, :media_removal], [{"remote.instance", "Some reason"}]) media_message = build_media_message() local_message = build_local_message() @@ -47,7 +47,7 @@ test "has a matching host" do end test "match with wildcard domain" do - Config.put([:mrf_simple, :media_removal], ["*.remote.instance"]) + Config.put([:mrf_simple, :media_removal], [{"*.remote.instance", "Whatever reason"}]) media_message = build_media_message() local_message = build_local_message() @@ -71,7 +71,7 @@ test "is empty" do end test "has a matching host" do - Config.put([:mrf_simple, :media_nsfw], ["remote.instance"]) + Config.put([:mrf_simple, :media_nsfw], [{"remote.instance", "Whetever"}]) media_message = build_media_message() local_message = build_local_message() @@ -85,7 +85,7 @@ test "has a matching host" do end test "match with wildcard domain" do - Config.put([:mrf_simple, :media_nsfw], ["*.remote.instance"]) + Config.put([:mrf_simple, :media_nsfw], [{"*.remote.instance", "yeah yeah"}]) media_message = build_media_message() local_message = build_local_message() @@ -122,7 +122,7 @@ test "is empty" do end test "has a matching host" do - Config.put([:mrf_simple, :report_removal], ["remote.instance"]) + Config.put([:mrf_simple, :report_removal], [{"remote.instance", "muh"}]) report_message = build_report_message() local_message = build_local_message() @@ -131,7 +131,7 @@ test "has a matching host" do end test "match with wildcard domain" do - Config.put([:mrf_simple, :report_removal], ["*.remote.instance"]) + Config.put([:mrf_simple, :report_removal], [{"*.remote.instance", "suya"}]) report_message = build_report_message() local_message = build_local_message() @@ -166,7 +166,7 @@ test "has a matching host" do |> URI.parse() |> Map.fetch!(:host) - Config.put([:mrf_simple, :federated_timeline_removal], [ftl_message_actor_host]) + Config.put([:mrf_simple, :federated_timeline_removal], [{ftl_message_actor_host, "uwu"}]) local_message = build_local_message() assert {:ok, ftl_message} = SimplePolicy.filter(ftl_message) @@ -187,7 +187,10 @@ test "match with wildcard domain" do |> URI.parse() |> Map.fetch!(:host) - Config.put([:mrf_simple, :federated_timeline_removal], ["*." <> ftl_message_actor_host]) + Config.put([:mrf_simple, :federated_timeline_removal], [ + {"*." <> ftl_message_actor_host, "owo"} + ]) + local_message = build_local_message() assert {:ok, ftl_message} = SimplePolicy.filter(ftl_message) @@ -210,7 +213,9 @@ test "has a matching host but only as:Public in to" do ftl_message = Map.put(ftl_message, "cc", []) - Config.put([:mrf_simple, :federated_timeline_removal], [ftl_message_actor_host]) + Config.put([: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"] @@ -239,7 +244,7 @@ test "is empty" do end test "activity has a matching host" do - Config.put([:mrf_simple, :reject], ["remote.instance"]) + Config.put([:mrf_simple, :reject], [{"remote.instance", ""}]) remote_message = build_remote_message() @@ -247,7 +252,7 @@ test "activity has a matching host" do end test "activity matches with wildcard domain" do - Config.put([:mrf_simple, :reject], ["*.remote.instance"]) + Config.put([:mrf_simple, :reject], [{"*.remote.instance", ""}]) remote_message = build_remote_message() @@ -255,7 +260,7 @@ test "activity matches with wildcard domain" do end test "actor has a matching host" do - Config.put([:mrf_simple, :reject], ["remote.instance"]) + Config.put([:mrf_simple, :reject], [{"remote.instance", ""}]) remote_user = build_remote_user() @@ -305,7 +310,7 @@ test "has a matching host" do |> URI.parse() |> Map.fetch!(:host) - Config.put([:mrf_simple, :followers_only], [actor_domain]) + Config.put([:mrf_simple, :followers_only], [{actor_domain, ""}]) assert {:ok, new_activity} = SimplePolicy.filter(activity) assert actor.follower_address in new_activity["cc"] @@ -333,7 +338,7 @@ test "is empty" do end test "is not empty but activity doesn't have a matching host" do - Config.put([:mrf_simple, :accept], ["non.matching.remote"]) + Config.put([:mrf_simple, :accept], [{"non.matching.remote", ""}]) local_message = build_local_message() remote_message = build_remote_message() @@ -343,7 +348,7 @@ test "is not empty but activity doesn't have a matching host" do end test "activity has a matching host" do - Config.put([:mrf_simple, :accept], ["remote.instance"]) + Config.put([:mrf_simple, :accept], [{"remote.instance", ""}]) local_message = build_local_message() remote_message = build_remote_message() @@ -353,7 +358,7 @@ test "activity has a matching host" do end test "activity matches with wildcard domain" do - Config.put([:mrf_simple, :accept], ["*.remote.instance"]) + Config.put([:mrf_simple, :accept], [{"*.remote.instance", ""}]) local_message = build_local_message() remote_message = build_remote_message() @@ -363,7 +368,7 @@ test "activity matches with wildcard domain" do end test "actor has a matching host" do - Config.put([:mrf_simple, :accept], ["remote.instance"]) + Config.put([:mrf_simple, :accept], [{"remote.instance", ""}]) remote_user = build_remote_user() @@ -381,7 +386,7 @@ test "is empty" do end test "is not empty but it doesn't have a matching host" do - Config.put([:mrf_simple, :avatar_removal], ["non.matching.remote"]) + Config.put([:mrf_simple, :avatar_removal], [{"non.matching.remote", ""}]) remote_user = build_remote_user() @@ -389,7 +394,7 @@ test "is not empty but it doesn't have a matching host" do end test "has a matching host" do - Config.put([:mrf_simple, :avatar_removal], ["remote.instance"]) + Config.put([:mrf_simple, :avatar_removal], [{"remote.instance", ""}]) remote_user = build_remote_user() {:ok, filtered} = SimplePolicy.filter(remote_user) @@ -398,7 +403,7 @@ test "has a matching host" do end test "match with wildcard domain" do - Config.put([:mrf_simple, :avatar_removal], ["*.remote.instance"]) + Config.put([:mrf_simple, :avatar_removal], [{"*.remote.instance", ""}]) remote_user = build_remote_user() {:ok, filtered} = SimplePolicy.filter(remote_user) @@ -417,7 +422,7 @@ test "is empty" do end test "is not empty but it doesn't have a matching host" do - Config.put([:mrf_simple, :banner_removal], ["non.matching.remote"]) + Config.put([:mrf_simple, :banner_removal], [{"non.matching.remote", ""}]) remote_user = build_remote_user() @@ -425,7 +430,7 @@ test "is not empty but it doesn't have a matching host" do end test "has a matching host" do - Config.put([:mrf_simple, :banner_removal], ["remote.instance"]) + Config.put([:mrf_simple, :banner_removal], [{"remote.instance", ""}]) remote_user = build_remote_user() {:ok, filtered} = SimplePolicy.filter(remote_user) @@ -434,7 +439,7 @@ test "has a matching host" do end test "match with wildcard domain" do - Config.put([:mrf_simple, :banner_removal], ["*.remote.instance"]) + Config.put([:mrf_simple, :banner_removal], [{"*.remote.instance", ""}]) remote_user = build_remote_user() {:ok, filtered} = SimplePolicy.filter(remote_user) @@ -447,7 +452,7 @@ test "match with wildcard domain" do setup do: Config.put([:mrf_simple, :reject_deletes], []) test "it accepts deletions even from rejected servers" do - Config.put([:mrf_simple, :reject], ["remote.instance"]) + Config.put([:mrf_simple, :reject], [{"remote.instance", ""}]) deletion_message = build_remote_deletion_message() @@ -455,7 +460,7 @@ test "it accepts deletions even from rejected servers" do end test "it accepts deletions even from non-whitelisted servers" do - Config.put([:mrf_simple, :accept], ["non.matching.remote"]) + Config.put([:mrf_simple, :accept], [{"non.matching.remote", ""}]) deletion_message = build_remote_deletion_message() @@ -464,10 +469,10 @@ test "it accepts deletions even from non-whitelisted servers" do end describe "when :reject_deletes is not empty but it doesn't have a matching host" do - setup do: Config.put([:mrf_simple, :reject_deletes], ["non.matching.remote"]) + setup do: Config.put([:mrf_simple, :reject_deletes], [{"non.matching.remote", ""}]) test "it accepts deletions even from rejected servers" do - Config.put([:mrf_simple, :reject], ["remote.instance"]) + Config.put([:mrf_simple, :reject], [{"remote.instance", ""}]) deletion_message = build_remote_deletion_message() @@ -475,7 +480,7 @@ test "it accepts deletions even from rejected servers" do end test "it accepts deletions even from non-whitelisted servers" do - Config.put([:mrf_simple, :accept], ["non.matching.remote"]) + Config.put([:mrf_simple, :accept], [{"non.matching.remote", ""}]) deletion_message = build_remote_deletion_message() @@ -484,7 +489,7 @@ test "it accepts deletions even from non-whitelisted servers" do end describe "when :reject_deletes has a matching host" do - setup do: Config.put([:mrf_simple, :reject_deletes], ["remote.instance"]) + setup do: Config.put([:mrf_simple, :reject_deletes], [{"remote.instance", ""}]) test "it rejects the deletion" do deletion_message = build_remote_deletion_message() @@ -494,7 +499,7 @@ test "it rejects the deletion" do end describe "when :reject_deletes match with wildcard domain" do - setup do: Config.put([:mrf_simple, :reject_deletes], ["*.remote.instance"]) + setup do: Config.put([:mrf_simple, :reject_deletes], [{"*.remote.instance", ""}]) test "it rejects the deletion" do deletion_message = build_remote_deletion_message() diff --git a/test/pleroma/web/activity_pub/mrf_test.exs b/test/pleroma/web/activity_pub/mrf_test.exs index 44a9cf086..d207e83bd 100644 --- a/test/pleroma/web/activity_pub/mrf_test.exs +++ b/test/pleroma/web/activity_pub/mrf_test.exs @@ -63,6 +63,15 @@ test "matches are case-insensitive" do end end + describe "instance_list_from_tuples/1" do + test "returns a list of instances from a list of {instance, reason} tuples" do + list = [{"some.tld", "a reason"}, {"other.tld", "another reason"}] + expected = ["some.tld", "other.tld"] + + assert MRF.instance_list_from_tuples(list) == expected + end + end + describe "describe/0" do test "it works as expected with noop policy" do clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.NoOpPolicy]) diff --git a/test/pleroma/web/activity_pub/publisher_test.exs b/test/pleroma/web/activity_pub/publisher_test.exs index 3503d25b2..61dec1e26 100644 --- a/test/pleroma/web/activity_pub/publisher_test.exs +++ b/test/pleroma/web/activity_pub/publisher_test.exs @@ -267,6 +267,80 @@ test "publish to url with with different ports" do end describe "publish/2" do + test_with_mock "doesn't publish a non-public activity to quarantined instances.", + Pleroma.Web.Federator.Publisher, + [:passthrough], + [] do + Config.put([:instance, :quarantined_instances], [{"domain.com", "some reason"}]) + + follower = + insert(:user, %{ + local: false, + inbox: "https://domain.com/users/nick1/inbox", + ap_enabled: true + }) + + actor = insert(:user, follower_address: follower.ap_id) + + {:ok, _follower_one} = Pleroma.User.follow(follower, actor) + actor = refresh_record(actor) + + note_activity = + insert(:followers_only_note_activity, + user: actor, + recipients: [follower.ap_id] + ) + + res = Publisher.publish(actor, note_activity) + + assert res == :ok + + assert not called( + Pleroma.Web.Federator.Publisher.enqueue_one(Publisher, %{ + inbox: "https://domain.com/users/nick1/inbox", + actor_id: actor.id, + id: note_activity.data["id"] + }) + ) + end + + test_with_mock "Publishes a non-public activity to non-quarantined instances.", + Pleroma.Web.Federator.Publisher, + [:passthrough], + [] do + Config.put([:instance, :quarantined_instances], [{"somedomain.com", "some reason"}]) + + follower = + insert(:user, %{ + local: false, + inbox: "https://domain.com/users/nick1/inbox", + ap_enabled: true + }) + + actor = insert(:user, follower_address: follower.ap_id) + + {:ok, _follower_one} = Pleroma.User.follow(follower, actor) + actor = refresh_record(actor) + + note_activity = + insert(:followers_only_note_activity, + user: actor, + recipients: [follower.ap_id] + ) + + res = Publisher.publish(actor, note_activity) + + assert res == :ok + + assert called( + Pleroma.Web.Federator.Publisher.enqueue_one(Publisher, %{ + inbox: "https://domain.com/users/nick1/inbox", + actor_id: actor.id, + id: note_activity.data["id"] + }) + ) + end + test_with_mock "publishes an activity with BCC to all relevant peers.", Pleroma.Web.Federator.Publisher, [:passthrough], diff --git a/test/pleroma/web/node_info_test.exs b/test/pleroma/web/node_info_test.exs index 06b33607f..b2523119b 100644 --- a/test/pleroma/web/node_info_test.exs +++ b/test/pleroma/web/node_info_test.exs @@ -152,37 +152,127 @@ test "it shows default features flags", %{conn: conn} do ) end - test "it shows MRF transparency data if enabled", %{conn: conn} do - clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy]) - clear_config([:mrf, :transparency], true) + describe "Quarantined instances" do + setup do + clear_config([:mrf, :transparency], true) + quarantined_instances = [{"example.com", "reason to quarantine"}] + clear_config([:instance, :quarantined_instances], quarantined_instances) + end - simple_config = %{"reject" => ["example.com"]} - clear_config(:mrf_simple, simple_config) + test "shows quarantined instances data if enabled", %{conn: conn} do + expected_config = ["example.com"] - response = - conn - |> get("/nodeinfo/2.1.json") - |> json_response(:ok) + response = + conn + |> get("/nodeinfo/2.1.json") + |> json_response(:ok) - assert response["metadata"]["federation"]["mrf_simple"] == simple_config + assert response["metadata"]["federation"]["quarantined_instances"] == expected_config + end + + test "shows extra information in the quarantined_info field for relevant entries", %{ + conn: conn + } do + clear_config([:mrf, :transparency], true) + + expected_config = %{ + "quarantined_instances" => %{ + "example.com" => %{"reason" => "reason to quarantine"} + } + } + + response = + conn + |> get("/nodeinfo/2.1.json") + |> json_response(:ok) + + assert response["metadata"]["federation"]["quarantined_instances_info"] == expected_config + end end - test "it performs exclusions from MRF transparency data if configured", %{conn: conn} do - clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy]) - clear_config([:mrf, :transparency], true) - clear_config([:mrf, :transparency_exclusions], ["other.site"]) + describe "MRF SimplePolicy" do + setup do + clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy]) + clear_config([:mrf, :transparency], true) + end - simple_config = %{"reject" => ["example.com", "other.site"]} - clear_config(:mrf_simple, simple_config) + test "shows MRF transparency data if enabled", %{conn: conn} do + simple_config = %{"reject" => [{"example.com", ""}]} + clear_config(:mrf_simple, simple_config) - expected_config = %{"reject" => ["example.com"]} + expected_config = %{"reject" => ["example.com"]} - response = - conn - |> get("/nodeinfo/2.1.json") - |> json_response(:ok) + response = + conn + |> get("/nodeinfo/2.1.json") + |> json_response(:ok) - assert response["metadata"]["federation"]["mrf_simple"] == expected_config - assert response["metadata"]["federation"]["exclusions"] == true + assert response["metadata"]["federation"]["mrf_simple"] == expected_config + end + + test "performs exclusions from MRF transparency data if configured", %{conn: conn} do + clear_config([:mrf, :transparency_exclusions], [ + {"other.site", "We don't want them to know"} + ]) + + simple_config = %{"reject" => [{"example.com", ""}, {"other.site", ""}]} + clear_config(:mrf_simple, simple_config) + + expected_config = %{"reject" => ["example.com"]} + + response = + conn + |> get("/nodeinfo/2.1.json") + |> json_response(:ok) + + assert response["metadata"]["federation"]["mrf_simple"] == expected_config + assert response["metadata"]["federation"]["exclusions"] == true + end + + test "shows extra information in the mrf_simple_info field for relevant entries", %{ + conn: conn + } do + simple_config = %{ + media_removal: [{"no.media", "LEEWWWDD >//<"}], + media_nsfw: [], + federated_timeline_removal: [{"no.ftl", ""}], + report_removal: [], + reject: [ + {"example.instance", "Some reason"}, + {"uwu.owo", "awoo to much"}, + {"no.reason", ""} + ], + followers_only: [], + accept: [], + avatar_removal: [], + banner_removal: [], + reject_deletes: [ + {"peak.me", "I want to peak at what they don't want me to see, eheh"} + ] + } + + clear_config(:mrf_simple, simple_config) + + clear_config([:mrf, :transparency_exclusions], [ + {"peak.me", "I don't want them to know"} + ]) + + expected_config = %{ + "media_removal" => %{ + "no.media" => %{"reason" => "LEEWWWDD >//<"} + }, + "reject" => %{ + "example.instance" => %{"reason" => "Some reason"}, + "uwu.owo" => %{"reason" => "awoo to much"} + } + } + + response = + conn + |> get("/nodeinfo/2.1.json") + |> json_response(:ok) + + assert response["metadata"]["federation"]["mrf_simple_info"] == expected_config + end end end diff --git a/test/support/factory.ex b/test/support/factory.ex index 8eb07dc3c..bbb04837a 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -104,6 +104,11 @@ def note_factory(attrs \\ %{}) do } end + def followers_only_note_factory(attrs \\ %{}) do + %Pleroma.Object{data: data} = note_factory(attrs) + %Pleroma.Object{data: Map.merge(data, %{"to" => [data["actor"] <> "/followers"]})} + end + def audio_factory(attrs \\ %{}) do text = sequence(:text, &"lain radio episode #{&1}") @@ -190,6 +195,33 @@ def direct_note_activity_factory do } end + def followers_only_note_activity_factory(attrs \\ %{}) do + user = attrs[:user] || insert(:user) + note = insert(:followers_only_note, user: user) + + data_attrs = attrs[:data_attrs] || %{} + attrs = Map.drop(attrs, [:user, :note, :data_attrs]) + + data = + %{ + "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(), + "type" => "Create", + "actor" => note.data["actor"], + "to" => note.data["to"], + "object" => note.data, + "published" => DateTime.utc_now() |> DateTime.to_iso8601(), + "context" => note.data["context"] + } + |> Map.merge(data_attrs) + + %Pleroma.Activity{ + data: data, + actor: data["actor"], + recipients: data["to"] + } + |> Map.merge(attrs) + end + def note_activity_factory(attrs \\ %{}) do user = attrs[:user] || insert(:user) note = attrs[:note] || insert(:note, user: user)