Merge branch 'fix/credo-issues' into 'develop'
authorkaniini <nenolod@gmail.com>
Sun, 10 Feb 2019 20:54:21 +0000 (20:54 +0000)
committerkaniini <nenolod@gmail.com>
Sun, 10 Feb 2019 20:54:21 +0000 (20:54 +0000)
Fix credo issues

See merge request pleroma/pleroma!786

17 files changed:
README.md
config/config.exs
docs/Clients.md [new file with mode: 0644]
docs/config.md
installation/caddyfile-pleroma.example
installation/init.d/pleroma
installation/pleroma.nginx
installation/pleroma.service
lib/pleroma/object.ex
lib/pleroma/user.ex
lib/pleroma/web/activity_pub/mrf/keyword_policy.ex [new file with mode: 0644]
lib/pleroma/web/activity_pub/utils.ex
lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
lib/pleroma/web/twitter_api/twitter_api.ex
test/object_test.exs
test/user_test.exs
test/web/activity_pub/mrf/keyword_policy_test.exs [new file with mode: 0644]

index d39731ef4cd5a96974ba2c8f6ba5d848d15944d3..4f22445d0121555fc81ddfa22d5e83110a4628fe 100644 (file)
--- a/README.md
+++ b/README.md
@@ -8,20 +8,7 @@ Pleroma is written in Elixir, high-performance and can run on small devices like
 
 For clients it supports both the [GNU Social API with Qvitter extensions](https://twitter-api.readthedocs.io/en/latest/index.html) and the [Mastodon client API](https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md).
 
-Client applications that are committed to supporting Pleroma:
-
-* Mastalab (Android, Streaming Ready)
-* nekonium (Android, Streaming Ready)
-* Tusky (Android, No Streaming)
-* Twidere (Android, No Streaming)
-* Mast (iOS, Paid)
-* Amaroq (iOS, No Streaming)
-
-Client applications that are known to work well:
-
-* Tootdon (Android + iOS, No Streaming)
-* Tootle (iOS, No Streaming)
-* Whalebird (Windows + Mac + Linux)
+- [Client Applications for Pleroma](docs/Clients.md)
 
 No release has been made yet, but several servers have been online for months already. If you want to run your own server, feel free to contact us at @lain@pleroma.soykaf.com or in our dev chat at #pleroma on freenode or via matrix at <https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org>.
 
index c272ef34a2980f70a048da959b248b2a356b8477..5db0ea9aa9a5397834fa53514c7b73fb71392724 100644 (file)
@@ -238,6 +238,11 @@ config :pleroma, :mrf_simple,
   reject: [],
   accept: []
 
+config :pleroma, :mrf_keyword,
+  reject: [],
+  federated_timeline_removal: [],
+  replace: []
+
 config :pleroma, :rich_media, enabled: true
 
 config :pleroma, :media_proxy,
diff --git a/docs/Clients.md b/docs/Clients.md
new file mode 100644 (file)
index 0000000..28f8afa
--- /dev/null
@@ -0,0 +1,83 @@
+# Pleroma Clients
+Note: Additionnal clients may be working but theses are officially supporting Pleroma.
+Feel free to contact us to be added to this list!
+
+## Desktop
+### Roma for Desktop
+- Homepage: <http://www.pleroma.com/desktop-app/>
+- Source Code: ???
+- Platforms: Windows, Mac, (Linux?)
+
+### Social
+- Source Code: <https://gitlab.gnome.org/BrainBlasted/Social>
+- Contact: [@brainblasted@social.libre.fi](https://social.libre.fi/users/brainblasted)
+- Platforms: Linux (GNOME)
+- Note(2019-01-28): Not at a pre-alpha stage yet
+
+### Whalebird
+- Homepage: <https://whalebird.org/>
+- Source Code: <https://github.com/h3poteto/whalebird-desktop>
+- Contact: [@h3poteto@pleroma.io](https://pleroma.io/users/h3poteto)
+- Platforms: Windows, Mac, Linux
+
+## Handheld
+### Amaroq
+- Homepage: <https://itunes.apple.com/us/app/amaroq-for-mastodon/id1214116200>
+- Source Code: <https://github.com/ReticentJohn/Amaroq>
+- Contact: [@eurasierboy@mastodon.social](https://mastodon.social/users/eurasierboy)
+- Platforms: iOS
+
+### Nekonium
+- Homepage: [F-Droid Repository](https://repo.gdgd.jp.net/), [Google Play](https://play.google.com/store/apps/details?id=com.apps.nekonium), [Amazon](https://www.amazon.co.jp/dp/B076FXPRBC/)
+- Source: <https://git.gdgd.jp.net/lin/nekonium/>
+- Contact: [@lin@pleroma.gdgd.jp.net](https://pleroma.gdgd.jp.net/users/lin)
+- Platforms: Android
+
+### Mastalab
+- Source Code: <https://gitlab.com/tom79/mastalab/>
+- Contact: [@tom79@mastodon.social](https://mastodon.social/users/tom79)
+- Platforms: Android
+
+### Roma
+- Homepage: <http://www.pleroma.com/>
+- Source Code: ???
+- Platforms: iOS, Android
+
+### Tootdon
+- Homepage: <http://tootdon.club/>, <http://blog.mastodon-tootdon.com/>
+- Source Code: ???
+- Contact: [@tootdon@mstdn.jp](https://mstdn.jp/users/tootdon)
+- Platforms: Android, iOS
+
+### Tusky
+- Homepage: <https://tuskyapp.github.io/>
+- Source Code: <https://github.com/tuskyapp/Tusky>
+- Contact: [@ConnyDuck@mastodon.social](https://mastodon.social/users/ConnyDuck)
+- Platforms: Android
+
+### Twidere
+- Homepage: <https://twidere.mariotaku.org/>
+- Source Code: <https://github.com/TwidereProject/Twidere-Android/>, <https://github.com/TwidereProject/Twidere-iOS/>
+- Contact: <me@mariotaku.org>
+- Platform: Android, iOS
+
+## Alternative Web Interfaces
+### Brutaldon
+- Homepage: <https://jfm.carcosa.net/projects/software/brutaldon/>
+- Source Code: <https://github.com/jfmcbrayer/brutaldon>
+- Contact: [@gcupc@glitch.social](https://glitch.social/users/gcupc)
+
+### Halcyon
+- Source Code: <https://notabug.org/halcyon-suite/halcyon>
+- Contact: [@halcyon@social.csswg.org](https://social.csswg.org/users/halcyon)
+
+### Pinafore
+- Homepage: <https://pinafore.social/>
+- Source Code: <https://github.com/nolanlawson/pinafore>
+- Contact: [@pinafore@mastodon.technology](https://mastodon.technology/users/pinafore)
+- Note: Pleroma support is a secondary goal
+
+### Sengi
+- Source Code: <https://github.com/NicolasConstant/sengi>
+- Contact: [@sengi_app@mastodon.social](https://mastodon.social/users/sengi_app)
+- Note(2019-01-28): The development is currently in a early stage.
index ba6807760867877e233106c1fe0a15df750b5356..74badd0da29d875e40330b0dafb0f754687fe5de 100644 (file)
@@ -171,6 +171,11 @@ This section is used to configure Pleroma-FE, unless ``:managed_config`` in ``:i
 * `delist_threshold`: Number of mentioned users after which the message gets delisted (the message can still be seen, but it will not show up in public timelines and mentioned users won't get notifications about it). Set to 0 to disable.
 * `reject_threshold`: Number of mentioned users after which the messaged gets rejected. Set to 0 to disable.
 
+## :mrf_keyword
+* `reject`: A list of patterns which result in message being rejected, each pattern can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html)
+* `federated_timeline_removal`: A list of patterns which result in message being removed from federated timelines (a.k.a unlisted), each pattern can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html)
+* `replace`: A list of tuples containing `{pattern, replacement}`, `pattern` can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html)
+
 ## :media_proxy
 * `enabled`: Enables proxying of remote media to the instance’s proxy
 * `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts.
index 03ff000b6c00b6ec6344f108d9b4ccdf3fbaade9..fcf76718e74ebd1f47f34e9461de26fc5c226779 100644 (file)
@@ -23,6 +23,11 @@ example.tld  {
 
   # If you do not want to use the mediaproxy function, remove these lines.
   # To use this directive, you need the http.cache plugin for Caddy.
+  cache {
+    match_path /media
+    default_max_age 720m
+  }
+
   cache {
     match_path /proxy
     default_max_age 720m
index 2b211df65ff59cebc2167a53c4950495a89191fa..ed50bb551a0b9591a7ea15503083859b2bfa77ca 100755 (executable)
@@ -1,7 +1,7 @@
 #!/sbin/openrc-run
 
 # Requires OpenRC >= 0.35
-directory=~pleroma/pleroma
+directory=/opt/pleroma
 
 command=/usr/bin/mix
 command_args="phx.server"
@@ -18,4 +18,4 @@ pidfile="/var/run/pleroma.pid"
 
 depend() {
     need nginx postgresql
-}
\ No newline at end of file
+}
index a24bb0e61fe7bf4dff98efb86f9de978435ac828..a3d55e4bff785db6bb9a8b88c5e4af67f1f553e3 100644 (file)
@@ -15,12 +15,13 @@ server {
     return         301 https://$server_name$request_uri;
 
     # Uncomment this if you need to use the 'webroot' method with certbot. Make sure
-    # that you also create the .well-known/acme-challenge directory structure in pleroma/priv/static and
-    # that is is accessible by the webserver. You may need to load this file with the ssl
-    # server block commented out, run certbot to get the certificate, and then uncomment it.
+    # that the directory exists and that it is accessible by the webserver. If you followed
+    # the guide, you already ran 'sudo mkdir -p /var/lib/letsencrypt' to create the folder.
+    # You may need to load this file with the ssl server block commented out, run certbot
+    # to get the certificate, and then uncomment it.
     #
     # location ~ /\.well-known/acme-challenge {
-    #     root <path to install>/pleroma/priv/static/;
+    #     root /var/lib/letsencrypt/.well-known/acme-challenge;
     # }
 }
 
index 72090bbc74cec8a0c5608980838cd3d7048805bb..5dcbc13877f916de16443bec9ead491ee26bb687 100644 (file)
@@ -14,15 +14,17 @@ Environment="MIX_ENV=prod"
 
 ; Make sure that all paths fit your installation.
 ; Path to the home directory of the user running the Pleroma service.
-Environment="HOME=/home/pleroma"
+Environment="HOME=/var/lib/pleroma"
 ; Path to the folder containing the Pleroma installation.
-WorkingDirectory=/home/pleroma/pleroma
+WorkingDirectory=/opt/pleroma
 ; Path to the Mix binary.
 ExecStart=/usr/bin/mix phx.server
 
 ; Some security directives.
 ; Use private /tmp and /var/tmp folders inside a new file system namespace, which are discarded after the process stops.
 PrivateTmp=true
+; The /home, /root, and /run/user folders can not be accessed by this service anymore. If your Pleroma user has its home folder in one of the restricted places, or use one of these folders as its working directory, you have to set this to false.
+ProtectHome=true
 ; Mount /usr, /boot, and /etc as read-only for processes invoked by this service.
 ProtectSystem=full
 ; Sets up a new /dev mount for the process and only adds API pseudo devices like /dev/null, /dev/zero or /dev/random but not physical devices. Disabled by default because it may not work on devices like the Raspberry Pi.
index 5f1fc801b592d70da0534fdb9defd34e99880758..dabb495364277fdd0299a5e185db8947f26fd8bc 100644 (file)
@@ -20,9 +20,29 @@ defmodule Pleroma.Object do
     timestamps()
   end
 
+  def insert_or_get(cng) do
+    {_, data} = fetch_field(cng, :data)
+    id = data["id"] || data[:id]
+    key = "object:#{id}"
+
+    fetcher = fn _ ->
+      with nil <- get_by_ap_id(id),
+           {:ok, object} <- Repo.insert(cng) do
+        {:commit, object}
+      else
+        %Object{} = object -> {:commit, object}
+        e -> {:ignore, e}
+      end
+    end
+
+    with {state, object} when state in [:commit, :ok] <- Cachex.fetch(:object_cache, key, fetcher) do
+      {:ok, object}
+    end
+  end
+
   def create(data) do
     Object.change(%Object{}, %{data: data})
-    |> Repo.insert()
+    |> insert_or_get()
   end
 
   def change(struct, params \\ %{}) do
index b44ba12799a2de96583116771e4820da93fdf761..0060d966bd17289cdfe7fba93dd88acbc7f192a9 100644 (file)
@@ -106,12 +106,6 @@ defmodule Pleroma.User do
     "#{ap_id(user)}/followers"
   end
 
-  def follow_changeset(struct, params \\ %{}) do
-    struct
-    |> cast(params, [:following])
-    |> validate_required([:following])
-  end
-
   def user_info(%User{} = user) do
     oneself = if user.local, do: 1, else: 0
 
@@ -266,8 +260,8 @@ defmodule Pleroma.User do
   @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
   def register(%Ecto.Changeset{} = changeset) do
     with {:ok, user} <- Repo.insert(changeset),
-         {:ok, _} <- try_send_confirmation_email(user),
-         {:ok, user} <- autofollow_users(user) do
+         {:ok, user} <- autofollow_users(user),
+         {:ok, _} <- try_send_confirmation_email(user) do
       {:ok, user}
     end
   end
@@ -317,10 +311,13 @@ defmodule Pleroma.User do
     end
   end
 
-  @doc "A mass follow for local users. Ignores blocks and has no side effects"
+  @doc "A mass follow for local users. Respects blocks but does not create activities."
   @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
   def follow_all(follower, followeds) do
-    followed_addresses = Enum.map(followeds, fn %{follower_address: fa} -> fa end)
+    followed_addresses =
+      followeds
+      |> Enum.reject(fn %{ap_id: ap_id} -> ap_id in follower.info.blocks end)
+      |> Enum.map(fn %{follower_address: fa} -> fa end)
 
     q =
       from(u in User,
diff --git a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
new file mode 100644 (file)
index 0000000..ce6d2e5
--- /dev/null
@@ -0,0 +1,73 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
+  @behaviour Pleroma.Web.ActivityPub.MRF
+  defp string_matches?(string, pattern) when is_binary(pattern) do
+    String.contains?(string, pattern)
+  end
+
+  defp string_matches?(string, pattern) do
+    String.match?(string, pattern)
+  end
+
+  defp check_reject(%{"object" => %{"content" => content}} = message) do
+    if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern ->
+         string_matches?(content, pattern)
+       end) do
+      {:reject, nil}
+    else
+      {:ok, message}
+    end
+  end
+
+  defp check_ftl_removal(%{"to" => to, "object" => %{"content" => content}} = message) do
+    if "https://www.w3.org/ns/activitystreams#Public" in to and
+         Enum.any?(Pleroma.Config.get([:mrf_keyword, :federated_timeline_removal]), fn pattern ->
+           string_matches?(content, pattern)
+         end) do
+      to = List.delete(to, "https://www.w3.org/ns/activitystreams#Public")
+      cc = ["https://www.w3.org/ns/activitystreams#Public" | message["cc"] || []]
+
+      message =
+        message
+        |> Map.put("to", to)
+        |> Map.put("cc", cc)
+
+      {:ok, message}
+    else
+      {:ok, message}
+    end
+  end
+
+  defp check_replace(%{"object" => %{"content" => content}} = message) do
+    content =
+      Enum.reduce(Pleroma.Config.get([:mrf_keyword, :replace]), content, fn {pattern, replacement},
+                                                                            acc ->
+        String.replace(acc, pattern, replacement)
+      end)
+
+    {:ok, put_in(message["object"]["content"], content)}
+  end
+
+  @impl true
+  def filter(%{"object" => %{"content" => nil}} = message) do
+    {:ok, message}
+  end
+
+  @impl true
+  def filter(%{"type" => "Create", "object" => %{"content" => _content}} = message) do
+    with {:ok, message} <- check_reject(message),
+         {:ok, message} <- check_ftl_removal(message),
+         {:ok, message} <- check_replace(message) do
+      {:ok, message}
+    else
+      _e ->
+        {:reject, nil}
+    end
+  end
+
+  @impl true
+  def filter(message), do: {:ok, message}
+end
index 964e11c9d424caae12a43dff5e6c2d46157ad06c..da6cca4ddd4052b93013b8c0c3b30a4fd88002b0 100644 (file)
@@ -142,14 +142,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do
     context = context || generate_id("contexts")
     changeset = Object.context_mapping(context)
 
-    case Repo.insert(changeset) do
-      {:ok, object} ->
-        object
-
-      # This should be solved by an upsert, but it seems ecto
-      # has problems accessing the constraint inside the jsonb.
-      {:error, _} ->
-        Object.get_cached_by_ap_id(context)
+    with {:ok, object} <- Object.insert_or_get(changeset) do
+      object
     end
   end
 
index c38827165ae7495fac783cfbf62834b5881b82e8..f4867d05bd0c4de0e78d921be7eb06c1f2da8b5c 100644 (file)
@@ -45,6 +45,33 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
       Application.get_env(:pleroma, :mrf_simple)
       |> Enum.into(%{})
 
+    # This horror is needed to convert regex sigils to strings
+    mrf_keyword =
+      Application.get_env(:pleroma, :mrf_keyword, [])
+      |> Enum.map(fn {key, value} ->
+        {key,
+         Enum.map(value, fn
+           {pattern, replacement} ->
+             %{
+               "pattern" =>
+                 if not is_binary(pattern) do
+                   inspect(pattern)
+                 else
+                   pattern
+                 end,
+               "replacement" => replacement
+             }
+
+           pattern ->
+             if not is_binary(pattern) do
+               inspect(pattern)
+             else
+               pattern
+             end
+         end)}
+      end)
+      |> Enum.into(%{})
+
     mrf_policies =
       MRF.get_policies()
       |> Enum.map(fn policy -> to_string(policy) |> String.split(".") |> List.last() end)
@@ -72,6 +99,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
         %{
           mrf_policies: mrf_policies,
           mrf_simple: mrf_simple,
+          mrf_keyword: mrf_keyword,
           mrf_user_allowlist: mrf_user_allowlist,
           quarantined_instances: quarantined
         }
index db521a3ad09599568d52dc597c2b1e469e8dc6e4..162beb9be06b7ffb7f95b51efb434ed3d5d1871a 100644 (file)
@@ -310,16 +310,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
     else
       _e ->
         changeset = Object.context_mapping(context)
-
-        case Repo.insert(changeset) do
-          {:ok, %{id: id}} ->
-            id
-
-          # This should be solved by an upsert, but it seems ecto
-          # has problems accessing the constraint inside the jsonb.
-          {:error, _} ->
-            Object.get_cached_by_ap_id(context).id
-        end
+        {:ok, object} = Object.insert_or_get(changeset)
+        object.id
     end
   end
 
index 72194975d7b25405bae21fedb1c7d9e93d0dedda..ab643101253fda533f61150aea58f1c222dbd6f4 100644 (file)
@@ -57,4 +57,32 @@ defmodule Pleroma.ObjectTest do
       assert cached_object.data["type"] == "Tombstone"
     end
   end
+
+  describe "insert_or_get" do
+    test "inserting the same object twice (by id) just returns the original object" do
+      data = %{data: %{"id" => Ecto.UUID.generate()}}
+      cng = Object.change(%Object{}, data)
+      {:ok, object} = Object.insert_or_get(cng)
+      {:ok, second_object} = Object.insert_or_get(cng)
+
+      Cachex.clear(:object_cache)
+      {:ok, third_object} = Object.insert_or_get(cng)
+
+      assert object == second_object
+      assert object == third_object
+    end
+  end
+
+  describe "create" do
+    test "inserts an object for a given data set" do
+      data = %{"id" => Ecto.UUID.generate()}
+
+      {:ok, object} = Object.create(data)
+      assert object.data["id"] == data["id"]
+
+      # Works when doing it twice.
+      {:ok, object} = Object.create(data)
+      assert object.data["id"] == data["id"]
+    end
+  end
 end
index 98d3bc4641a89dde961f50a220f4c43aabd5b047..523ab1ea430256ebb3ac257aac2080262df2a8ba 100644 (file)
@@ -53,16 +53,20 @@ defmodule Pleroma.UserTest do
     followed_zero = insert(:user)
     followed_one = insert(:user)
     followed_two = insert(:user)
+    blocked = insert(:user)
     not_followed = insert(:user)
 
+    {:ok, user} = User.block(user, blocked)
+
     {:ok, user} = User.follow(user, followed_zero)
 
-    {:ok, user} = User.follow_all(user, [followed_one, followed_two])
+    {:ok, user} = User.follow_all(user, [followed_one, followed_two, blocked])
 
     assert User.following?(user, followed_one)
     assert User.following?(user, followed_two)
     assert User.following?(user, followed_zero)
     refute User.following?(user, not_followed)
+    refute User.following?(user, blocked)
   end
 
   test "follow_all follows mutliple users without duplicating" do
diff --git a/test/web/activity_pub/mrf/keyword_policy_test.exs b/test/web/activity_pub/mrf/keyword_policy_test.exs
new file mode 100644 (file)
index 0000000..67a5858
--- /dev/null
@@ -0,0 +1,111 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicyTest do
+  use Pleroma.DataCase
+
+  alias Pleroma.Web.ActivityPub.MRF.KeywordPolicy
+
+  setup do
+    Pleroma.Config.put([:mrf_keyword], %{reject: [], federated_timeline_removal: [], replace: []})
+  end
+
+  describe "rejecting based on keywords" do
+    test "rejects if string matches" do
+      Pleroma.Config.put([:mrf_keyword, :reject], ["pun"])
+
+      message = %{
+        "type" => "Create",
+        "object" => %{"content" => "just a daily reminder that compLAINer is a good pun"}
+      }
+
+      assert {:reject, nil} == KeywordPolicy.filter(message)
+    end
+
+    test "rejects if regex matches" do
+      Pleroma.Config.put([:mrf_keyword, :reject], [~r/comp[lL][aA][iI][nN]er/])
+
+      assert true ==
+               Enum.all?(["complainer", "compLainer", "compLAiNer", "compLAINer"], fn content ->
+                 message = %{
+                   "type" => "Create",
+                   "object" => %{
+                     "content" => "just a daily reminder that #{content} is a good pun"
+                   }
+                 }
+
+                 {:reject, nil} == KeywordPolicy.filter(message)
+               end)
+    end
+  end
+
+  describe "delisting from ftl based on keywords" do
+    test "delists if string matches" do
+      Pleroma.Config.put([:mrf_keyword, :federated_timeline_removal], ["pun"])
+
+      message = %{
+        "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+        "type" => "Create",
+        "object" => %{"content" => "just a daily reminder that compLAINer is a good pun"}
+      }
+
+      {:ok, result} = KeywordPolicy.filter(message)
+      assert ["https://www.w3.org/ns/activitystreams#Public"] == result["cc"]
+      refute ["https://www.w3.org/ns/activitystreams#Public"] == result["to"]
+    end
+
+    test "delists if regex matches" do
+      Pleroma.Config.put([:mrf_keyword, :federated_timeline_removal], [~r/comp[lL][aA][iI][nN]er/])
+
+      assert true ==
+               Enum.all?(["complainer", "compLainer", "compLAiNer", "compLAINer"], fn content ->
+                 message = %{
+                   "type" => "Create",
+                   "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+                   "object" => %{
+                     "content" => "just a daily reminder that #{content} is a good pun"
+                   }
+                 }
+
+                 {:ok, result} = KeywordPolicy.filter(message)
+
+                 ["https://www.w3.org/ns/activitystreams#Public"] == result["cc"] and
+                   not (["https://www.w3.org/ns/activitystreams#Public"] == result["to"])
+               end)
+    end
+  end
+
+  describe "replacing keywords" do
+    test "replaces keyword if string matches" do
+      Pleroma.Config.put([:mrf_keyword, :replace], [{"opensource", "free software"}])
+
+      message = %{
+        "type" => "Create",
+        "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+        "object" => %{"content" => "ZFS is opensource"}
+      }
+
+      {:ok, %{"object" => %{"content" => result}}} = KeywordPolicy.filter(message)
+      assert result == "ZFS is free software"
+    end
+
+    test "replaces keyword if regex matches" do
+      Pleroma.Config.put([:mrf_keyword, :replace], [
+        {~r/open(-|\s)?source\s?(software)?/, "free software"}
+      ])
+
+      assert true ==
+               Enum.all?(["opensource", "open-source", "open source"], fn content ->
+                 message = %{
+                   "type" => "Create",
+                   "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+                   "object" => %{"content" => "ZFS is #{content}"}
+                 }
+
+                 {:ok, %{"object" => %{"content" => result}}} = KeywordPolicy.filter(message)
+                 result == "ZFS is free software"
+               end)
+    end
+  end
+end