Merge branch 'feature/2222-config-descriptions-for-custom-modules' into 'develop'

Config descriptions for custom MRF policies

Closes #2222

See merge request pleroma/pleroma!3128
This commit is contained in:
feld 2020-11-11 13:48:03 +00:00
commit 3cd7ea693f
16 changed files with 454 additions and 339 deletions

View file

@ -1,5 +1,4 @@
use Mix.Config
alias Pleroma.Docs.Generator
websocket_config = [
path: "/websocket",
@ -1555,298 +1554,6 @@
}
]
},
%{
group: :pleroma,
key: :mrf,
tab: :mrf,
label: "MRF",
type: :group,
description: "General MRF settings",
children: [
%{
key: :policies,
type: [:module, {:list, :module}],
description:
"A list of MRF policies enabled. Module names are shortened (removed leading `Pleroma.Web.ActivityPub.MRF.` part), but on adding custom module you need to use full name.",
suggestions: {:list_behaviour_implementations, Pleroma.Web.ActivityPub.MRF}
},
%{
key: :transparency,
label: "MRF transparency",
type: :boolean,
description:
"Make the content of your Message Rewrite Facility settings public (via nodeinfo)"
},
%{
key: :transparency_exclusions,
label: "MRF transparency exclusions",
type: {:list, :string},
description:
"Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.",
suggestions: [
"exclusion.com"
]
}
]
},
%{
group: :pleroma,
key: :mrf_simple,
tab: :mrf,
related_policy: "Pleroma.Web.ActivityPub.MRF.SimplePolicy",
label: "MRF Simple",
type: :group,
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"]
}
]
},
%{
group: :pleroma,
key: :mrf_activity_expiration,
tab: :mrf,
related_policy: "Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy",
label: "MRF Activity Expiration Policy",
type: :group,
description: "Adds automatic expiration to all local activities",
children: [
%{
key: :days,
type: :integer,
description: "Default global expiration time for all local activities (in days)",
suggestions: [90, 365]
}
]
},
%{
group: :pleroma,
key: :mrf_subchain,
tab: :mrf,
related_policy: "Pleroma.Web.ActivityPub.MRF.SubchainPolicy",
label: "MRF Subchain",
type: :group,
description:
"This policy processes messages through an alternate pipeline when a given message matches certain criteria." <>
" All criteria are configured as a map of regular expressions to lists of policy modules.",
children: [
%{
key: :match_actor,
type: {:map, {:list, :string}},
description: "Matches a series of regular expressions against the actor field",
suggestions: [
%{
~r/https:\/\/example.com/s => [Pleroma.Web.ActivityPub.MRF.DropPolicy]
}
]
}
]
},
%{
group: :pleroma,
key: :mrf_rejectnonpublic,
tab: :mrf,
related_policy: "Pleroma.Web.ActivityPub.MRF.RejectNonPublic",
description: "RejectNonPublic drops posts with non-public visibility settings.",
label: "MRF Reject Non Public",
type: :group,
children: [
%{
key: :allow_followersonly,
label: "Allow followers-only",
type: :boolean,
description: "Whether to allow followers-only posts"
},
%{
key: :allow_direct,
type: :boolean,
description: "Whether to allow direct messages"
}
]
},
%{
group: :pleroma,
key: :mrf_hellthread,
tab: :mrf,
related_policy: "Pleroma.Web.ActivityPub.MRF.HellthreadPolicy",
label: "MRF Hellthread",
type: :group,
description: "Block messages with excessive user mentions",
children: [
%{
key: :delist_threshold,
type: :integer,
description:
"Number of mentioned users after which the message gets removed from timelines and" <>
"disables notifications. Set to 0 to disable.",
suggestions: [10]
},
%{
key: :reject_threshold,
type: :integer,
description:
"Number of mentioned users after which the messaged gets rejected. Set to 0 to disable.",
suggestions: [20]
}
]
},
%{
group: :pleroma,
key: :mrf_keyword,
tab: :mrf,
related_policy: "Pleroma.Web.ActivityPub.MRF.KeywordPolicy",
label: "MRF Keyword",
type: :group,
description:
"Reject or Word-Replace messages matching a keyword or [Regex](https://hexdocs.pm/elixir/Regex.html).",
children: [
%{
key: :reject,
type: {:list, :string},
description: """
A list of patterns which result in message being rejected.
Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
""",
suggestions: ["foo", ~r/foo/iu]
},
%{
key: :federated_timeline_removal,
type: {:list, :string},
description: """
A list of patterns which result in message being removed from federated timelines (a.k.a unlisted).
Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
""",
suggestions: ["foo", ~r/foo/iu]
},
%{
key: :replace,
type: {:list, :tuple},
description: """
**Pattern**: a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
**Replacement**: a string. Leaving the field empty is permitted.
"""
}
]
},
%{
group: :pleroma,
key: :mrf_mention,
tab: :mrf,
related_policy: "Pleroma.Web.ActivityPub.MRF.MentionPolicy",
label: "MRF Mention",
type: :group,
description: "Block messages which mention a specific user",
children: [
%{
key: :actors,
type: {:list, :string},
description: "A list of actors for which any post mentioning them will be dropped",
suggestions: ["actor1", "actor2"]
}
]
},
%{
group: :pleroma,
key: :mrf_vocabulary,
tab: :mrf,
related_policy: "Pleroma.Web.ActivityPub.MRF.VocabularyPolicy",
label: "MRF Vocabulary",
type: :group,
description: "Filter messages which belong to certain activity vocabularies",
children: [
%{
key: :accept,
type: {:list, :string},
description:
"A list of ActivityStreams terms to accept. If empty, all supported messages are accepted.",
suggestions: ["Create", "Follow", "Mention", "Announce", "Like"]
},
%{
key: :reject,
type: {:list, :string},
description:
"A list of ActivityStreams terms to reject. If empty, no messages are rejected.",
suggestions: ["Create", "Follow", "Mention", "Announce", "Like"]
}
]
},
# %{
# group: :pleroma,
# key: :mrf_user_allowlist,
# tab: :mrf,
# related_policy: "Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy",
# type: :map,
# description:
# "The keys in this section are the domain names that the policy should apply to." <>
# " Each key should be assigned a list of users that should be allowed through by their ActivityPub ID",
# suggestions: [
# %{"example.org" => ["https://example.org/users/admin"]}
# ]
# ]
# },
%{
group: :pleroma,
key: :media_proxy,
@ -3159,22 +2866,6 @@
}
]
},
%{
group: :pleroma,
key: :mrf_normalize_markup,
tab: :mrf,
related_policy: "Pleroma.Web.ActivityPub.MRF.NormalizeMarkup",
label: "MRF Normalize Markup",
description: "MRF NormalizeMarkup settings. Scrub configured hypertext markup.",
type: :group,
children: [
%{
key: :scrub_policy,
type: :module,
suggestions: [Pleroma.HTML.Scrubber.Default]
}
]
},
%{
group: :pleroma,
key: Pleroma.User,
@ -3364,33 +3055,6 @@
}
]
},
%{
group: :pleroma,
key: :mrf_object_age,
tab: :mrf,
related_policy: "Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy",
label: "MRF Object Age",
type: :group,
description:
"Rejects or delists posts based on their timestamp deviance from your server's clock.",
children: [
%{
key: :threshold,
type: :integer,
description: "Required age (in seconds) of a post before actions are taken.",
suggestions: [172_800]
},
%{
key: :actions,
type: {:list, :atom},
description:
"A list of actions to apply to the post. `:delist` removes the post from public timelines; " <>
"`:strip_followers` removes followers from the ActivityPub recipient list ensuring they won't be delivered to home timelines; " <>
"`:reject` rejects the message entirely",
suggestions: [:delist, :strip_followers, :reject]
}
]
},
%{
group: :pleroma,
key: :modules,

View file

@ -11,7 +11,11 @@ defmodule Pleroma.Docs.JSON do
@spec compile :: :ok
def compile do
:persistent_term.put(@term, Pleroma.Docs.Generator.convert_to_strings(@raw_descriptions))
descriptions =
Pleroma.Web.ActivityPub.MRF.config_descriptions()
|> Enum.reduce(@raw_descriptions, fn description, acc -> [description | acc] end)
:persistent_term.put(@term, Pleroma.Docs.Generator.convert_to_strings(descriptions))
end
@spec compiled_descriptions :: Map.t()

View file

@ -3,7 +3,62 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF do
require Logger
@mrf_config_descriptions [
%{
group: :pleroma,
key: :mrf,
tab: :mrf,
label: "MRF",
type: :group,
description: "General MRF settings",
children: [
%{
key: :policies,
type: [:module, {:list, :module}],
description:
"A list of MRF policies enabled. Module names are shortened (removed leading `Pleroma.Web.ActivityPub.MRF.` part), but on adding custom module you need to use full name.",
suggestions: {:list_behaviour_implementations, Pleroma.Web.ActivityPub.MRF}
},
%{
key: :transparency,
label: "MRF transparency",
type: :boolean,
description:
"Make the content of your Message Rewrite Facility settings public (via nodeinfo)"
},
%{
key: :transparency_exclusions,
label: "MRF transparency exclusions",
type: {:list, :string},
description:
"Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.",
suggestions: [
"exclusion.com"
]
}
]
}
]
@default_description %{
label: "",
description: ""
}
@required_description_keys [:key, :related_policy]
@callback filter(Map.t()) :: {:ok | :reject, Map.t()}
@callback describe() :: {:ok | :error, Map.t()}
@callback config_description() :: %{
optional(:children) => [map()],
key: atom(),
related_policy: String.t(),
label: String.t(),
description: String.t()
}
@optional_callbacks config_description: 0
def filter(policies, %{} = message) do
policies
@ -51,8 +106,6 @@ def subdomain_match?(domains, host) do
Enum.any?(domains, fn domain -> Regex.match?(domain, host) end)
end
@callback describe() :: {:ok | :error, Map.t()}
def describe(policies) do
{:ok, policy_configs} =
policies
@ -82,4 +135,41 @@ def describe(policies) do
end
def describe, do: get_policies() |> describe()
def config_descriptions do
Pleroma.Web.ActivityPub.MRF
|> Pleroma.Docs.Generator.list_behaviour_implementations()
|> config_descriptions()
end
def config_descriptions(policies) do
Enum.reduce(policies, @mrf_config_descriptions, fn policy, acc ->
if function_exported?(policy, :config_description, 0) do
description =
@default_description
|> Map.merge(policy.config_description)
|> Map.put(:group, :pleroma)
|> Map.put(:tab, :mrf)
|> Map.put(:type, :group)
if Enum.all?(@required_description_keys, &Map.has_key?(description, &1)) do
[description | acc]
else
Logger.warn(
"#{policy} config description doesn't have one or all required keys #{
inspect(@required_description_keys)
}"
)
acc
end
else
Logger.info(
"#{policy} is excluded from config descriptions, because does not implement `config_description/0` method."
)
acc
end
end)
end
end

