@@ -4,10 +4,13 @@ All notable changes to this project will be documented in this file. | |||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). | ||||
## [unreleased] | ## [unreleased] | ||||
### Changed | ### Changed | ||||
- MFR policy to set global expiration for all local Create activities | - MFR policy to set global expiration for all local Create activities | ||||
- OGP rich media parser merged with TwitterCard | - OGP rich media parser merged with TwitterCard | ||||
- Configuration: `rewrite_policy` renamed to `policies` and moved from `instance` to `mrf` group. Old config namespace is deprecated. | |||||
- Configuration: `mrf_transparency` renamed to `transparency` and moved from `instance` to `mrf` group. Old config namespace is deprecated. | |||||
- Configuration: `mrf_transparency_exclusions` renamed to `transparency_exclusions` and moved from `instance` to `mrf` group. Old config namespace is deprecated. | |||||
<details> | <details> | ||||
<summary>API Changes</summary> | <summary>API Changes</summary> | ||||
- **Breaking:** Emoji API: changed methods and renamed routes. | - **Breaking:** Emoji API: changed methods and renamed routes. | ||||
@@ -209,7 +209,6 @@ config :pleroma, :instance, | |||||
Pleroma.Web.ActivityPub.Publisher | Pleroma.Web.ActivityPub.Publisher | ||||
], | ], | ||||
allow_relay: true, | allow_relay: true, | ||||
rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy, | |||||
public: true, | public: true, | ||||
quarantined_instances: [], | quarantined_instances: [], | ||||
managed_config: true, | managed_config: true, | ||||
@@ -220,8 +219,6 @@ config :pleroma, :instance, | |||||
"text/markdown", | "text/markdown", | ||||
"text/bbcode" | "text/bbcode" | ||||
], | ], | ||||
mrf_transparency: true, | |||||
mrf_transparency_exclusions: [], | |||||
autofollowed_nicknames: [], | autofollowed_nicknames: [], | ||||
max_pinned_statuses: 1, | max_pinned_statuses: 1, | ||||
attachment_links: false, | attachment_links: false, | ||||
@@ -685,6 +682,11 @@ config :pleroma, :restrict_unauthenticated, | |||||
config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: false | config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: false | ||||
config :pleroma, :mrf, | |||||
policies: Pleroma.Web.ActivityPub.MRF.NoOpPolicy, | |||||
transparency: true, | |||||
transparency_exclusions: [] | |||||
# Import environment specific config. This must remain at the bottom | # Import environment specific config. This must remain at the bottom | ||||
# of this file so it overrides the configuration defined above. | # of this file so it overrides the configuration defined above. | ||||
import_config "#{Mix.env()}.exs" | import_config "#{Mix.env()}.exs" |
@@ -690,17 +690,6 @@ config :pleroma, :config_description, [ | |||||
description: "Enable Pleroma's Relay, which makes it possible to follow a whole instance" | description: "Enable Pleroma's Relay, which makes it possible to follow a whole instance" | ||||
}, | }, | ||||
%{ | %{ | ||||
key: :rewrite_policy, | |||||
type: [:module, {:list, :module}], | |||||
description: | |||||
"A list of enabled MRF policies. Module names are shortened (removed leading `Pleroma.Web.ActivityPub.MRF.` part), but on adding custom module you need to use full name.", | |||||
suggestions: | |||||
Generator.list_modules_in_dir( | |||||
"lib/pleroma/web/activity_pub/mrf", | |||||
"Elixir.Pleroma.Web.ActivityPub.MRF." | |||||
) | |||||
}, | |||||
%{ | |||||
key: :public, | key: :public, | ||||
type: :boolean, | type: :boolean, | ||||
description: | description: | ||||
@@ -743,23 +732,6 @@ config :pleroma, :config_description, [ | |||||
] | ] | ||||
}, | }, | ||||
%{ | %{ | ||||
key: :mrf_transparency, | |||||
label: "MRF transparency", | |||||
type: :boolean, | |||||
description: | |||||
"Make the content of your Message Rewrite Facility settings public (via nodeinfo)" | |||||
}, | |||||
%{ | |||||
key: :mrf_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" | |||||
] | |||||
}, | |||||
%{ | |||||
key: :extended_nickname_format, | key: :extended_nickname_format, | ||||
type: :boolean, | type: :boolean, | ||||
description: | description: | ||||
@@ -3325,5 +3297,41 @@ config :pleroma, :config_description, [ | |||||
suggestions: [false] | suggestions: [false] | ||||
} | } | ||||
] | ] | ||||
}, | |||||
%{ | |||||
group: :pleroma, | |||||
key: :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: | |||||
Generator.list_modules_in_dir( | |||||
"lib/pleroma/web/activity_pub/mrf", | |||||
"Elixir.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" | |||||
] | |||||
} | |||||
] | |||||
} | } | ||||
] | ] |
@@ -36,26 +36,10 @@ To add configuration to your config file, you can copy it from the base config. | |||||
* `federation_incoming_replies_max_depth`: Max. depth of reply-to activities fetching on incoming federation, to prevent out-of-memory situations while fetching very long threads. If set to `nil`, threads of any depth will be fetched. Lower this value if you experience out-of-memory crashes. | * `federation_incoming_replies_max_depth`: Max. depth of reply-to activities fetching on incoming federation, to prevent out-of-memory situations while fetching very long threads. If set to `nil`, threads of any depth will be fetched. Lower this value if you experience out-of-memory crashes. | ||||
* `federation_reachability_timeout_days`: Timeout (in days) of each external federation target being unreachable prior to pausing federating to it. | * `federation_reachability_timeout_days`: Timeout (in days) of each external federation target being unreachable prior to pausing federating to it. | ||||
* `allow_relay`: Enable Pleroma’s Relay, which makes it possible to follow a whole instance. | * `allow_relay`: Enable Pleroma’s Relay, which makes it possible to follow a whole instance. | ||||
* `rewrite_policy`: Message Rewrite Policy, either one or a list. Here are the ones available by default: | |||||
* `Pleroma.Web.ActivityPub.MRF.NoOpPolicy`: Doesn’t modify activities (default). | |||||
* `Pleroma.Web.ActivityPub.MRF.DropPolicy`: Drops all activities. It generally doesn’t makes sense to use in production. | |||||
* `Pleroma.Web.ActivityPub.MRF.SimplePolicy`: Restrict the visibility of activities from certain instances (See [`:mrf_simple`](#mrf_simple)). | |||||
* `Pleroma.Web.ActivityPub.MRF.TagPolicy`: Applies policies to individual users based on tags, which can be set using pleroma-fe/admin-fe/any other app that supports Pleroma Admin API. For example it allows marking posts from individual users nsfw (sensitive). | |||||
* `Pleroma.Web.ActivityPub.MRF.SubchainPolicy`: Selectively runs other MRF policies when messages match (See [`:mrf_subchain`](#mrf_subchain)). | |||||
* `Pleroma.Web.ActivityPub.MRF.RejectNonPublic`: Drops posts with non-public visibility settings (See [`:mrf_rejectnonpublic`](#mrf_rejectnonpublic)). | |||||
* `Pleroma.Web.ActivityPub.MRF.EnsureRePrepended`: Rewrites posts to ensure that replies to posts with subjects do not have an identical subject and instead begin with re:. | |||||
* `Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy`: Rejects posts from likely spambots by rejecting posts from new users that contain links. | |||||
* `Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`: Crawls attachments using their MediaProxy URLs so that the MediaProxy cache is primed. | |||||
* `Pleroma.Web.ActivityPub.MRF.MentionPolicy`: Drops posts mentioning configurable users. (See [`:mrf_mention`](#mrf_mention)). | |||||
* `Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`: Restricts activities to a configured set of vocabulary. (See [`:mrf_vocabulary`](#mrf_vocabulary)). | |||||
* `Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy`: Rejects or delists posts based on their age when received. (See [`:mrf_object_age`](#mrf_object_age)). | |||||
* `Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy`: Adds expiration to all local Create activities (see [`:mrf_activity_expiration`](#mrf_activity_expiration)). | |||||
* `public`: Makes the client API in authenticated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network. | * `public`: Makes the client API in authenticated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network. | ||||
* `quarantined_instances`: List of ActivityPub instances where private(DMs, followers-only) activities will not be send. | * `quarantined_instances`: List of ActivityPub instances where private(DMs, followers-only) activities will not be send. | ||||
* `managed_config`: Whenether the config for pleroma-fe is configured in [:frontend_configurations](#frontend_configurations) or in ``static/config.json``. | * `managed_config`: Whenether the config for pleroma-fe is configured in [:frontend_configurations](#frontend_configurations) or in ``static/config.json``. | ||||
* `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML). | * `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML). | ||||
* `mrf_transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo). | |||||
* `mrf_transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value. | |||||
* `extended_nickname_format`: Set to `true` to use extended local nicknames format (allows underscores/dashes). This will break federation with | * `extended_nickname_format`: Set to `true` to use extended local nicknames format (allows underscores/dashes). This will break federation with | ||||
older software for theses nicknames. | older software for theses nicknames. | ||||
* `max_pinned_statuses`: The maximum number of pinned statuses. `0` will disable the feature. | * `max_pinned_statuses`: The maximum number of pinned statuses. `0` will disable the feature. | ||||
@@ -78,11 +62,30 @@ To add configuration to your config file, you can copy it from the base config. | |||||
* `external_user_synchronization`: Enabling following/followers counters synchronization for external users. | * `external_user_synchronization`: Enabling following/followers counters synchronization for external users. | ||||
* `cleanup_attachments`: Remove attachments along with statuses. Does not affect duplicate files and attachments without status. Enabling this will increase load to database when deleting statuses on larger instances. | * `cleanup_attachments`: Remove attachments along with statuses. Does not affect duplicate files and attachments without status. Enabling this will increase load to database when deleting statuses on larger instances. | ||||
## Message rewrite facility | |||||
### :mrf | |||||
* `policies`: Message Rewrite Policy, either one or a list. Here are the ones available by default: | |||||
* `Pleroma.Web.ActivityPub.MRF.NoOpPolicy`: Doesn’t modify activities (default). | |||||
* `Pleroma.Web.ActivityPub.MRF.DropPolicy`: Drops all activities. It generally doesn’t makes sense to use in production. | |||||
* `Pleroma.Web.ActivityPub.MRF.SimplePolicy`: Restrict the visibility of activities from certains instances (See [`:mrf_simple`](#mrf_simple)). | |||||
* `Pleroma.Web.ActivityPub.MRF.TagPolicy`: Applies policies to individual users based on tags, which can be set using pleroma-fe/admin-fe/any other app that supports Pleroma Admin API. For example it allows marking posts from individual users nsfw (sensitive). | |||||
* `Pleroma.Web.ActivityPub.MRF.SubchainPolicy`: Selectively runs other MRF policies when messages match (See [`:mrf_subchain`](#mrf_subchain)). | |||||
* `Pleroma.Web.ActivityPub.MRF.RejectNonPublic`: Drops posts with non-public visibility settings (See [`:mrf_rejectnonpublic`](#mrf_rejectnonpublic)). | |||||
* `Pleroma.Web.ActivityPub.MRF.EnsureRePrepended`: Rewrites posts to ensure that replies to posts with subjects do not have an identical subject and instead begin with re:. | |||||
* `Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy`: Rejects posts from likely spambots by rejecting posts from new users that contain links. | |||||
* `Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`: Crawls attachments using their MediaProxy URLs so that the MediaProxy cache is primed. | |||||
* `Pleroma.Web.ActivityPub.MRF.MentionPolicy`: Drops posts mentioning configurable users. (See [`:mrf_mention`](#mrf_mention)). | |||||
* `Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`: Restricts activities to a configured set of vocabulary. (See [`:mrf_vocabulary`](#mrf_vocabulary)). | |||||
* `Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy`: Rejects or delists posts based on their age when received. (See [`:mrf_object_age`](#mrf_object_age)). | |||||
* `transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo). | |||||
* `transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value. | |||||
## Federation | ## Federation | ||||
### MRF policies | ### MRF policies | ||||
!!! note | !!! note | ||||
Configuring MRF policies is not enough for them to take effect. You have to enable them by specifying their module in `rewrite_policy` under [:instance](#instance) section. | |||||
Configuring MRF policies is not enough for them to take effect. You have to enable them by specifying their module in `policies` under [:mrf](#mrf) section. | |||||
#### :mrf_simple | #### :mrf_simple | ||||
* `media_removal`: List of instances to remove media from. | * `media_removal`: List of instances to remove media from. | ||||
@@ -969,13 +972,13 @@ config :pleroma, :database_config_whitelist, [ | |||||
Restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses. | Restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses. | ||||
* `timelines` - public and federated timelines | |||||
* `local` - public timeline | |||||
* `timelines`: public and federated timelines | |||||
* `local`: public timeline | |||||
* `federated` | * `federated` | ||||
* `profiles` - user profiles | |||||
* `profiles`: user profiles | |||||
* `local` | * `local` | ||||
* `remote` | * `remote` | ||||
* `activities` - statuses | |||||
* `activities`: statuses | |||||
* `local` | * `local` | ||||
* `remote` | * `remote` | ||||
@@ -34,9 +34,9 @@ config :pleroma, :instance, | |||||
To use `SimplePolicy`, you must enable it. Do so by adding the following to your `:instance` config object, so that it looks like this: | To use `SimplePolicy`, you must enable it. Do so by adding the following to your `:instance` config object, so that it looks like this: | ||||
```elixir | ```elixir | ||||
config :pleroma, :instance, | |||||
config :pleroma, :mrf, | |||||
[...] | [...] | ||||
rewrite_policy: Pleroma.Web.ActivityPub.MRF.SimplePolicy | |||||
policies: Pleroma.Web.ActivityPub.MRF.SimplePolicy | |||||
``` | ``` | ||||
Once `SimplePolicy` is enabled, you can configure various groups in the `:mrf_simple` config object. These groups are: | Once `SimplePolicy` is enabled, you can configure various groups in the `:mrf_simple` config object. These groups are: | ||||
@@ -58,8 +58,8 @@ Servers should be configured as lists. | |||||
This example will enable `SimplePolicy`, block media from `illegalporn.biz`, mark media as NSFW from `porn.biz` and `porn.business`, reject messages from `spam.com`, remove messages from `spam.university` from the federated timeline and block reports (flags) from `whiny.whiner`: | This example will enable `SimplePolicy`, block media from `illegalporn.biz`, mark media as NSFW from `porn.biz` and `porn.business`, reject messages from `spam.com`, remove messages from `spam.university` from the federated timeline and block reports (flags) from `whiny.whiner`: | ||||
```elixir | ```elixir | ||||
config :pleroma, :instance, | |||||
rewrite_policy: [Pleroma.Web.ActivityPub.MRF.SimplePolicy] | |||||
config :pleroma, :mrf, | |||||
policies: [Pleroma.Web.ActivityPub.MRF.SimplePolicy] | |||||
config :pleroma, :mrf_simple, | config :pleroma, :mrf_simple, | ||||
media_removal: ["illegalporn.biz"], | media_removal: ["illegalporn.biz"], | ||||
@@ -75,7 +75,7 @@ The effects of MRF policies can be very drastic. It is important to use this fun | |||||
## Writing your own MRF Policy | ## Writing your own MRF Policy | ||||
As discussed above, the MRF system is a modular system that supports pluggable policies. This means that an admin may write a custom MRF policy in Elixir or any other language that runs on the Erlang VM, by specifying the module name in the `rewrite_policy` config setting. | |||||
As discussed above, the MRF system is a modular system that supports pluggable policies. This means that an admin may write a custom MRF policy in Elixir or any other language that runs on the Erlang VM, by specifying the module name in the `policies` config setting. | |||||
For example, here is a sample policy module which rewrites all messages to "new message content": | For example, here is a sample policy module which rewrites all messages to "new message content": | ||||
@@ -125,8 +125,8 @@ end | |||||
If you save this file as `lib/pleroma/web/activity_pub/mrf/rewrite_policy.ex`, it will be included when you next rebuild Pleroma. You can enable it in the configuration like so: | If you save this file as `lib/pleroma/web/activity_pub/mrf/rewrite_policy.ex`, it will be included when you next rebuild Pleroma. You can enable it in the configuration like so: | ||||
```elixir | ```elixir | ||||
config :pleroma, :instance, | |||||
rewrite_policy: [ | |||||
config :pleroma, :mrf, | |||||
policies: [ | |||||
Pleroma.Web.ActivityPub.MRF.SimplePolicy, | Pleroma.Web.ActivityPub.MRF.SimplePolicy, | ||||
Pleroma.Web.ActivityPub.MRF.RewritePolicy | Pleroma.Web.ActivityPub.MRF.RewritePolicy | ||||
] | ] | ||||
@@ -54,13 +54,13 @@ defmodule Pleroma.ConfigDB do | |||||
defp create(params) do | defp create(params) do | ||||
%ConfigDB{} | %ConfigDB{} | ||||
|> changeset(params) | |||||
|> changeset(params, transform?) | |||||
|> Repo.insert() | |> Repo.insert() | ||||
end | end | ||||
defp update(%ConfigDB{} = config, %{value: value}) do | defp update(%ConfigDB{} = config, %{value: value}) do | ||||
config | config | ||||
|> changeset(%{value: value}) | |||||
|> changeset(%{value: value}, transform?) | |||||
|> Repo.update() | |> Repo.update() | ||||
end | end | ||||
@@ -167,7 +167,9 @@ defmodule Pleroma.ConfigDB do | |||||
end) | end) | ||||
end | end | ||||
@spec delete(map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()} | |||||
@spec delete(ConfigDB.t() | map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()} | |||||
def delete(%ConfigDB{} = config), do: Repo.delete(config) | |||||
def delete(params) do | def delete(params) do | ||||
search_opts = Map.delete(params, :subkeys) | search_opts = Map.delete(params, :subkeys) | ||||
@@ -3,9 +3,23 @@ | |||||
# SPDX-License-Identifier: AGPL-3.0-only | # SPDX-License-Identifier: AGPL-3.0-only | ||||
defmodule Pleroma.Config.DeprecationWarnings do | defmodule Pleroma.Config.DeprecationWarnings do | ||||
alias Pleroma.Config | |||||
require Logger | require Logger | ||||
alias Pleroma.Config | alias Pleroma.Config | ||||
@type config_namespace() :: [atom()] | |||||
@type config_map() :: {config_namespace(), config_namespace(), String.t()} | |||||
@mrf_config_map [ | |||||
{[:instance, :rewrite_policy], [:mrf, :policies], | |||||
"\n* `config :pleroma, :instance, rewrite_policy` is now `config :pleroma, :mrf, policies`"}, | |||||
{[:instance, :mrf_transparency], [:mrf, :transparency], | |||||
"\n* `config :pleroma, :instance, mrf_transparency` is now `config :pleroma, :mrf, transparency`"}, | |||||
{[:instance, :mrf_transparency_exclusions], [:mrf, :transparency_exclusions], | |||||
"\n* `config :pleroma, :instance, mrf_transparency_exclusions` is now `config :pleroma, :mrf, transparency_exclusions`"} | |||||
] | |||||
def check_hellthread_threshold do | def check_hellthread_threshold do | ||||
if Config.get([:mrf_hellthread, :threshold]) do | if Config.get([:mrf_hellthread, :threshold]) do | ||||
Logger.warn(""" | Logger.warn(""" | ||||
@@ -39,5 +53,35 @@ defmodule Pleroma.Config.DeprecationWarnings do | |||||
def warn do | def warn do | ||||
check_hellthread_threshold() | check_hellthread_threshold() | ||||
mrf_user_allowlist() | mrf_user_allowlist() | ||||
check_old_mrf_config() | |||||
end | |||||
def check_old_mrf_config do | |||||
warning_preface = """ | |||||
!!!DEPRECATION WARNING!!! | |||||
Your config is using old namespaces for MRF configuration. They should work for now, but you are advised to change to new namespaces to prevent possible issues later: | |||||
""" | |||||
move_namespace_and_warn(@mrf_config_map, warning_preface) | |||||
end | |||||
@spec move_namespace_and_warn([config_map()], String.t()) :: :ok | |||||
def move_namespace_and_warn(config_map, warning_preface) do | |||||
warning = | |||||
Enum.reduce(config_map, "", fn | |||||
{old, new, err_msg}, acc -> | |||||
old_config = Config.get(old) | |||||
if old_config do | |||||
Config.put(new, old_config) | |||||
acc <> err_msg | |||||
else | |||||
acc | |||||
end | |||||
end) | |||||
if warning != "" do | |||||
Logger.warn(warning_preface <> warning) | |||||
end | |||||
end | end | ||||
end | end |
@@ -16,7 +16,7 @@ defmodule Pleroma.Web.ActivityPub.MRF do | |||||
def filter(%{} = object), do: get_policies() |> filter(object) | def filter(%{} = object), do: get_policies() |> filter(object) | ||||
def get_policies do | def get_policies do | ||||
Pleroma.Config.get([:instance, :rewrite_policy], []) |> get_policies() | |||||
Pleroma.Config.get([:mrf, :policies], []) |> get_policies() | |||||
end | end | ||||
defp get_policies(policy) when is_atom(policy), do: [policy] | defp get_policies(policy) when is_atom(policy), do: [policy] | ||||
@@ -51,7 +51,7 @@ defmodule Pleroma.Web.ActivityPub.MRF do | |||||
get_policies() | get_policies() | ||||
|> Enum.map(fn policy -> to_string(policy) |> String.split(".") |> List.last() end) | |> Enum.map(fn policy -> to_string(policy) |> String.split(".") |> List.last() end) | ||||
exclusions = Pleroma.Config.get([:instance, :mrf_transparency_exclusions]) | |||||
exclusions = Pleroma.Config.get([:mrf, :transparency_exclusions]) | |||||
base = | base = | ||||
%{ | %{ | ||||
@@ -3,21 +3,23 @@ | |||||
# SPDX-License-Identifier: AGPL-3.0-only | # SPDX-License-Identifier: AGPL-3.0-only | ||||
defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do | defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do | ||||
alias Pleroma.User | |||||
alias Pleroma.Web.ActivityPub.MRF | |||||
@moduledoc "Filter activities depending on their origin instance" | @moduledoc "Filter activities depending on their origin instance" | ||||
@behaviour Pleroma.Web.ActivityPub.MRF | @behaviour Pleroma.Web.ActivityPub.MRF | ||||
alias Pleroma.Config | |||||
alias Pleroma.User | |||||
alias Pleroma.Web.ActivityPub.MRF | |||||
require Pleroma.Constants | require Pleroma.Constants | ||||
defp check_accept(%{host: actor_host} = _actor_info, object) do | defp check_accept(%{host: actor_host} = _actor_info, object) do | ||||
accepts = | accepts = | ||||
Pleroma.Config.get([:mrf_simple, :accept]) | |||||
Config.get([:mrf_simple, :accept]) | |||||
|> MRF.subdomains_regex() | |> MRF.subdomains_regex() | ||||
cond do | cond do | ||||
accepts == [] -> {:ok, object} | accepts == [] -> {:ok, object} | ||||
actor_host == Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, object} | |||||
actor_host == Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, object} | |||||
MRF.subdomain_match?(accepts, actor_host) -> {:ok, object} | MRF.subdomain_match?(accepts, actor_host) -> {:ok, object} | ||||
true -> {:reject, nil} | true -> {:reject, nil} | ||||
end | end | ||||
@@ -25,7 +27,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do | |||||
defp check_reject(%{host: actor_host} = _actor_info, object) do | defp check_reject(%{host: actor_host} = _actor_info, object) do | ||||
rejects = | rejects = | ||||
Pleroma.Config.get([:mrf_simple, :reject]) | |||||
Config.get([:mrf_simple, :reject]) | |||||
|> MRF.subdomains_regex() | |> MRF.subdomains_regex() | ||||
if MRF.subdomain_match?(rejects, actor_host) do | if MRF.subdomain_match?(rejects, actor_host) do | ||||
@@ -41,7 +43,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do | |||||
) | ) | ||||
when length(child_attachment) > 0 do | when length(child_attachment) > 0 do | ||||
media_removal = | media_removal = | ||||
Pleroma.Config.get([:mrf_simple, :media_removal]) | |||||
Config.get([:mrf_simple, :media_removal]) | |||||
|> MRF.subdomains_regex() | |> MRF.subdomains_regex() | ||||
object = | object = | ||||
@@ -65,7 +67,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do | |||||
} = object | } = object | ||||
) do | ) do | ||||
media_nsfw = | media_nsfw = | ||||
Pleroma.Config.get([:mrf_simple, :media_nsfw]) | |||||
Config.get([:mrf_simple, :media_nsfw]) | |||||
|> MRF.subdomains_regex() | |> MRF.subdomains_regex() | ||||
object = | object = | ||||
@@ -85,7 +87,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do | |||||
defp check_ftl_removal(%{host: actor_host} = _actor_info, object) do | defp check_ftl_removal(%{host: actor_host} = _actor_info, object) do | ||||
timeline_removal = | timeline_removal = | ||||
Pleroma.Config.get([:mrf_simple, :federated_timeline_removal]) | |||||
Config.get([:mrf_simple, :federated_timeline_removal]) | |||||
|> MRF.subdomains_regex() | |> MRF.subdomains_regex() | ||||
object = | object = | ||||
@@ -108,7 +110,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do | |||||
defp check_report_removal(%{host: actor_host} = _actor_info, %{"type" => "Flag"} = object) do | defp check_report_removal(%{host: actor_host} = _actor_info, %{"type" => "Flag"} = object) do | ||||
report_removal = | report_removal = | ||||
Pleroma.Config.get([:mrf_simple, :report_removal]) | |||||
Config.get([:mrf_simple, :report_removal]) | |||||
|> MRF.subdomains_regex() | |> MRF.subdomains_regex() | ||||
if MRF.subdomain_match?(report_removal, actor_host) do | if MRF.subdomain_match?(report_removal, actor_host) do | ||||
@@ -122,7 +124,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do | |||||
defp check_avatar_removal(%{host: actor_host} = _actor_info, %{"icon" => _icon} = object) do | defp check_avatar_removal(%{host: actor_host} = _actor_info, %{"icon" => _icon} = object) do | ||||
avatar_removal = | avatar_removal = | ||||
Pleroma.Config.get([:mrf_simple, :avatar_removal]) | |||||
Config.get([:mrf_simple, :avatar_removal]) | |||||
|> MRF.subdomains_regex() | |> MRF.subdomains_regex() | ||||
if MRF.subdomain_match?(avatar_removal, actor_host) do | if MRF.subdomain_match?(avatar_removal, actor_host) do | ||||
@@ -136,7 +138,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do | |||||
defp check_banner_removal(%{host: actor_host} = _actor_info, %{"image" => _image} = object) do | defp check_banner_removal(%{host: actor_host} = _actor_info, %{"image" => _image} = object) do | ||||
banner_removal = | banner_removal = | ||||
Pleroma.Config.get([:mrf_simple, :banner_removal]) | |||||
Config.get([:mrf_simple, :banner_removal]) | |||||
|> MRF.subdomains_regex() | |> MRF.subdomains_regex() | ||||
if MRF.subdomain_match?(banner_removal, actor_host) do | if MRF.subdomain_match?(banner_removal, actor_host) do | ||||
@@ -197,10 +199,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do | |||||
@impl true | @impl true | ||||
def describe do | def describe do | ||||
exclusions = Pleroma.Config.get([:instance, :mrf_transparency_exclusions]) | |||||
exclusions = Config.get([:mrf, :transparency_exclusions]) | |||||
mrf_simple = | mrf_simple = | ||||
Pleroma.Config.get(:mrf_simple) | |||||
Config.get(:mrf_simple) | |||||
|> Enum.map(fn {k, v} -> {k, Enum.reject(v, fn v -> v in exclusions end)} end) | |> Enum.map(fn {k, v} -> {k, Enum.reject(v, fn v -> v in exclusions end)} end) | ||||
|> Enum.into(%{}) | |> Enum.into(%{}) | ||||
@@ -78,7 +78,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do | |||||
def federation do | def federation do | ||||
quarantined = Config.get([:instance, :quarantined_instances], []) | quarantined = Config.get([:instance, :quarantined_instances], []) | ||||
if Config.get([:instance, :mrf_transparency]) do | |||||
if Config.get([:mrf, :transparency]) do | |||||
{:ok, data} = MRF.describe() | {:ok, data} = MRF.describe() | ||||
data | data | ||||
@@ -0,0 +1,39 @@ | |||||
defmodule Pleroma.Repo.Migrations.MrfConfigMoveFromInstanceNamespace do | |||||
use Ecto.Migration | |||||
alias Pleroma.ConfigDB | |||||
@old_keys [:rewrite_policy, :mrf_transparency, :mrf_transparency_exclusions] | |||||
def change do | |||||
config = ConfigDB.get_by_params(%{group: ":pleroma", key: ":instance"}) | |||||
if config do | |||||
old_instance = ConfigDB.from_binary(config.value) | |||||
mrf = | |||||
old_instance | |||||
|> Keyword.take(@old_keys) | |||||
|> Keyword.new(fn | |||||
{:rewrite_policy, policies} -> {:policies, policies} | |||||
{:mrf_transparency, transparency} -> {:transparency, transparency} | |||||
{:mrf_transparency_exclusions, exclusions} -> {:transparency_exclusions, exclusions} | |||||
end) | |||||
if mrf != [] do | |||||
{:ok, _} = | |||||
ConfigDB.create( | |||||
%{group: ":pleroma", key: ":mrf", value: ConfigDB.to_binary(mrf)}, | |||||
false | |||||
) | |||||
new_instance = Keyword.drop(old_instance, @old_keys) | |||||
if new_instance != [] do | |||||
{:ok, _} = ConfigDB.update(config, %{value: ConfigDB.to_binary(new_instance)}, false) | |||||
else | |||||
{:ok, _} = ConfigDB.delete(config) | |||||
end | |||||
end | |||||
end | |||||
end | |||||
end |
@@ -0,0 +1,57 @@ | |||||
defmodule Pleroma.Config.DeprecationWarningsTest do | |||||
use ExUnit.Case, async: true | |||||
use Pleroma.Tests.Helpers | |||||
import ExUnit.CaptureLog | |||||
test "check_old_mrf_config/0" do | |||||
clear_config([:instance, :rewrite_policy], Pleroma.Web.ActivityPub.MRF.NoOpPolicy) | |||||
clear_config([:instance, :mrf_transparency], true) | |||||
clear_config([:instance, :mrf_transparency_exclusions], []) | |||||
assert capture_log(fn -> Pleroma.Config.DeprecationWarnings.check_old_mrf_config() end) =~ | |||||
""" | |||||
!!!DEPRECATION WARNING!!! | |||||
Your config is using old namespaces for MRF configuration. They should work for now, but you are advised to change to new namespaces to prevent possible issues later: | |||||
* `config :pleroma, :instance, rewrite_policy` is now `config :pleroma, :mrf, policies` | |||||
* `config :pleroma, :instance, mrf_transparency` is now `config :pleroma, :mrf, transparency` | |||||
* `config :pleroma, :instance, mrf_transparency_exclusions` is now `config :pleroma, :mrf, transparency_exclusions` | |||||
""" | |||||
end | |||||
test "move_namespace_and_warn/2" do | |||||
old_group1 = [:group, :key] | |||||
old_group2 = [:group, :key2] | |||||
old_group3 = [:group, :key3] | |||||
new_group1 = [:another_group, :key4] | |||||
new_group2 = [:another_group, :key5] | |||||
new_group3 = [:another_group, :key6] | |||||
clear_config(old_group1, 1) | |||||
clear_config(old_group2, 2) | |||||
clear_config(old_group3, 3) | |||||
clear_config(new_group1) | |||||
clear_config(new_group2) | |||||
clear_config(new_group3) | |||||
config_map = [ | |||||
{old_group1, new_group1, "\n error :key"}, | |||||
{old_group2, new_group2, "\n error :key2"}, | |||||
{old_group3, new_group3, "\n error :key3"} | |||||
] | |||||
assert capture_log(fn -> | |||||
Pleroma.Config.DeprecationWarnings.move_namespace_and_warn( | |||||
config_map, | |||||
"Warning preface" | |||||
) | |||||
end) =~ "Warning preface\n error :key\n error :key2\n error :key3" | |||||
assert Pleroma.Config.get(new_group1) == 1 | |||||
assert Pleroma.Config.get(new_group2) == 2 | |||||
assert Pleroma.Config.get(new_group3) == 3 | |||||
end | |||||
end |
@@ -120,14 +120,11 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do | |||||
federation_reachability_timeout_days: 7, | federation_reachability_timeout_days: 7, | ||||
federation_publisher_modules: [Pleroma.Web.ActivityPub.Publisher], | federation_publisher_modules: [Pleroma.Web.ActivityPub.Publisher], | ||||
allow_relay: true, | allow_relay: true, | ||||
rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy, | |||||
public: true, | public: true, | ||||
quarantined_instances: [], | quarantined_instances: [], | ||||
managed_config: true, | managed_config: true, | ||||
static_dir: "instance/static/", | static_dir: "instance/static/", | ||||
allowed_post_formats: ["text/plain", "text/html", "text/markdown", "text/bbcode"], | allowed_post_formats: ["text/plain", "text/html", "text/markdown", "text/bbcode"], | ||||
mrf_transparency: true, | |||||
mrf_transparency_exclusions: [], | |||||
autofollowed_nicknames: [], | autofollowed_nicknames: [], | ||||
max_pinned_statuses: 1, | max_pinned_statuses: 1, | ||||
attachment_links: false, | attachment_links: false, | ||||
@@ -174,7 +171,7 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do | |||||
end | end | ||||
assert file == | assert file == | ||||
"#{header}\n\nconfig :pleroma, :instance,\n name: \"Pleroma\",\n email: \"example@example.com\",\n notify_email: \"noreply@example.com\",\n description: \"A Pleroma instance, an alternative fediverse server\",\n limit: 5000,\n chat_limit: 5000,\n remote_limit: 100_000,\n upload_limit: 16_000_000,\n avatar_upload_limit: 2_000_000,\n background_upload_limit: 4_000_000,\n banner_upload_limit: 4_000_000,\n poll_limits: %{\n max_expiration: 31_536_000,\n max_option_chars: 200,\n max_options: 20,\n min_expiration: 0\n },\n registrations_open: true,\n federating: true,\n federation_incoming_replies_max_depth: 100,\n federation_reachability_timeout_days: 7,\n federation_publisher_modules: [Pleroma.Web.ActivityPub.Publisher],\n allow_relay: true,\n rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,\n public: true,\n quarantined_instances: [],\n managed_config: true,\n static_dir: \"instance/static/\",\n allowed_post_formats: [\"text/plain\", \"text/html\", \"text/markdown\", \"text/bbcode\"],\n mrf_transparency: true,\n mrf_transparency_exclusions: [],\n autofollowed_nicknames: [],\n max_pinned_statuses: 1,\n attachment_links: false,\n welcome_user_nickname: nil,\n welcome_message: nil,\n max_report_comment_size: 1000,\n safe_dm_mentions: false,\n healthcheck: false,\n remote_post_retention_days: 90,\n skip_thread_containment: true,\n limit_to_local_content: :unauthenticated,\n user_bio_length: 5000,\n user_name_length: 100,\n max_account_fields: 10,\n max_remote_account_fields: 20,\n account_field_name_length: 512,\n account_field_value_length: 2048,\n external_user_synchronization: true,\n extended_nickname_format: true,\n multi_factor_authentication: [\n totp: [digits: 6, period: 30],\n backup_codes: [number: 2, length: 6]\n ]\n" | |||||
"#{header}\n\nconfig :pleroma, :instance,\n name: \"Pleroma\",\n email: \"example@example.com\",\n notify_email: \"noreply@example.com\",\n description: \"A Pleroma instance, an alternative fediverse server\",\n limit: 5000,\n chat_limit: 5000,\n remote_limit: 100_000,\n upload_limit: 16_000_000,\n avatar_upload_limit: 2_000_000,\n background_upload_limit: 4_000_000,\n banner_upload_limit: 4_000_000,\n poll_limits: %{\n max_expiration: 31_536_000,\n max_option_chars: 200,\n max_options: 20,\n min_expiration: 0\n },\n registrations_open: true,\n federating: true,\n federation_incoming_replies_max_depth: 100,\n federation_reachability_timeout_days: 7,\n federation_publisher_modules: [Pleroma.Web.ActivityPub.Publisher],\n allow_relay: true,\n public: true,\n quarantined_instances: [],\n managed_config: true,\n static_dir: \"instance/static/\",\n allowed_post_formats: [\"text/plain\", \"text/html\", \"text/markdown\", \"text/bbcode\"],\n autofollowed_nicknames: [],\n max_pinned_statuses: 1,\n attachment_links: false,\n welcome_user_nickname: nil,\n welcome_message: nil,\n max_report_comment_size: 1000,\n safe_dm_mentions: false,\n healthcheck: false,\n remote_post_retention_days: 90,\n skip_thread_containment: true,\n limit_to_local_content: :unauthenticated,\n user_bio_length: 5000,\n user_name_length: 100,\n max_account_fields: 10,\n max_remote_account_fields: 20,\n account_field_name_length: 512,\n account_field_value_length: 2048,\n external_user_synchronization: true,\n extended_nickname_format: true,\n multi_factor_authentication: [\n totp: [digits: 6, period: 30],\n backup_codes: [number: 2, length: 6]\n ]\n" | |||||
end | end | ||||
end | end | ||||
end | end |
@@ -60,8 +60,6 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do | |||||
end | end | ||||
describe "describe/0" do | describe "describe/0" do | ||||
setup do: clear_config([:instance, :rewrite_policy]) | |||||
test "it works as expected with noop policy" do | test "it works as expected with noop policy" do | ||||
expected = %{ | expected = %{ | ||||
mrf_policies: ["NoOpPolicy"], | mrf_policies: ["NoOpPolicy"], | ||||
@@ -72,7 +70,7 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do | |||||
end | end | ||||
test "it works as expected with mock policy" do | test "it works as expected with mock policy" do | ||||
Pleroma.Config.put([:instance, :rewrite_policy], [MRFModuleMock]) | |||||
clear_config([:mrf, :policies], [MRFModuleMock]) | |||||
expected = %{ | expected = %{ | ||||
mrf_policies: ["MRFModuleMock"], | mrf_policies: ["MRFModuleMock"], | ||||
@@ -23,7 +23,7 @@ defmodule Pleroma.Web.FederatorTest do | |||||
setup_all do: clear_config([:instance, :federating], true) | setup_all do: clear_config([:instance, :federating], true) | ||||
setup do: clear_config([:instance, :allow_relay]) | setup do: clear_config([:instance, :allow_relay]) | ||||
setup do: clear_config([:instance, :rewrite_policy]) | |||||
setup do: clear_config([:mrf, :policies]) | |||||
setup do: clear_config([:mrf_keyword]) | setup do: clear_config([:mrf_keyword]) | ||||
describe "Publish an activity" do | describe "Publish an activity" do | ||||
@@ -158,7 +158,7 @@ defmodule Pleroma.Web.FederatorTest do | |||||
Pleroma.Config.put([:mrf_keyword, :reject], ["lain"]) | Pleroma.Config.put([:mrf_keyword, :reject], ["lain"]) | ||||
Pleroma.Config.put( | Pleroma.Config.put( | ||||
[:instance, :rewrite_policy], | |||||
[:mrf, :policies], | |||||
Pleroma.Web.ActivityPub.MRF.KeywordPolicy | Pleroma.Web.ActivityPub.MRF.KeywordPolicy | ||||
) | ) | ||||
@@ -67,10 +67,10 @@ defmodule Pleroma.Web.NodeInfoTest do | |||||
end | end | ||||
test "returns fieldsLimits field", %{conn: conn} do | test "returns fieldsLimits field", %{conn: conn} do | ||||
Config.put([:instance, :max_account_fields], 10) | |||||
Config.put([:instance, :max_remote_account_fields], 15) | |||||
Config.put([:instance, :account_field_name_length], 255) | |||||
Config.put([:instance, :account_field_value_length], 2048) | |||||
clear_config([:instance, :max_account_fields], 10) | |||||
clear_config([:instance, :max_remote_account_fields], 15) | |||||
clear_config([:instance, :account_field_name_length], 255) | |||||
clear_config([:instance, :account_field_value_length], 2048) | |||||
response = | response = | ||||
conn | conn | ||||
@@ -84,8 +84,7 @@ defmodule Pleroma.Web.NodeInfoTest do | |||||
end | end | ||||
test "it returns the safe_dm_mentions feature if enabled", %{conn: conn} do | test "it returns the safe_dm_mentions feature if enabled", %{conn: conn} do | ||||
option = Config.get([:instance, :safe_dm_mentions]) | |||||
Config.put([:instance, :safe_dm_mentions], true) | |||||
clear_config([:instance, :safe_dm_mentions], true) | |||||
response = | response = | ||||
conn | conn | ||||
@@ -102,8 +101,6 @@ defmodule Pleroma.Web.NodeInfoTest do | |||||
|> json_response(:ok) | |> json_response(:ok) | ||||
refute "safe_dm_mentions" in response["metadata"]["features"] | refute "safe_dm_mentions" in response["metadata"]["features"] | ||||
Config.put([:instance, :safe_dm_mentions], option) | |||||
end | end | ||||
describe "`metadata/federation/enabled`" do | describe "`metadata/federation/enabled`" do | ||||
@@ -156,14 +153,11 @@ defmodule Pleroma.Web.NodeInfoTest do | |||||
end | end | ||||
test "it shows MRF transparency data if enabled", %{conn: conn} do | test "it shows MRF transparency data if enabled", %{conn: conn} do | ||||
config = Config.get([:instance, :rewrite_policy]) | |||||
Config.put([:instance, :rewrite_policy], [Pleroma.Web.ActivityPub.MRF.SimplePolicy]) | |||||
option = Config.get([:instance, :mrf_transparency]) | |||||
Config.put([:instance, :mrf_transparency], true) | |||||
clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy]) | |||||
clear_config([:mrf, :transparency], true) | |||||
simple_config = %{"reject" => ["example.com"]} | simple_config = %{"reject" => ["example.com"]} | ||||
Config.put(:mrf_simple, simple_config) | |||||
clear_config(:mrf_simple, simple_config) | |||||
response = | response = | ||||
conn | conn | ||||
@@ -171,26 +165,17 @@ defmodule Pleroma.Web.NodeInfoTest do | |||||
|> json_response(:ok) | |> json_response(:ok) | ||||
assert response["metadata"]["federation"]["mrf_simple"] == simple_config | assert response["metadata"]["federation"]["mrf_simple"] == simple_config | ||||
Config.put([:instance, :rewrite_policy], config) | |||||
Config.put([:instance, :mrf_transparency], option) | |||||
Config.put(:mrf_simple, %{}) | |||||
end | end | ||||
test "it performs exclusions from MRF transparency data if configured", %{conn: conn} do | test "it performs exclusions from MRF transparency data if configured", %{conn: conn} do | ||||
config = Config.get([:instance, :rewrite_policy]) | |||||
Config.put([:instance, :rewrite_policy], [Pleroma.Web.ActivityPub.MRF.SimplePolicy]) | |||||
option = Config.get([:instance, :mrf_transparency]) | |||||
Config.put([:instance, :mrf_transparency], true) | |||||
exclusions = Config.get([:instance, :mrf_transparency_exclusions]) | |||||
Config.put([:instance, :mrf_transparency_exclusions], ["other.site"]) | |||||
clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy]) | |||||
clear_config([:mrf, :transparency], true) | |||||
clear_config([:mrf, :transparency_exclusions], ["other.site"]) | |||||
simple_config = %{"reject" => ["example.com", "other.site"]} | simple_config = %{"reject" => ["example.com", "other.site"]} | ||||
expected_config = %{"reject" => ["example.com"]} | |||||
clear_config(:mrf_simple, simple_config) | |||||
Config.put(:mrf_simple, simple_config) | |||||
expected_config = %{"reject" => ["example.com"]} | |||||
response = | response = | ||||
conn | conn | ||||
@@ -199,10 +184,5 @@ defmodule Pleroma.Web.NodeInfoTest do | |||||
assert response["metadata"]["federation"]["mrf_simple"] == expected_config | assert response["metadata"]["federation"]["mrf_simple"] == expected_config | ||||
assert response["metadata"]["federation"]["exclusions"] == true | assert response["metadata"]["federation"]["exclusions"] == true | ||||
Config.put([:instance, :rewrite_policy], config) | |||||
Config.put([:instance, :mrf_transparency], option) | |||||
Config.put([:instance, :mrf_transparency_exclusions], exclusions) | |||||
Config.put(:mrf_simple, %{}) | |||||
end | end | ||||
end | end |