View file

@ -40,4 +40,22 @@ defp maybe_add_expiration(activity) do
_ -> Map.put(activity, "expires_at", expires_at)
end
end
@impl true
def config_description do
%{
key: :mrf_activity_expiration,
related_policy: "Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy",
label: "MRF Activity Expiration Policy",
description: "Adds automatic expiration to all local activities",
children: [
%{
key: :days,
type: :integer,
description: "Default global expiration time for all local activities (in days)",
suggestions: [90, 365]
}
]
}
end
end

View file

@ -97,4 +97,31 @@ def filter(message), do: {:ok, message}
@impl true
def describe,
do: {:ok, %{mrf_hellthread: Pleroma.Config.get(:mrf_hellthread) |> Enum.into(%{})}}
@impl true
def config_description do
%{
key: :mrf_hellthread,
related_policy: "Pleroma.Web.ActivityPub.MRF.HellthreadPolicy",
label: "MRF Hellthread",
description: "Block messages with excessive user mentions",
children: [
%{
key: :delist_threshold,
type: :integer,
description:
"Number of mentioned users after which the message gets removed from timelines and" <>
"disables notifications. Set to 0 to disable.",
suggestions: [10]
},
%{
key: :reject_threshold,
type: :integer,
description:
"Number of mentioned users after which the messaged gets rejected. Set to 0 to disable.",
suggestions: [20]
}
]
}
end
end

View file

@ -126,4 +126,46 @@ def describe do
{:ok, %{mrf_keyword: mrf_keyword}}
end
@impl true
def config_description do
%{
key: :mrf_keyword,
related_policy: "Pleroma.Web.ActivityPub.MRF.KeywordPolicy",
label: "MRF Keyword",
description:
"Reject or Word-Replace messages matching a keyword or [Regex](https://hexdocs.pm/elixir/Regex.html).",
children: [
%{
key: :reject,
type: {:list, :string},
description: """
A list of patterns which result in message being rejected.
Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
""",
suggestions: ["foo", ~r/foo/iu]
},
%{
key: :federated_timeline_removal,
type: {:list, :string},
description: """
A list of patterns which result in message being removed from federated timelines (a.k.a unlisted).
Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
""",
suggestions: ["foo", ~r/foo/iu]
},
%{
key: :replace,
type: {:list, :tuple},
description: """
**Pattern**: a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
**Replacement**: a string. Leaving the field empty is permitted.
"""
}
]
}
end
end

View file

@ -25,4 +25,22 @@ def filter(message), do: {:ok, message}
@impl true
def describe, do: {:ok, %{}}
@impl true
def config_description do
%{
key: :mrf_mention,
related_policy: "Pleroma.Web.ActivityPub.MRF.MentionPolicy",
label: "MRF Mention",
description: "Block messages which mention a specific user",
children: [
%{
key: :actors,
type: {:list, :string},
description: "A list of actors for which any post mentioning them will be dropped",
suggestions: ["actor1", "actor2"]
}
]
}
end
end

View file

@ -8,6 +8,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkup do
@behaviour Pleroma.Web.ActivityPub.MRF
@impl true
def filter(%{"type" => "Create", "object" => child_object} = object) do
scrub_policy = Pleroma.Config.get([:mrf_normalize_markup, :scrub_policy])
@ -22,5 +23,23 @@ def filter(%{"type" => "Create", "object" => child_object} = object) do
def filter(object), do: {:ok, object}
@impl true
def describe, do: {:ok, %{}}
@impl true
def config_description do
%{
key: :mrf_normalize_markup,
related_policy: "Pleroma.Web.ActivityPub.MRF.NormalizeMarkup",
label: "MRF Normalize Markup",
description: "MRF NormalizeMarkup settings. Scrub configured hypertext markup.",
children: [
%{
key: :scrub_policy,
type: :module,
suggestions: [Pleroma.HTML.Scrubber.Default]
}
]
}
end
end

View file

@ -106,4 +106,32 @@ def describe do
{:ok, %{mrf_object_age: mrf_object_age}}
end
@impl true
def config_description do
%{
key: :mrf_object_age,
related_policy: "Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy",
label: "MRF Object Age",
description:
"Rejects or delists posts based on their timestamp deviance from your server's clock.",
children: [
%{
key: :threshold,
type: :integer,
description: "Required age (in seconds) of a post before actions are taken.",
suggestions: [172_800]
},
%{
key: :actions,
type: {:list, :atom},
description:
"A list of actions to apply to the post. `:delist` removes the post from public timelines; " <>
"`:strip_followers` removes followers from the ActivityPub recipient list ensuring they won't be delivered to home timelines; " <>
"`:reject` rejects the message entirely",
suggestions: [:delist, :strip_followers, :reject]
}
]
}
end
end

View file

@ -48,4 +48,27 @@ def filter(object), do: {:ok, object}
@impl true
def describe,
do: {:ok, %{mrf_rejectnonpublic: Config.get(:mrf_rejectnonpublic) |> Enum.into(%{})}}
@impl true
def config_description do
%{
key: :mrf_rejectnonpublic,
related_policy: "Pleroma.Web.ActivityPub.MRF.RejectNonPublic",
description: "RejectNonPublic drops posts with non-public visibility settings.",
label: "MRF Reject Non Public",
children: [
%{
key: :allow_followersonly,
label: "Allow followers-only",
type: :boolean,
description: "Whether to allow followers-only posts"
},
%{
key: :allow_direct,
type: :boolean,
description: "Whether to allow direct messages"
}
]
}
end
end

View file

@ -244,4 +244,78 @@ def describe do
{:ok, %{mrf_simple: mrf_simple}}
end
@impl true
def config_description do
%{
key: :mrf_simple,
related_policy: "Pleroma.Web.ActivityPub.MRF.SimplePolicy",
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"]
}
]
}
end
end

View file

@ -39,4 +39,28 @@ def filter(message), do: {:ok, message}
@impl true
def describe, do: {:ok, %{}}
@impl true
def config_description do
%{
key: :mrf_subchain,
related_policy: "Pleroma.Web.ActivityPub.MRF.SubchainPolicy",
label: "MRF Subchain",
description:
"This policy processes messages through an alternate pipeline when a given message matches certain criteria." <>
" All criteria are configured as a map of regular expressions to lists of policy modules.",
children: [
%{
key: :match_actor,
type: {:map, {:list, :string}},
description: "Matches a series of regular expressions against the actor field",
suggestions: [
%{
~r/https:\/\/example.com/s => [Pleroma.Web.ActivityPub.MRF.DropPolicy]
}
]
}
]
}
end
end

View file

@ -41,4 +41,25 @@ def describe do
{:ok, %{mrf_user_allowlist: mrf_user_allowlist}}
end
# TODO: change way of getting settings on `lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex:18` to use `hosts` subkey
# @impl true
# def config_description do
# %{
# key: :mrf_user_allowlist,
# related_policy: "Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy",
# description: "Accept-list of users from specified instances",
# children: [
# %{
# key: :hosts,
# type: :map,
# description:
# "The keys in this section are the domain names that the policy should apply to." <>
# " Each key should be assigned a list of users that should be allowed " <>
# "through by their ActivityPub ID",
# suggestions: [%{"example.org" => ["https://example.org/users/admin"]}]
# }
# ]
# }
# end
end

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicy do
@behaviour Pleroma.Web.ActivityPub.MRF
@impl true
def filter(%{"type" => "Undo", "object" => child_message} = message) do
with {:ok, _} <- filter(child_message) do
{:ok, message}
@ -36,6 +37,33 @@ def filter(%{"type" => message_type} = message) do
def filter(message), do: {:ok, message}
@impl true
def describe,
do: {:ok, %{mrf_vocabulary: Pleroma.Config.get(:mrf_vocabulary) |> Enum.into(%{})}}
@impl true
def config_description do
%{
key: :mrf_vocabulary,
related_policy: "Pleroma.Web.ActivityPub.MRF.VocabularyPolicy",
label: "MRF Vocabulary",
description: "Filter messages which belong to certain activity vocabularies",
children: [
%{
key: :accept,
type: {:list, :string},
description:
"A list of ActivityStreams terms to accept. If empty, all supported messages are accepted.",
suggestions: ["Create", "Follow", "Mention", "Announce", "Like"]
},
%{
key: :reject,
type: {:list, :string},
description:
"A list of ActivityStreams terms to reject. If empty, no messages are rejected.",
suggestions: ["Create", "Follow", "Mention", "Announce", "Like"]
}
]
}
end
end

19
test/fixtures/modules/good_mrf.ex vendored Normal file
View file

@ -0,0 +1,19 @@
defmodule Fixtures.Modules.GoodMRF do
@behaviour Pleroma.Web.ActivityPub.MRF
@impl true
def filter(a), do: {:ok, a}
@impl true
def describe, do: %{}
@impl true
def config_description do
%{
key: :good_mrf,
related_policy: "Fixtures.Modules.GoodMRF",
label: "Good MRF",
description: "Some description"
}
end
end

View file

@ -87,4 +87,20 @@ test "it works as expected with mock policy" do
{:ok, ^expected} = MRF.describe()
end
end
test "config_descriptions/0" do
descriptions = MRF.config_descriptions()
good_mrf = Enum.find(descriptions, fn %{key: key} -> key == :good_mrf end)
assert good_mrf == %{
key: :good_mrf,
related_policy: "Fixtures.Modules.GoodMRF",
label: "Good MRF",
description: "Some description",
group: :pleroma,
tab: :mrf,
type: :group
}
end
end