Merge branch 'bugfix/change_password' into 'develop'
authorHaelwenn <contact+git.pleroma.social@hacktivis.me>
Tue, 10 Aug 2021 18:40:13 +0000 (18:40 +0000)
committerHaelwenn <contact+git.pleroma.social@hacktivis.me>
Tue, 10 Aug 2021 18:40:13 +0000 (18:40 +0000)
TwitterAPI: Make change_password require body params instead of query

Closes #2740

See merge request pleroma/pleroma!3503

48 files changed:
CHANGELOG.md
config/config.exs
config/description.exs
docs/configuration/cheatsheet.md
docs/configuration/mrf.md
docs/installation/alpine_linux_en.md
docs/installation/arch_linux_en.md
docs/installation/debian_based_en.md
docs/installation/gentoo_en.md
docs/installation/migrating_from_source_otp_en.md
docs/installation/otp_en.md
docs/installation/otp_vs_from_source.include [new file with mode: 0644]
docs/installation/otp_vs_from_source_source.include [new file with mode: 0644]
lib/pleroma/config/deprecation_warnings.ex
lib/pleroma/notification.ex
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/activity_pub/mrf.ex
lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
lib/pleroma/web/activity_pub/mrf/object_age_policy.ex
lib/pleroma/web/activity_pub/mrf/reject_non_public.ex
lib/pleroma/web/activity_pub/mrf/simple_policy.ex
lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex
lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex
lib/pleroma/web/activity_pub/publisher.ex
lib/pleroma/web/activity_pub/side_effects.ex
lib/pleroma/web/api_spec/operations/notification_operation.ex
lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
lib/pleroma/web/mastodon_api/views/instance_view.ex
lib/pleroma/web/mastodon_api/views/notification_view.ex
lib/pleroma/web/push/subscription.ex
lib/pleroma/workers/poll_worker.ex [new file with mode: 0644]
priv/repo/migrations/20201005123100_simple_policy_string_to_tuple.exs [new file with mode: 0644]
priv/repo/migrations/20201005124600_quarantained_policy_string_to_tuple.exs [new file with mode: 0644]
priv/repo/migrations/20201005132900_transparency_exclusions_string_to_tuple.exs [new file with mode: 0644]
priv/repo/migrations/20210717000000_add_poll_to_notifications_enum.exs [new file with mode: 0644]
test/pleroma/config/deprecation_warnings_test.exs
test/pleroma/notification_test.exs
test/pleroma/user_test.exs
test/pleroma/web/activity_pub/mrf/object_age_policy_test.exs
test/pleroma/web/activity_pub/mrf/simple_policy_test.exs
test/pleroma/web/activity_pub/mrf_test.exs
test/pleroma/web/activity_pub/publisher_test.exs
test/pleroma/web/activity_pub/side_effects_test.exs
test/pleroma/web/common_api_test.exs
test/pleroma/web/mastodon_api/controllers/status_controller_test.exs
test/pleroma/web/mastodon_api/views/notification_view_test.exs
test/pleroma/web/node_info_test.exs
test/support/factory.ex

index 4e3e408646f1aad4403ca427ee2d67914bf62472..45a365505756860a3c5a3b272efdd5a7138b0f67 100644 (file)
@@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 ### Changed
 
 - **Breaking:** Configuration: `:chat, enabled` moved to `:shout, enabled` and `:instance, chat_limit` moved to `:shout, limit`
+- **Breaking** Entries for simple_policy, transparency_exclusions and quarantined_instances now list both the instance and a reason.
 - Support for Erlang/OTP 24
 - The `application` metadata returned with statuses is no longer hardcoded. Apps that want to display these details will now have valid data for new posts after this change.
 - HTTPSecurityPlug now sends a response header to opt out of Google's FLoC (Federated Learning of Cohorts) targeted advertising.
@@ -34,6 +35,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - AdminAPI: return `created_at` date with users.
 - `AnalyzeMetadata` upload filter for extracting image/video attachment dimensions and generating blurhashes for images. Blurhashes for videos are not generated at this time.
 - Attachment dimensions and blurhashes are federated when available.
+- Mastodon API: support `poll` notification.
 - Pinned posts federation
 
 ### Fixed
index b50c910b12b985ed1c8f6311fc6b8a25c6f8c550..828fe00857518594c69495ae44f705c9debd8b71 100644 (file)
@@ -560,6 +560,7 @@ config :pleroma, Oban,
     mailer: 10,
     transmogrifier: 20,
     scheduled_activities: 10,
+    poll_notifications: 10,
     background: 5,
     remote_fetcher: 2,
     attachments_cleanup: 1,
index 934a62a6293d77479affb6649320045d13ab8e24..c72231faa5ef479b117e413290d96fc9d16ef025 100644 (file)
@@ -687,12 +687,14 @@ config :pleroma, :config_description, [
       },
       %{
         key: :quarantined_instances,
-        type: {:list, :string},
+        type: {:list, :tuple},
+        key_placeholder: "instance",
+        value_placeholder: "reason",
         description:
-          "List of ActivityPub instances where private (DMs, followers-only) activities will not be sent",
+          "List of ActivityPub instances where private (DMs, followers-only) activities will not be sent and the reason for doing so",
         suggestions: [
-          "quarantined.com",
-          "*.quarantined.com"
+          {"quarantined.com", "Reason"},
+          {"*.quarantined.com", "Reason"}
         ]
       },
       %{
index 5b49185dc2c056dfc3cc4353f03f8fef893b079c..d3c9c5716e7928d69fc905f897f65590a30b29b3 100644 (file)
@@ -39,7 +39,7 @@ To add configuration to your config file, you can copy it from the base config.
 * `federation_reachability_timeout_days`: Timeout (in days) of each external federation target being unreachable prior to pausing federating to it.
 * `allow_relay`: Permits remote instances to subscribe to all public posts of your instance. This may increase the visibility of your instance.
 * `public`: Makes the client API in authenticated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network. Note that there is a dependent setting restricting or allowing unauthenticated access to specific resources, see `restrict_unauthenticated` for more details.
-* `quarantined_instances`: List of ActivityPub instances where private (DMs, followers-only) activities will not be send.
+* `quarantined_instances`: ActivityPub instances where private (DMs, followers-only) activities will not be send.
 * `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML).
 * `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.
@@ -135,15 +135,16 @@ To add configuration to your config file, you can copy it from the base config.
     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
-* `media_removal`: List of instances to remove media from.
-* `media_nsfw`: List of instances to put media as NSFW(sensitive) from.
-* `federated_timeline_removal`: List of instances to remove from Federated (aka The Whole Known Network) Timeline.
-* `reject`: List of instances to reject any activities from.
-* `accept`: List of instances to accept any activities from.
-* `followers_only`: List of instances to decrease post visibility to only the followers, including for DM mentions.
-* `report_removal`: List of instances to reject reports from.
-* `avatar_removal`: List of instances to strip avatars from.
-* `banner_removal`: List of instances to strip banners from.
+* `media_removal`: List of instances to strip media attachments from and the reason for doing so.
+* `media_nsfw`: List of instances to tag all media as NSFW (sensitive) from and the reason for doing so.
+* `federated_timeline_removal`: List of instances to remove from the Federated Timeline (aka The Whole Known Network) and the reason for doing so.
+* `reject`: List of instances to reject activities (except deletes) from and the reason for doing so.
+* `accept`: List of instances to only accept activities (except deletes) from and the reason for doing so.
+* `followers_only`: Force posts from the given instances to be visible by followers only and the reason for doing so.
+* `report_removal`: List of instances to reject reports from and the reason for doing so.
+* `avatar_removal`: List of instances to strip avatars from and the reason for doing so.
+* `banner_removal`: List of instances to strip banners from and the reason for doing so.
+* `reject_deletes`: List of instances to reject deletions from and the reason for doing so.
 
 #### :mrf_subchain
 This policy processes messages through an alternate pipeline when a given message matches certain criteria.
index 5618634a20d4a2ffedb7866de0c5be8981038b91..a31c26b9c1812b2aec33c9a810cbb00cef4e1358 100644 (file)
@@ -55,18 +55,18 @@ Servers should be configured as lists.
 
 ### Example
 
-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`. We also give a reason why the moderation was done:
 
 ```elixir
 config :pleroma, :mrf,
   policies: [Pleroma.Web.ActivityPub.MRF.SimplePolicy]
 
 config :pleroma, :mrf_simple,
-  media_removal: ["illegalporn.biz"],
-  media_nsfw: ["porn.biz", "porn.business"],
-  reject: ["spam.com"],
-  federated_timeline_removal: ["spam.university"],
-  report_removal: ["whiny.whiner"]
+  media_removal: [{"illegalporn.biz", "Media can contain illegal contant"}],
+  media_nsfw: [{"porn.biz", "unmarked nsfw media"}, {"porn.business", "A lot of unmarked nsfw media"}],
+  reject: [{"spam.com", "They keep spamming our users"}],
+  federated_timeline_removal: [{"spam.university", "Annoying low-quality posts who otherwise fill up TWKN"}],
+  report_removal: [{"whiny.whiner", "Keep spamming us with irrelevant reports"}]
 ```
 
 ### Use with Care
index 13395ff25ebb4c3c86e4b33143c873f66c0ca978..c37ff0c636c2c40002cca5d76a6fc776d4fa4b83 100644 (file)
@@ -1,4 +1,7 @@
 # Installing on Alpine Linux
+
+{! backend/installation/otp_vs_from_source_source.include !}
+
 ## Installation
 
 This guide is a step-by-step installation guide for Alpine Linux. The instructions were verified against Alpine v3.10 standard image. You might miss additional dependencies if you use `netboot` instead.
index d11deb6210cc1e0c70c74db4fcbe61d41c5a150f..285743d564197cc00bbf1af7ef0762f1971603e8 100644 (file)
@@ -1,4 +1,7 @@
 # Installing on Arch Linux
+
+{! backend/installation/otp_vs_from_source_source.include !}
+
 ## Installation
 
 This guide will assume that you have administrative rights, either as root or a user with [sudo permissions](https://wiki.archlinux.org/index.php/Sudo). If you want to run this guide with root, ignore the `sudo` at the beginning of the lines, unless it calls a user like `sudo -Hu pleroma`; in this case, use `su <username> -s $SHELL -c 'command'` instead.
index 02682e5b0c4aebea784a54712fc7ca699aef0455..4e52b2155b6de0c3055d2d13b08ed1055fd4e211 100644 (file)
@@ -1,4 +1,7 @@
 # Installing on Debian Based Distributions
+
+{! backend/installation/otp_vs_from_source_source.include !}
+
 ## Installation
 
 This guide will assume you are on Debian 11 (“bullseye”) or later. This guide should also work with Ubuntu 18.04 (“Bionic Beaver”) and later. It also assumes that you have administrative rights, either as root or a user with [sudo permissions](https://www.digitalocean.com/community/tutorials/how-to-add-delete-and-grant-sudo-privileges-to-users-on-a-debian-vps). If you want to run this guide with root, ignore the `sudo` at the beginning of the lines, unless it calls a user like `sudo -Hu pleroma`; in this case, use `su <username> -s $SHELL -c 'command'` instead.
index 982ab52d2b9ef5d530206edda4dcd772d6ec3bd8..36882c8c86bf88d7ced69882fcba70cb95a2983c 100644 (file)
@@ -1,4 +1,7 @@
 # Installing on Gentoo GNU/Linux
+
+{! backend/installation/otp_vs_from_source_source.include !}
+
 ## Installation
 
 This guide will assume that you have administrative rights, either as root or a user with [sudo permissions](https://wiki.gentoo.org/wiki/Sudo). Lines that begin with `#` indicate that they should be run as the superuser. Lines using `$` should be run as the indicated user, e.g. `pleroma$` should be run as the `pleroma` user.
index d303a6daf4807b681eb2334030b65be6a2786cac..e4a01d8db37c02cbcfebdb9aa60069bc852cfa40 100644 (file)
@@ -1,7 +1,8 @@
 # Switching a from-source install to OTP releases
 
-## What are OTP releases?
-OTP releases are as close as you can get to binary releases with Erlang/Elixir. The release is self-contained, and provides everything needed to boot it, it is easily administered via the provided shell script to open up a remote console, start/stop/restart the release, start in the background, send remote commands, and more.
+{! backend/installation/otp_vs_from_source.include !}
+
+In this guide we cover how you can migrate from a from source installation to one using OTP releases.
 
 ## Pre-requisites
 You will be running commands as root. If you aren't root already, please elevate your priviledges by executing `sudo su`/`su`.
index 3f67534ac65c4b1a6b2a5ee4a79e1e843634ab86..0861a8157fb59c3938da1627498ae3033be40476 100644 (file)
@@ -1,5 +1,9 @@
 # Installing on Linux using OTP releases
 
+{! backend/installation/otp_vs_from_source.include !}
+
+This guide covers a installation using an OTP release. To install Pleroma from source, please check out the corresponding guide for your distro.
+
 ## Pre-requisites
 * A machine running Linux with GNU (e.g. Debian, Ubuntu) or musl (e.g. Alpine) libc and `x86_64`, `aarch64` or `armv7l` CPU, you have root access to. If you are not sure if it's compatible see [Detecting flavour section](#detecting-flavour) below
 * A (sub)domain pointed to the machine
diff --git a/docs/installation/otp_vs_from_source.include b/docs/installation/otp_vs_from_source.include
new file mode 100644 (file)
index 0000000..63e837a
--- /dev/null
@@ -0,0 +1,3 @@
+## OTP releases vs from-source installations
+
+There are two ways to install Pleroma. You can use OTP releases or do a from-source installation. OTP releases are as close as you can get to binary releases with Erlang/Elixir. The release is self-contained, and provides everything needed to boot it, it is easily administered via the provided shell script to open up a remote console, start/stop/restart the release, start in the background, send remote commands, and more. With from source installations you install Pleroma from source, meaning you have to install certain dependencies like Erlang+Elixir and compile Pleroma yourself.
diff --git a/docs/installation/otp_vs_from_source_source.include b/docs/installation/otp_vs_from_source_source.include
new file mode 100644 (file)
index 0000000..63482b6
--- /dev/null
@@ -0,0 +1,3 @@
+{! backend/installation/otp_vs_from_source.include !}
+
+This guide covers a from-source installation. To install using OTP releases, please check out [the OTP guide](./otp_en.md).
index fedd58a7ef584bc09e9da5467d4583b80edde24e..029ee8b6526c5166fa6d2e9a8b49a6e65403a569 100644 (file)
@@ -20,6 +20,140 @@ defmodule Pleroma.Config.DeprecationWarnings do
      "\n* `config :pleroma, :instance, mrf_transparency_exclusions` is now `config :pleroma, :mrf, transparency_exclusions`"}
   ]
 
+  def check_simple_policy_tuples do
+    has_strings =
+      Config.get([:mrf_simple])
+      |> Enum.any?(fn {_, v} -> Enum.any?(v, &is_binary/1) end)
+
+    if has_strings do
+      Logger.warn("""
+      !!!DEPRECATION WARNING!!!
+      Your config is using strings in the SimplePolicy configuration instead of tuples. They should work for now, but you are advised to change to the new configuration to prevent possible issues later:
+
+      ```
+      config :pleroma, :mrf_simple,
+        media_removal: ["instance.tld"],
+        media_nsfw: ["instance.tld"],
+        federated_timeline_removal: ["instance.tld"],
+        report_removal: ["instance.tld"],
+        reject: ["instance.tld"],
+        followers_only: ["instance.tld"],
+        accept: ["instance.tld"],
+        avatar_removal: ["instance.tld"],
+        banner_removal: ["instance.tld"],
+        reject_deletes: ["instance.tld"]
+      ```
+
+      Is now
+
+
+      ```
+      config :pleroma, :mrf_simple,
+        media_removal: [{"instance.tld", "Reason for media removal"}],
+        media_nsfw: [{"instance.tld", "Reason for media nsfw"}],
+        federated_timeline_removal: [{"instance.tld", "Reason for federated timeline removal"}],
+        report_removal: [{"instance.tld", "Reason for report removal"}],
+        reject: [{"instance.tld", "Reason for reject"}],
+        followers_only: [{"instance.tld", "Reason for followers only"}],
+        accept: [{"instance.tld", "Reason for accept"}],
+        avatar_removal: [{"instance.tld", "Reason for avatar removal"}],
+        banner_removal: [{"instance.tld", "Reason for banner removal"}],
+        reject_deletes: [{"instance.tld", "Reason for reject deletes"}]
+      ```
+      """)
+
+      new_config =
+        Config.get([:mrf_simple])
+        |> Enum.map(fn {k, v} ->
+          {k,
+           Enum.map(v, fn
+             {instance, reason} -> {instance, reason}
+             instance -> {instance, ""}
+           end)}
+        end)
+
+      Config.put([:mrf_simple], new_config)
+
+      :error
+    else
+      :ok
+    end
+  end
+
+  def check_quarantined_instances_tuples do
+    has_strings = Config.get([:instance, :quarantined_instances]) |> Enum.any?(&is_binary/1)
+
+    if has_strings do
+      Logger.warn("""
+      !!!DEPRECATION WARNING!!!
+      Your config is using strings in the quarantined_instances configuration instead of tuples. They should work for now, but you are advised to change to the new configuration to prevent possible issues later:
+
+      ```
+      config :pleroma, :instance,
+        quarantined_instances: ["instance.tld"]
+      ```
+
+      Is now
+
+
+      ```
+      config :pleroma, :instance,
+        quarantined_instances: [{"instance.tld", "Reason for quarantine"}]
+      ```
+      """)
+
+      new_config =
+        Config.get([:instance, :quarantined_instances])
+        |> Enum.map(fn
+          {instance, reason} -> {instance, reason}
+          instance -> {instance, ""}
+        end)
+
+      Config.put([:instance, :quarantined_instances], new_config)
+
+      :error
+    else
+      :ok
+    end
+  end
+
+  def check_transparency_exclusions_tuples do
+    has_strings = Config.get([:mrf, :transparency_exclusions]) |> Enum.any?(&is_binary/1)
+
+    if has_strings do
+      Logger.warn("""
+      !!!DEPRECATION WARNING!!!
+      Your config is using strings in the transparency_exclusions configuration instead of tuples. They should work for now, but you are advised to change to the new configuration to prevent possible issues later:
+
+      ```
+      config :pleroma, :mrf,
+        transparency_exclusions: ["instance.tld"]
+      ```
+
+      Is now
+
+
+      ```
+      config :pleroma, :mrf,
+        transparency_exclusions: [{"instance.tld", "Reason to exlude transparency"}]
+      ```
+      """)
+
+      new_config =
+        Config.get([:mrf, :transparency_exclusions])
+        |> Enum.map(fn
+          {instance, reason} -> {instance, reason}
+          instance -> {instance, ""}
+        end)
+
+      Config.put([:mrf, :transparency_exclusions], new_config)
+
+      :error
+    else
+      :ok
+    end
+  end
+
   def check_hellthread_threshold do
     if Config.get([:mrf_hellthread, :threshold]) do
       Logger.warn("""
@@ -34,20 +168,24 @@ defmodule Pleroma.Config.DeprecationWarnings do
   end
 
   def warn do
-    with :ok <- check_hellthread_threshold(),
-         :ok <- check_old_mrf_config(),
-         :ok <- check_media_proxy_whitelist_config(),
-         :ok <- check_welcome_message_config(),
-         :ok <- check_gun_pool_options(),
-         :ok <- check_activity_expiration_config(),
-         :ok <- check_remote_ip_plug_name(),
-         :ok <- check_uploders_s3_public_endpoint(),
-         :ok <- check_old_chat_shoutbox() do
-      :ok
-    else
-      _ ->
-        :error
-    end
+    [
+      check_hellthread_threshold(),
+      check_old_mrf_config(),
+      check_media_proxy_whitelist_config(),
+      check_welcome_message_config(),
+      check_gun_pool_options(),
+      check_activity_expiration_config(),
+      check_remote_ip_plug_name(),
+      check_uploders_s3_public_endpoint(),
+      check_old_chat_shoutbox(),
+      check_quarantined_instances_tuples(),
+      check_transparency_exclusions_tuples(),
+      check_simple_policy_tuples()
+    ]
+    |> Enum.reduce(:ok, fn
+      :ok, :ok -> :ok
+      _, _ -> :error
+    end)
   end
 
   def check_welcome_message_config do
index 7efbdc49afe1811a145a78cc548ad0dd54935849..32f13df69c31a6400d77156b6fdfcaba36e0289d 100644 (file)
@@ -72,6 +72,7 @@ defmodule Pleroma.Notification do
     pleroma:emoji_reaction
     pleroma:report
     reblog
+    poll
   }
 
   def changeset(%Notification{} = notification, attrs) do
@@ -379,7 +380,7 @@ defmodule Pleroma.Notification do
     notifications =
       Enum.map(potential_receivers, fn user ->
         do_send = do_send && user in enabled_receivers
-        create_notification(activity, user, do_send)
+        create_notification(activity, user, do_send: do_send)
       end)
       |> Enum.reject(&is_nil/1)
 
@@ -435,15 +436,18 @@ defmodule Pleroma.Notification do
   end
 
   # TODO move to sql, too.
-  def create_notification(%Activity{} = activity, %User{} = user, do_send \\ true) do
-    unless skip?(activity, user) do
+  def create_notification(%Activity{} = activity, %User{} = user, opts \\ []) do
+    do_send = Keyword.get(opts, :do_send, true)
+    type = Keyword.get(opts, :type, type_from_activity(activity))
+
+    unless skip?(activity, user, opts) do
       {:ok, %{notification: notification}} =
         Multi.new()
         |> Multi.insert(:notification, %Notification{
           user_id: user.id,
           activity: activity,
           seen: mark_as_read?(activity, user),
-          type: type_from_activity(activity)
+          type: type
         })
         |> Marker.multi_set_last_read_id(user, "notifications")
         |> Repo.transaction()
@@ -457,6 +461,28 @@ defmodule Pleroma.Notification do
     end
   end
 
+  def create_poll_notifications(%Activity{} = activity) do
+    with %Object{data: %{"type" => "Question", "actor" => actor} = data} <-
+           Object.normalize(activity) do
+      voters =
+        case data do
+          %{"voters" => voters} when is_list(voters) -> voters
+          _ -> []
+        end
+
+      notifications =
+        Enum.reduce([actor | voters], [], fn ap_id, acc ->
+          with %User{local: true} = user <- User.get_by_ap_id(ap_id) do
+            [create_notification(activity, user, type: "poll") | acc]
+          else
+            _ -> acc
+          end
+        end)
+
+      {:ok, notifications}
+    end
+  end
+
   @doc """
   Returns a tuple with 2 elements:
     {notification-enabled receivers, currently disabled receivers (blocking / [thread] muting)}
@@ -572,8 +598,10 @@ defmodule Pleroma.Notification do
     Enum.uniq(ap_ids) -- thread_muter_ap_ids
   end
 
-  @spec skip?(Activity.t(), User.t()) :: boolean()
-  def skip?(%Activity{} = activity, %User{} = user) do
+  def skip?(activity, user, opts \\ [])
+
+  @spec skip?(Activity.t(), User.t(), Keyword.t()) :: boolean()
+  def skip?(%Activity{} = activity, %User{} = user, opts) do
     [
       :self,
       :invisible,
@@ -581,17 +609,21 @@ defmodule Pleroma.Notification do
       :recently_followed,
       :filtered
     ]
-    |> Enum.find(&skip?(&1, activity, user))
+    |> Enum.find(&skip?(&1, activity, user, opts))
   end
 
-  def skip?(_, _), do: false
+  def skip?(_activity, _user, _opts), do: false
 
-  @spec skip?(atom(), Activity.t(), User.t()) :: boolean()
-  def skip?(:self, %Activity{} = activity, %User{} = user) do
-    activity.data["actor"] == user.ap_id
+  @spec skip?(atom(), Activity.t(), User.t(), Keyword.t()) :: boolean()
+  def skip?(:self, %Activity{} = activity, %User{} = user, opts) do
+    cond do
+      opts[:type] == "poll" -> false
+      activity.data["actor"] == user.ap_id -> true
+      true -> false
+    end
   end
 
-  def skip?(:invisible, %Activity{} = activity, _) do
+  def skip?(:invisible, %Activity{} = activity, _user, _opts) do
     actor = activity.data["actor"]
     user = User.get_cached_by_ap_id(actor)
     User.invisible?(user)
@@ -600,15 +632,27 @@ defmodule Pleroma.Notification do
   def skip?(
         :block_from_strangers,
         %Activity{} = activity,
-        %User{notification_settings: %{block_from_strangers: true}} = user
+        %User{notification_settings: %{block_from_strangers: true}} = user,
+        opts
       ) do
     actor = activity.data["actor"]
     follower = User.get_cached_by_ap_id(actor)
-    !User.following?(follower, user)
+
+    cond do
+      opts[:type] == "poll" -> false
+      user.ap_id == actor -> false
+      !User.following?(follower, user) -> true
+      true -> false
+    end
   end
 
   # To do: consider defining recency in hours and checking FollowingRelationship with a single SQL
-  def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity, %User{} = user) do
+  def skip?(
+        :recently_followed,
+        %Activity{data: %{"type" => "Follow"}} = activity,
+        %User{} = user,
+        _opts
+      ) do
     actor = activity.data["actor"]
 
     Notification.for_user(user)
@@ -618,9 +662,10 @@ defmodule Pleroma.Notification do
     end)
   end
 
-  def skip?(:filtered, %{data: %{"type" => type}}, _) when type in ["Follow", "Move"], do: false
+  def skip?(:filtered, %{data: %{"type" => type}}, _user, _opts) when type in ["Follow", "Move"],
+    do: false
 
-  def skip?(:filtered, activity, user) do
+  def skip?(:filtered, activity, user, _opts) do
     object = Object.normalize(activity, fetch: false)
 
     cond do
@@ -638,7 +683,7 @@ defmodule Pleroma.Notification do
     end
   end
 
-  def skip?(_, _, _), do: false
+  def skip?(_type, _activity, _user, _opts), do: false
 
   def mark_as_read?(activity, target_user) do
     user = Activity.user_actor(activity)
index 4c29dda355534fcae437700e130e4128ed0c515e..19961a4a55ea5b9370922d1df63ca0b9330fe00a 100644 (file)
@@ -25,6 +25,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
   alias Pleroma.Web.Streamer
   alias Pleroma.Web.WebFinger
   alias Pleroma.Workers.BackgroundWorker
+  alias Pleroma.Workers.PollWorker
 
   import Ecto.Query
   import Pleroma.Web.ActivityPub.Utils
@@ -288,6 +289,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
          {:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},
          {:ok, _actor} <- increase_note_count_if_public(actor, activity),
          _ <- notify_and_stream(activity),
+         :ok <- maybe_schedule_poll_notifications(activity),
          :ok <- maybe_federate(activity) do
       {:ok, activity}
     else
@@ -302,6 +304,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     end
   end
 
+  defp maybe_schedule_poll_notifications(activity) do
+    PollWorker.schedule_poll_end(activity)
+    :ok
+  end
+
   @spec listen(map()) :: {:ok, Activity.t()} | {:error, any()}
   def listen(%{to: to, actor: actor, context: context, object: object} = params) do
     additional = params[:additional] || %{}
index ac00fa54ba27f541851edcf939ed25c54946cb2d..23ea039c34b24af5903c5a14d4338930887ab39c 100644 (file)
@@ -33,9 +33,11 @@ defmodule Pleroma.Web.ActivityPub.MRF do
         %{
           key: :transparency_exclusions,
           label: "MRF transparency exclusions",
-          type: {:list, :string},
+          type: {:list, :tuple},
+          key_placeholder: "instance",
+          value_placeholder: "reason",
           description:
-            "Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.",
+            "Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value. You can also provide a reason for excluding these instance names. The instances and reasons won't be publicly disclosed.",
           suggestions: [
             "exclusion.com"
           ]
@@ -100,6 +102,11 @@ defmodule Pleroma.Web.ActivityPub.MRF do
     Enum.any?(domains, fn domain -> Regex.match?(domain, host) end)
   end
 
+  @spec instance_list_from_tuples([{String.t(), String.t()}]) :: [String.t()]
+  def instance_list_from_tuples(list) do
+    Enum.map(list, fn {instance, _} -> instance end)
+  end
+
   def describe(policies) do
     {:ok, policy_configs} =
       policies
index 646008dd9abb5b033e9516b48fddf8afdcf1cdd8..1383fa757365836c2d102212f2c0c9868cb9ff62 100644 (file)
@@ -159,6 +159,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
         %{
           key: :replace,
           type: {:list, :tuple},
+          key_placeholder: "instance",
+          value_placeholder: "reason",
           description: """
             **Pattern**: a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
 
index 9a211fd447fc4d9020dd38f9e880b226c7344de7..02c9b18ed5eb5203f3496577e3ff4c9ad80c07d8 100644 (file)
@@ -49,6 +49,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
           message
           |> Map.put("to", to)
           |> Map.put("cc", cc)
+          |> Kernel.put_in(["object", "to"], to)
+          |> Kernel.put_in(["object", "cc"], cc)
 
         {:ok, message}
       else
@@ -70,6 +72,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
           message
           |> Map.put("to", to)
           |> Map.put("cc", cc)
+          |> Kernel.put_in(["object", "to"], to)
+          |> Kernel.put_in(["object", "cc"], cc)
 
         {:ok, message}
       else
@@ -82,7 +86,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
   end
 
   @impl true
-  def filter(%{"type" => "Create", "published" => _} = message) do
+  def filter(%{"type" => "Create", "object" => %{"published" => _}} = message) do
     with actions <- Config.get([:mrf_object_age, :actions]),
          {:reject, _} <- check_date(message),
          {:ok, message} <- check_reject(message, actions),
index b9d3e52c7eeb94ac8223fec06caa45da6e313941..dbb7ca0df7b08c0a41af63f21fa697cd8bbd64e0 100644 (file)
@@ -47,7 +47,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do
 
   @impl true
   def describe,
-    do: {:ok, %{mrf_rejectnonpublic: Config.get(:mrf_rejectnonpublic) |> Enum.into(%{})}}
+    do: {:ok, %{mrf_rejectnonpublic: Config.get(:mrf_rejectnonpublic) |> Map.new()}}
 
   @impl true
   def config_description do
index 30562ac085bd193d44cd3f78bc43f4dd581c2a08..c631cc85fad4c814107a8c6d369dfcf66cd82537 100644 (file)
@@ -15,7 +15,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
 
   defp check_accept(%{host: actor_host} = _actor_info, object) do
     accepts =
-      Config.get([:mrf_simple, :accept])
+      instance_list(:accept)
       |> MRF.subdomains_regex()
 
     cond do
@@ -28,7 +28,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
 
   defp check_reject(%{host: actor_host} = _actor_info, object) do
     rejects =
-      Config.get([:mrf_simple, :reject])
+      instance_list(:reject)
       |> MRF.subdomains_regex()
 
     if MRF.subdomain_match?(rejects, actor_host) do
@@ -44,7 +44,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
        )
        when length(child_attachment) > 0 do
     media_removal =
-      Config.get([:mrf_simple, :media_removal])
+      instance_list(:media_removal)
       |> MRF.subdomains_regex()
 
     object =
@@ -68,7 +68,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
          } = object
        ) do
     media_nsfw =
-      Config.get([:mrf_simple, :media_nsfw])
+      instance_list(:media_nsfw)
       |> MRF.subdomains_regex()
 
     object =
@@ -85,7 +85,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
 
   defp check_ftl_removal(%{host: actor_host} = _actor_info, object) do
     timeline_removal =
-      Config.get([:mrf_simple, :federated_timeline_removal])
+      instance_list(:federated_timeline_removal)
       |> MRF.subdomains_regex()
 
     object =
@@ -112,7 +112,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
 
   defp check_followers_only(%{host: actor_host} = _actor_info, object) do
     followers_only =
-      Config.get([:mrf_simple, :followers_only])
+      instance_list(:followers_only)
       |> MRF.subdomains_regex()
 
     object =
@@ -137,7 +137,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
 
   defp check_report_removal(%{host: actor_host} = _actor_info, %{"type" => "Flag"} = object) do
     report_removal =
-      Config.get([:mrf_simple, :report_removal])
+      instance_list(:report_removal)
       |> MRF.subdomains_regex()
 
     if MRF.subdomain_match?(report_removal, actor_host) do
@@ -151,7 +151,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
 
   defp check_avatar_removal(%{host: actor_host} = _actor_info, %{"icon" => _icon} = object) do
     avatar_removal =
-      Config.get([:mrf_simple, :avatar_removal])
+      instance_list(:avatar_removal)
       |> MRF.subdomains_regex()
 
     if MRF.subdomain_match?(avatar_removal, actor_host) do
@@ -165,7 +165,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
 
   defp check_banner_removal(%{host: actor_host} = _actor_info, %{"image" => _image} = object) do
     banner_removal =
-      Config.get([:mrf_simple, :banner_removal])
+      instance_list(:banner_removal)
       |> MRF.subdomains_regex()
 
     if MRF.subdomain_match?(banner_removal, actor_host) do
@@ -185,12 +185,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
 
   defp check_object(object), do: {:ok, object}
 
+  defp instance_list(config_key) do
+    Config.get([:mrf_simple, config_key])
+    |> MRF.instance_list_from_tuples()
+  end
+
   @impl true
   def filter(%{"type" => "Delete", "actor" => actor} = object) do
     %{host: actor_host} = URI.parse(actor)
 
     reject_deletes =
-      Config.get([:mrf_simple, :reject_deletes])
+      instance_list(:reject_deletes)
       |> MRF.subdomains_regex()
 
     if MRF.subdomain_match?(reject_deletes, actor_host) do
@@ -253,14 +258,42 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
 
   @impl true
   def describe do
-    exclusions = Config.get([:mrf, :transparency_exclusions])
+    exclusions = Config.get([:mrf, :transparency_exclusions]) |> MRF.instance_list_from_tuples()
 
-    mrf_simple =
+    mrf_simple_excluded =
       Config.get(:mrf_simple)
-      |> Enum.map(fn {k, v} -> {k, Enum.reject(v, fn v -> v in exclusions end)} end)
-      |> Enum.into(%{})
+      |> Enum.map(fn {rule, instances} ->
+        {rule, Enum.reject(instances, fn {host, _} -> host in exclusions end)}
+      end)
 
-    {:ok, %{mrf_simple: mrf_simple}}
+    mrf_simple =
+      mrf_simple_excluded
+      |> Enum.map(fn {rule, instances} ->
+        {rule, Enum.map(instances, fn {host, _} -> host end)}
+      end)
+      |> Map.new()
+
+    # This is for backwards compatibility. We originally didn't sent
+    # extra info like a reason why an instance was rejected/quarantined/etc.
+    # Because we didn't want to break backwards compatibility it was decided
+    # to add an extra "info" key.
+    mrf_simple_info =
+      mrf_simple_excluded
+      |> Enum.map(fn {rule, instances} ->
+        {rule, Enum.reject(instances, fn {_, reason} -> reason == "" end)}
+      end)
+      |> Enum.reject(fn {_, instances} -> instances == [] end)
+      |> Enum.map(fn {rule, instances} ->
+        instances =
+          instances
+          |> Enum.map(fn {host, reason} -> {host, %{"reason" => reason}} end)
+          |> Map.new()
+
+        {rule, instances}
+      end)
+      |> Map.new()
+
+    {:ok, %{mrf_simple: mrf_simple, mrf_simple_info: mrf_simple_info}}
   end
 
   @impl true
@@ -270,70 +303,67 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
       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"]
-        }
-      ]
+      children:
+        [
+          %{
+            key: :media_removal,
+            description:
+              "List of instances to strip media attachments from and the reason for doing so"
+          },
+          %{
+            key: :media_nsfw,
+            label: "Media NSFW",
+            description:
+              "List of instances to tag all media as NSFW (sensitive) from and the reason for doing so"
+          },
+          %{
+            key: :federated_timeline_removal,
+            description:
+              "List of instances to remove from the Federated (aka The Whole Known Network) Timeline and the reason for doing so"
+          },
+          %{
+            key: :reject,
+            description:
+              "List of instances to reject activities from (except deletes) and the reason for doing so"
+          },
+          %{
+            key: :accept,
+            description:
+              "List of instances to only accept activities from (except deletes) and the reason for doing so"
+          },
+          %{
+            key: :followers_only,
+            description:
+              "Force posts from the given instances to be visible by followers only and the reason for doing so"
+          },
+          %{
+            key: :report_removal,
+            description: "List of instances to reject reports from and the reason for doing so"
+          },
+          %{
+            key: :avatar_removal,
+            description: "List of instances to strip avatars from and the reason for doing so"
+          },
+          %{
+            key: :banner_removal,
+            description: "List of instances to strip banners from and the reason for doing so"
+          },
+          %{
+            key: :reject_deletes,
+            description: "List of instances to reject deletions from and the reason for doing so"
+          }
+        ]
+        |> Enum.map(fn setting ->
+          Map.merge(
+            setting,
+            %{
+              type: {:list, :tuple},
+              key_placeholder: "instance",
+              value_placeholder: "reason",
+              suggestions: [{"example.com", "Some reason"}, {"*.example.com", "Another reason"}]
+            }
+          )
+        end)
     }
   end
 end
index 1bcb3688ba5716670987d023dca9381076eb6e41..52fb02a84bebd1695bf9541a9dc998587228e4fa 100644 (file)
@@ -37,7 +37,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy do
   def describe do
     mrf_user_allowlist =
       Config.get([:mrf_user_allowlist], [])
-      |> Enum.into(%{}, fn {k, v} -> {k, length(v)} end)
+      |> Map.new(fn {k, v} -> {k, length(v)} end)
 
     {:ok, %{mrf_user_allowlist: mrf_user_allowlist}}
   end
index 20f57f60983b1fa7a09ca47cbaaf8b8f3b67ace0..602e10b44500b6ead903cf8aad7df0b65b94dcf7 100644 (file)
@@ -39,7 +39,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicy do
 
   @impl true
   def describe,
-    do: {:ok, %{mrf_vocabulary: Pleroma.Config.get(:mrf_vocabulary) |> Enum.into(%{})}}
+    do: {:ok, %{mrf_vocabulary: Pleroma.Config.get(:mrf_vocabulary) |> Map.new()}}
 
   @impl true
   def config_description do
index 590beef64a19cdbf89fe17c45a74f3a478cf7c3d..4f29a441136a2377eb3a7f1cd6ea07430c708e86 100644 (file)
@@ -112,6 +112,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
 
       quarantined_instances =
         Config.get([:instance, :quarantined_instances], [])
+        |> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples()
         |> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
 
       !Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
index b0ec84ade17cb44309759b0de73861c114159036..701181a1419e2f12bce3932fba3d0014832019da 100644 (file)
@@ -10,7 +10,6 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
   collection, and so on.
   """
   alias Pleroma.Activity
-  alias Pleroma.Activity.Ir.Topics
   alias Pleroma.Chat
   alias Pleroma.Chat.MessageReference
   alias Pleroma.FollowingRelationship
@@ -24,6 +23,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
   alias Pleroma.Web.ActivityPub.Utils
   alias Pleroma.Web.Push
   alias Pleroma.Web.Streamer
+  alias Pleroma.Workers.PollWorker
 
   require Logger
 
@@ -195,7 +195,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
   # - Set up notifications
   @impl true
   def handle(%{data: %{"type" => "Create"}} = activity, meta) do
-    with {:ok, object, meta} <- handle_object_creation(meta[:object_data], meta),
+    with {:ok, object, meta} <- handle_object_creation(meta[:object_data], activity, meta),
          %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
       {:ok, notifications} = Notification.create_notifications(activity, do_send: false)
       {:ok, _user} = ActivityPub.increase_note_count_if_public(user, object)
@@ -225,6 +225,8 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
         meta
         |> add_notifications(notifications)
 
+      ap_streamer().stream_out(activity)
+
       {:ok, activity, meta}
     else
       e -> Repo.rollback(e)
@@ -245,9 +247,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
     if !User.is_internal_user?(user) do
       Notification.create_notifications(object)
 
-      object
-      |> Topics.get_activity_topics()
-      |> Streamer.stream(object)
+      ap_streamer().stream_out(object)
     end
 
     {:ok, object, meta}
@@ -389,7 +389,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
     {:ok, object, meta}
   end
 
-  def handle_object_creation(%{"type" => "ChatMessage"} = object, meta) do
+  def handle_object_creation(%{"type" => "ChatMessage"} = object, _activity, meta) do
     with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do
       actor = User.get_cached_by_ap_id(object.data["actor"])
       recipient = User.get_cached_by_ap_id(hd(object.data["to"]))
@@ -424,7 +424,14 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
     end
   end
 
-  def handle_object_creation(%{"type" => "Answer"} = object_map, meta) do
+  def handle_object_creation(%{"type" => "Question"} = object, activity, meta) do
+    with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do
+      PollWorker.schedule_poll_end(activity)
+      {:ok, object, meta}
+    end
+  end
+
+  def handle_object_creation(%{"type" => "Answer"} = object_map, _activity, meta) do
     with {:ok, object, meta} <- Pipeline.common_pipeline(object_map, meta) do
       Object.increase_vote_count(
         object.data["inReplyTo"],
@@ -436,15 +443,15 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
     end
   end
 
-  def handle_object_creation(%{"type" => objtype} = object, meta)
-      when objtype in ~w[Audio Video Question Event Article Note Page] do
+  def handle_object_creation(%{"type" => objtype} = object, _activity, meta)
+      when objtype in ~w[Audio Video Event Article Note Page] do
     with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do
       {:ok, object, meta}
     end
   end
 
   # Nothing to do
-  def handle_object_creation(object, meta) do
+  def handle_object_creation(object, _activity, meta) do
     {:ok, object, meta}
   end
 
index ec88eabe1f28d65f57bd590de6eaab77b811a2bc..e4ce42f1cf91540c2c0963950b0b255259551db9 100644 (file)
@@ -195,7 +195,8 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
         "pleroma:chat_mention",
         "pleroma:report",
         "move",
-        "follow_request"
+        "follow_request",
+        "poll"
       ],
       description: """
       The type of event that resulted in the notification.
index 647ba661e7b917151eb904e78a1644dedc0f9eb6..002d6b2cea48762b61eabc927cde7adfacc7da57 100644 (file)
@@ -50,6 +50,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
     favourite
     move
     pleroma:emoji_reaction
+    poll
   }
   def index(%{assigns: %{user: user}} = conn, params) do
     params =
index 3528185d50345e102edc18686db70bde420cd08f..ef208062bdb121f12ecd77be6c5d5d38979d4587 100644 (file)
@@ -95,7 +95,20 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
       {:ok, data} = MRF.describe()
 
       data
-      |> Map.merge(%{quarantined_instances: quarantined})
+      |> Map.put(
+        :quarantined_instances,
+        Enum.map(quarantined, fn {instance, _reason} -> instance end)
+      )
+      # This is for backwards compatibility. We originally didn't sent
+      # extra info like a reason why an instance was rejected/quarantined/etc.
+      # Because we didn't want to break backwards compatibility it was decided
+      # to add an extra "info" key.
+      |> Map.put(:quarantined_instances_info, %{
+        "quarantined_instances" =>
+          quarantined
+          |> Enum.map(fn {instance, reason} -> {instance, %{"reason" => reason}} end)
+          |> Map.new()
+      })
     else
       %{}
     end
index df9bedfed478b894a1b22bde6eb7912af7b3972b..35c636d4e8c24679ae5be3a5c87ecbd703d49272 100644 (file)
@@ -112,6 +112,9 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
       "move" ->
         put_target(response, activity, reading_user, %{})
 
+      "poll" ->
+        put_status(response, activity, reading_user, status_render_opts)
+
       "pleroma:emoji_reaction" ->
         response
         |> put_status(parent_activity_fn.(), reading_user, status_render_opts)
index 4f6c9bc9f731397794212ffb85103cc888cffe73..35bf2e2236dc61f382e5684ed3ef7946a9e9ff4e 100644 (file)
@@ -26,7 +26,7 @@ defmodule Pleroma.Web.Push.Subscription do
   end
 
   # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
-  @supported_alert_types ~w[follow favourite mention reblog pleroma:chat_mention pleroma:emoji_reaction]a
+  @supported_alert_types ~w[follow favourite mention reblog poll pleroma:chat_mention pleroma:emoji_reaction]a
 
   defp alerts(%{data: %{alerts: alerts}}) do
     alerts = Map.take(alerts, @supported_alert_types)
diff --git a/lib/pleroma/workers/poll_worker.ex b/lib/pleroma/workers/poll_worker.ex
new file mode 100644 (file)
index 0000000..3423cc8
--- /dev/null
@@ -0,0 +1,45 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Workers.PollWorker do
+  @moduledoc """
+  Generates notifications when a poll ends.
+  """
+  use Pleroma.Workers.WorkerHelper, queue: "poll_notifications"
+
+  alias Pleroma.Activity
+  alias Pleroma.Notification
+  alias Pleroma.Object
+
+  @impl Oban.Worker
+  def perform(%Job{args: %{"op" => "poll_end", "activity_id" => activity_id}}) do
+    with %Activity{} = activity <- find_poll_activity(activity_id) do
+      Notification.create_poll_notifications(activity)
+    end
+  end
+
+  defp find_poll_activity(activity_id) do
+    with nil <- Activity.get_by_id(activity_id) do
+      {:error, :poll_activity_not_found}
+    end
+  end
+
+  def schedule_poll_end(%Activity{data: %{"type" => "Create"}, id: activity_id} = activity) do
+    with %Object{data: %{"type" => "Question", "closed" => closed}} when is_binary(closed) <-
+           Object.normalize(activity),
+         {:ok, end_time} <- NaiveDateTime.from_iso8601(closed),
+         :gt <- NaiveDateTime.compare(end_time, NaiveDateTime.utc_now()) do
+      %{
+        op: "poll_end",
+        activity_id: activity_id
+      }
+      |> new(scheduled_at: end_time)
+      |> Oban.insert()
+    else
+      _ -> {:error, activity}
+    end
+  end
+
+  def schedule_poll_end(activity), do: {:error, activity}
+end
diff --git a/priv/repo/migrations/20201005123100_simple_policy_string_to_tuple.exs b/priv/repo/migrations/20201005123100_simple_policy_string_to_tuple.exs
new file mode 100644 (file)
index 0000000..77a4a73
--- /dev/null
@@ -0,0 +1,40 @@
+defmodule Pleroma.Repo.Migrations.SimplePolicyStringToTuple do
+  use Ecto.Migration
+
+  alias Pleroma.ConfigDB
+
+  def up, do: ConfigDB.get_by_params(%{group: :pleroma, key: :mrf_simple}) |> update_to_tuples
+  def down, do: ConfigDB.get_by_params(%{group: :pleroma, key: :mrf_simple}) |> update_to_strings
+
+  defp update_to_tuples(%{value: value}) do
+    new_value =
+      value
+      |> Enum.map(fn {k, v} ->
+        {k,
+         Enum.map(v, fn
+           {instance, reason} -> {instance, reason}
+           instance -> {instance, ""}
+         end)}
+      end)
+
+    ConfigDB.update_or_create(%{group: :pleroma, key: :mrf_simple, value: new_value})
+  end
+
+  defp update_to_tuples(nil), do: {:ok, nil}
+
+  defp update_to_strings(%{value: value}) do
+    new_value =
+      value
+      |> Enum.map(fn {k, v} ->
+        {k,
+         Enum.map(v, fn
+           {instance, _} -> instance
+           instance -> instance
+         end)}
+      end)
+
+    ConfigDB.update_or_create(%{group: :pleroma, key: :mrf_simple, value: new_value})
+  end
+
+  defp update_to_strings(nil), do: {:ok, nil}
+end
diff --git a/priv/repo/migrations/20201005124600_quarantained_policy_string_to_tuple.exs b/priv/repo/migrations/20201005124600_quarantained_policy_string_to_tuple.exs
new file mode 100644 (file)
index 0000000..b924e46
--- /dev/null
@@ -0,0 +1,61 @@
+defmodule Pleroma.Repo.Migrations.QuarantainedStringToTuple do
+  use Ecto.Migration
+
+  alias Pleroma.ConfigDB
+
+  def up,
+    do:
+      ConfigDB.get_by_params(%{group: :pleroma, key: :instance})
+      |> update_quarantined_instances_to_tuples
+
+  def down,
+    do:
+      ConfigDB.get_by_params(%{group: :pleroma, key: :instance})
+      |> update_quarantined_instances_to_strings
+
+  defp update_quarantined_instances_to_tuples(%{value: settings}) do
+    settings |> List.keyfind(:quarantined_instances, 0) |> update_to_tuples
+  end
+
+  defp update_quarantined_instances_to_tuples(nil), do: {:ok, nil}
+
+  defp update_to_tuples({:quarantined_instances, instance_list}) do
+    new_value =
+      instance_list
+      |> Enum.map(fn
+        {v, r} -> {v, r}
+        v -> {v, ""}
+      end)
+
+    ConfigDB.update_or_create(%{
+      group: :pleroma,
+      key: :instance,
+      value: [quarantined_instances: new_value]
+    })
+  end
+
+  defp update_to_tuples(nil), do: {:ok, nil}
+
+  defp update_quarantined_instances_to_strings(%{value: settings}) do
+    settings |> List.keyfind(:quarantined_instances, 0) |> update_to_strings
+  end
+
+  defp update_quarantined_instances_to_strings(nil), do: {:ok, nil}
+
+  defp update_to_strings({:quarantined_instances, instance_list}) do
+    new_value =
+      instance_list
+      |> Enum.map(fn
+        {v, _} -> v
+        v -> v
+      end)
+
+    ConfigDB.update_or_create(%{
+      group: :pleroma,
+      key: :instance,
+      value: [quarantined_instances: new_value]
+    })
+  end
+
+  defp update_to_strings(nil), do: {:ok, nil}
+end
diff --git a/priv/repo/migrations/20201005132900_transparency_exclusions_string_to_tuple.exs b/priv/repo/migrations/20201005132900_transparency_exclusions_string_to_tuple.exs
new file mode 100644 (file)
index 0000000..6516083
--- /dev/null
@@ -0,0 +1,61 @@
+defmodule Pleroma.Repo.Migrations.TransparencyExclusionsStringToTuple do
+  use Ecto.Migration
+
+  alias Pleroma.ConfigDB
+
+  def up,
+    do:
+      ConfigDB.get_by_params(%{group: :pleroma, key: :mrf})
+      |> update_transparency_exclusions_instances_to_tuples
+
+  def down,
+    do:
+      ConfigDB.get_by_params(%{group: :pleroma, key: :mrf})
+      |> update_transparency_exclusions_instances_to_strings
+
+  defp update_transparency_exclusions_instances_to_tuples(%{value: settings}) do
+    settings |> List.keyfind(:transparency_exclusions, 0) |> update_to_tuples
+  end
+
+  defp update_transparency_exclusions_instances_to_tuples(nil), do: {:ok, nil}
+
+  defp update_to_tuples({:transparency_exclusions, instance_list}) do
+    new_value =
+      instance_list
+      |> Enum.map(fn
+        {v, r} -> {v, r}
+        v -> {v, ""}
+      end)
+
+    ConfigDB.update_or_create(%{
+      group: :pleroma,
+      key: :mrf,
+      value: [transparency_exclusions: new_value]
+    })
+  end
+
+  defp update_to_tuples(nil), do: {:ok, nil}
+
+  defp update_transparency_exclusions_instances_to_strings(%{value: settings}) do
+    settings |> List.keyfind(:transparency_exclusions, 0) |> update_to_strings
+  end
+
+  defp update_transparency_exclusions_instances_to_strings(nil), do: {:ok, nil}
+
+  defp update_to_strings({:transparency_exclusions, instance_list}) do
+    new_value =
+      instance_list
+      |> Enum.map(fn
+        {v, _} -> v
+        v -> v
+      end)
+
+    ConfigDB.update_or_create(%{
+      group: :pleroma,
+      key: :mrf,
+      value: [transparency_exclusions: new_value]
+    })
+  end
+
+  defp update_to_strings(nil), do: {:ok, nil}
+end
diff --git a/priv/repo/migrations/20210717000000_add_poll_to_notifications_enum.exs b/priv/repo/migrations/20210717000000_add_poll_to_notifications_enum.exs
new file mode 100644 (file)
index 0000000..9abf40b
--- /dev/null
@@ -0,0 +1,49 @@
+defmodule Pleroma.Repo.Migrations.AddPollToNotificationsEnum do
+  use Ecto.Migration
+
+  @disable_ddl_transaction true
+
+  def up do
+    """
+    alter type notification_type add value 'poll'
+    """
+    |> execute()
+  end
+
+  def down do
+    alter table(:notifications) do
+      modify(:type, :string)
+    end
+
+    """
+    delete from notifications where type = 'poll'
+    """
+    |> execute()
+
+    """
+    drop type if exists notification_type
+    """
+    |> execute()
+
+    """
+    create type notification_type as enum (
+      'follow',
+      'follow_request',
+      'mention',
+      'move',
+      'pleroma:emoji_reaction',
+      'pleroma:chat_mention',
+      'reblog',
+      'favourite',
+      'pleroma:report'
+    )
+    """
+    |> execute()
+
+    """
+    alter table notifications
+    alter column type type notification_type using (type::notification_type)
+    """
+    |> execute()
+  end
+end
index ccf86634f0b593f5de77d1be6420f54afcb6f66d..c5e2b20f4d1628083ed50debc0d06a2085ff6897 100644 (file)
@@ -11,6 +11,183 @@ defmodule Pleroma.Config.DeprecationWarningsTest do
   alias Pleroma.Config
   alias Pleroma.Config.DeprecationWarnings
 
+  describe "simple policy tuples" do
+    test "gives warning when there are still strings" do
+      clear_config([:mrf_simple],
+        media_removal: ["some.removal"],
+        media_nsfw: ["some.nsfw"],
+        federated_timeline_removal: ["some.tl.removal"],
+        report_removal: ["some.report.removal"],
+        reject: ["some.reject"],
+        followers_only: ["some.followers.only"],
+        accept: ["some.accept"],
+        avatar_removal: ["some.avatar.removal"],
+        banner_removal: ["some.banner.removal"],
+        reject_deletes: ["some.reject.deletes"]
+      )
+
+      assert capture_log(fn -> DeprecationWarnings.check_simple_policy_tuples() end) =~
+               """
+               !!!DEPRECATION WARNING!!!
+               Your config is using strings in the SimplePolicy configuration instead of tuples. They should work for now, but you are advised to change to the new configuration to prevent possible issues later:
+
+               ```
+               config :pleroma, :mrf_simple,
+                 media_removal: ["instance.tld"],
+                 media_nsfw: ["instance.tld"],
+                 federated_timeline_removal: ["instance.tld"],
+                 report_removal: ["instance.tld"],
+                 reject: ["instance.tld"],
+                 followers_only: ["instance.tld"],
+                 accept: ["instance.tld"],
+                 avatar_removal: ["instance.tld"],
+                 banner_removal: ["instance.tld"],
+                 reject_deletes: ["instance.tld"]
+               ```
+
+               Is now
+
+
+               ```
+               config :pleroma, :mrf_simple,
+                 media_removal: [{"instance.tld", "Reason for media removal"}],
+                 media_nsfw: [{"instance.tld", "Reason for media nsfw"}],
+                 federated_timeline_removal: [{"instance.tld", "Reason for federated timeline removal"}],
+                 report_removal: [{"instance.tld", "Reason for report removal"}],
+                 reject: [{"instance.tld", "Reason for reject"}],
+                 followers_only: [{"instance.tld", "Reason for followers only"}],
+                 accept: [{"instance.tld", "Reason for accept"}],
+                 avatar_removal: [{"instance.tld", "Reason for avatar removal"}],
+                 banner_removal: [{"instance.tld", "Reason for banner removal"}],
+                 reject_deletes: [{"instance.tld", "Reason for reject deletes"}]
+               ```
+               """
+    end
+
+    test "transforms config to tuples" do
+      clear_config([:mrf_simple],
+        media_removal: ["some.removal", {"some.other.instance", "Some reason"}]
+      )
+
+      expected_config = [
+        {:media_removal, [{"some.removal", ""}, {"some.other.instance", "Some reason"}]}
+      ]
+
+      capture_log(fn -> DeprecationWarnings.warn() end)
+
+      assert Config.get([:mrf_simple]) == expected_config
+    end
+
+    test "doesn't give a warning with correct config" do
+      clear_config([:mrf_simple],
+        media_removal: [{"some.removal", ""}, {"some.other.instance", "Some reason"}]
+      )
+
+      assert capture_log(fn -> DeprecationWarnings.check_simple_policy_tuples() end) == ""
+    end
+  end
+
+  describe "quarantined_instances tuples" do
+    test "gives warning when there are still strings" do
+      clear_config([:instance, :quarantined_instances], [
+        {"domain.com", "some reason"},
+        "somedomain.tld"
+      ])
+
+      assert capture_log(fn -> DeprecationWarnings.check_quarantined_instances_tuples() end) =~
+               """
+               !!!DEPRECATION WARNING!!!
+               Your config is using strings in the quarantined_instances configuration instead of tuples. They should work for now, but you are advised to change to the new configuration to prevent possible issues later:
+
+               ```
+               config :pleroma, :instance,
+                 quarantined_instances: ["instance.tld"]
+               ```
+
+               Is now
+
+
+               ```
+               config :pleroma, :instance,
+                 quarantined_instances: [{"instance.tld", "Reason for quarantine"}]
+               ```
+               """
+    end
+
+    test "transforms config to tuples" do
+      clear_config([:instance, :quarantined_instances], [
+        {"domain.com", "some reason"},
+        "some.tld"
+      ])
+
+      expected_config = [{"domain.com", "some reason"}, {"some.tld", ""}]
+
+      capture_log(fn -> DeprecationWarnings.warn() end)
+
+      assert Config.get([:instance, :quarantined_instances]) == expected_config
+    end
+
+    test "doesn't give a warning with correct config" do
+      clear_config([:instance, :quarantined_instances], [
+        {"domain.com", "some reason"},
+        {"some.tld", ""}
+      ])
+
+      assert capture_log(fn -> DeprecationWarnings.check_quarantined_instances_tuples() end) == ""
+    end
+  end
+
+  describe "transparency_exclusions tuples" do
+    test "gives warning when there are still strings" do
+      clear_config([:mrf, :transparency_exclusions], [
+        {"domain.com", "some reason"},
+        "somedomain.tld"
+      ])
+
+      assert capture_log(fn -> DeprecationWarnings.check_transparency_exclusions_tuples() end) =~
+               """
+               !!!DEPRECATION WARNING!!!
+               Your config is using strings in the transparency_exclusions configuration instead of tuples. They should work for now, but you are advised to change to the new configuration to prevent possible issues later:
+
+               ```
+               config :pleroma, :mrf,
+                 transparency_exclusions: ["instance.tld"]
+               ```
+
+               Is now
+
+
+               ```
+               config :pleroma, :mrf,
+                 transparency_exclusions: [{"instance.tld", "Reason to exlude transparency"}]
+               ```
+               """
+    end
+
+    test "transforms config to tuples" do
+      clear_config([:mrf, :transparency_exclusions], [
+        {"domain.com", "some reason"},
+        "some.tld"
+      ])
+
+      expected_config = [{"domain.com", "some reason"}, {"some.tld", ""}]
+
+      capture_log(fn -> DeprecationWarnings.warn() end)
+
+      assert Config.get([:mrf, :transparency_exclusions]) == expected_config
+    end
+
+    test "doesn't give a warning with correct config" do
+      clear_config([:mrf, :transparency_exclusions], [
+        {"domain.com", "some reason"},
+        {"some.tld", ""}
+      ])
+
+      assert capture_log(fn -> DeprecationWarnings.check_transparency_exclusions_tuples() end) ==
+               ""
+    end
+  end
+
   test "check_old_mrf_config/0" do
     clear_config([:instance, :rewrite_policy], [])
     clear_config([:instance, :mrf_transparency], true)
index 85f895f0fab7fa8973822d3429c51522c59063ce..716af496d8074a6e1dd25098c7e8c03fdfd18cfb 100644 (file)
@@ -129,6 +129,19 @@ defmodule Pleroma.NotificationTest do
     end
   end
 
+  test "create_poll_notifications/1" do
+    [user1, user2, user3, _, _] = insert_list(5, :user)
+    question = insert(:question, user: user1)
+    activity = insert(:question_activity, question: question)
+
+    {:ok, _, _} = CommonAPI.vote(user2, question, [0])
+    {:ok, _, _} = CommonAPI.vote(user3, question, [1])
+
+    {:ok, notifications} = Notification.create_poll_notifications(activity)
+
+    assert [user2.id, user3.id, user1.id] == Enum.map(notifications, & &1.user_id)
+  end
+
   describe "CommonApi.post/2 notification-related functionality" do
     test_with_mock "creates but does NOT send notification to blocker user",
                    Push,
index 4021a565da63258b6a480ef06cb7119d92f0fea6..c2ed2c2a3f2b4d47ed07713d72ddaed29beabf97 100644 (file)
@@ -480,7 +480,7 @@ defmodule Pleroma.UserTest do
             )
 
     test "it sends a welcome chat message when Simple policy applied to local instance" do
-      clear_config([:mrf_simple, :media_nsfw], ["localhost"])
+      clear_config([:mrf_simple, :media_nsfw], [{"localhost", ""}])
 
       welcome_user = insert(:user)
       clear_config([:welcome, :chat_message, :enabled], true)
index 137aafd39ed62d2139ac7aec2baae17e0ea21421..2f649a0a4818e731fe7cc04944709331d14c261d 100644 (file)
@@ -22,6 +22,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicyTest do
   defp get_old_message do
     File.read!("test/fixtures/mastodon-post-activity.json")
     |> Jason.decode!()
+    |> Map.drop(["published"])
   end
 
   defp get_new_message do
index 0b0143d09435386e792bf6a2cedf1990923b1579..0a0f51bdbb8c91bf27dc903f216579414e900887 100644 (file)
@@ -33,7 +33,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
     end
 
     test "has a matching host" do
-      clear_config([:mrf_simple, :media_removal], ["remote.instance"])
+      clear_config([:mrf_simple, :media_removal], [{"remote.instance", "Some reason"}])
       media_message = build_media_message()
       local_message = build_local_message()
 
@@ -46,7 +46,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
     end
 
     test "match with wildcard domain" do
-      clear_config([:mrf_simple, :media_removal], ["*.remote.instance"])
+      clear_config([:mrf_simple, :media_removal], [{"*.remote.instance", "Whatever reason"}])
       media_message = build_media_message()
       local_message = build_local_message()
 
@@ -70,7 +70,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
     end
 
     test "has a matching host" do
-      clear_config([:mrf_simple, :media_nsfw], ["remote.instance"])
+      clear_config([:mrf_simple, :media_nsfw], [{"remote.instance", "Whetever"}])
       media_message = build_media_message()
       local_message = build_local_message()
 
@@ -81,7 +81,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
     end
 
     test "match with wildcard domain" do
-      clear_config([:mrf_simple, :media_nsfw], ["*.remote.instance"])
+      clear_config([:mrf_simple, :media_nsfw], [{"*.remote.instance", "yeah yeah"}])
       media_message = build_media_message()
       local_message = build_local_message()
 
@@ -115,7 +115,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
     end
 
     test "has a matching host" do
-      clear_config([:mrf_simple, :report_removal], ["remote.instance"])
+      clear_config([:mrf_simple, :report_removal], [{"remote.instance", "muh"}])
       report_message = build_report_message()
       local_message = build_local_message()
 
@@ -124,7 +124,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
     end
 
     test "match with wildcard domain" do
-      clear_config([:mrf_simple, :report_removal], ["*.remote.instance"])
+      clear_config([:mrf_simple, :report_removal], [{"*.remote.instance", "suya"}])
       report_message = build_report_message()
       local_message = build_local_message()
 
@@ -159,7 +159,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
         |> URI.parse()
         |> Map.fetch!(:host)
 
-      clear_config([:mrf_simple, :federated_timeline_removal], [ftl_message_actor_host])
+      clear_config([:mrf_simple, :federated_timeline_removal], [{ftl_message_actor_host, "uwu"}])
       local_message = build_local_message()
 
       assert {:ok, ftl_message} = SimplePolicy.filter(ftl_message)
@@ -180,7 +180,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
         |> URI.parse()
         |> Map.fetch!(:host)
 
-      clear_config([:mrf_simple, :federated_timeline_removal], ["*." <> ftl_message_actor_host])
+      clear_config([:mrf_simple, :federated_timeline_removal], [
+        {"*." <> ftl_message_actor_host, "owo"}
+      ])
+
       local_message = build_local_message()
 
       assert {:ok, ftl_message} = SimplePolicy.filter(ftl_message)
@@ -203,7 +206,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
 
       ftl_message = Map.put(ftl_message, "cc", [])
 
-      clear_config([:mrf_simple, :federated_timeline_removal], [ftl_message_actor_host])
+      clear_config([:mrf_simple, :federated_timeline_removal], [
+        {ftl_message_actor_host, "spiderwaifu goes 88w88"}
+      ])
 
       assert {:ok, ftl_message} = SimplePolicy.filter(ftl_message)
       refute "https://www.w3.org/ns/activitystreams#Public" in ftl_message["to"]
@@ -232,7 +237,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
     end
 
     test "activity has a matching host" do
-      clear_config([:mrf_simple, :reject], ["remote.instance"])
+      clear_config([:mrf_simple, :reject], [{"remote.instance", ""}])
 
       remote_message = build_remote_message()
 
@@ -240,7 +245,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
     end
 
     test "activity matches with wildcard domain" do
-      clear_config([:mrf_simple, :reject], ["*.remote.instance"])
+      clear_config([:mrf_simple, :reject], [{"*.remote.instance", ""}])
 
       remote_message = build_remote_message()
 
@@ -248,7 +253,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
     end
 
     test "actor has a matching host" do
-      clear_config([:mrf_simple, :reject], ["remote.instance"])
+      clear_config([:mrf_simple, :reject], [{"remote.instance", ""}])
 
       remote_user = build_remote_user()
 
@@ -256,7 +261,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
     end
 
     test "reject Announce when object would be rejected" do
-      clear_config([:mrf_simple, :reject], ["blocked.tld"])
+      clear_config([:mrf_simple, :reject], [{"blocked.tld", ""}])
 
       announce = %{
         "type" => "Announce",
@@ -268,7 +273,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
     end
 
     test "reject by URI object" do
-      clear_config([:mrf_simple, :reject], ["blocked.tld"])
+      clear_config([:mrf_simple, :reject], [{"blocked.tld", ""}])
 
       announce = %{
         "type" => "Announce",
@@ -322,7 +327,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
         |> URI.parse()
         |> Map.fetch!(:host)
 
-      clear_config([:mrf_simple, :followers_only], [actor_domain])
+      clear_config([:mrf_simple, :followers_only], [{actor_domain, ""}])
 
       assert {:ok, new_activity} = SimplePolicy.filter(activity)
       assert actor.follower_address in new_activity["cc"]
@@ -350,7 +355,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
     end
 
     test "is not empty but activity doesn't have a matching host" do
-      clear_config([:mrf_simple, :accept], ["non.matching.remote"])
+      clear_config([:mrf_simple, :accept], [{"non.matching.remote", ""}])
 
       local_message = build_local_message()
       remote_message = build_remote_message()
@@ -360,7 +365,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
     end
 
     test "activity has a matching host" do
-      clear_config([:mrf_simple, :accept], ["remote.instance"])
+      clear_config([:mrf_simple, :accept], [{"remote.instance", ""}])
 
       local_message = build_local_message()
       remote_message = build_remote_message()
@@ -370,7 +375,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
     end
 
     test "activity matches with wildcard domain" do
-      clear_config([:mrf_simple, :accept], ["*.remote.instance"])
+      clear_config([:mrf_simple, :accept], [{"*.remote.instance", ""}])
 
       local_message = build_local_message()
       remote_message = build_remote_message()
@@ -380,7 +385,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
     end
 
     test "actor has a matching host" do
-      clear_config([:mrf_simple, :accept], ["remote.instance"])
+      clear_config([:mrf_simple, :accept], [{"remote.instance", ""}])
 
       remote_user = build_remote_user()
 
@@ -398,7 +403,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
     end
 
     test "is not empty but it doesn't have a matching host" do
-      clear_config([:mrf_simple, :avatar_removal], ["non.matching.remote"])
+      clear_config([:mrf_simple, :avatar_removal], [{"non.matching.remote", ""}])
 
       remote_user = build_remote_user()
 
@@ -406,7 +411,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
     end
 
     test "has a matching host" do
-      clear_config([:mrf_simple, :avatar_removal], ["remote.instance"])
+      clear_config([:mrf_simple, :avatar_removal], [{"remote.instance", ""}])
 
       remote_user = build_remote_user()
       {:ok, filtered} = SimplePolicy.filter(remote_user)
@@ -415,7 +420,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
     end
 
     test "match with wildcard domain" do
-      clear_config([:mrf_simple, :avatar_removal], ["*.remote.instance"])
+      clear_config([:mrf_simple, :avatar_removal], [{"*.remote.instance", ""}])
 
       remote_user = build_remote_user()
       {:ok, filtered} = SimplePolicy.filter(remote_user)
@@ -434,7 +439,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
     end
 
     test "is not empty but it doesn't have a matching host" do
-      clear_config([:mrf_simple, :banner_removal], ["non.matching.remote"])
+      clear_config([:mrf_simple, :banner_removal], [{"non.matching.remote", ""}])
 
       remote_user = build_remote_user()
 
@@ -442,7 +447,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
     end
 
     test "has a matching host" do
-      clear_config([:mrf_simple, :banner_removal], ["remote.instance"])
+      clear_config([:mrf_simple, :banner_removal], [{"remote.instance", ""}])
 
       remote_user = build_remote_user()
       {:ok, filtered} = SimplePolicy.filter(remote_user)
@@ -451,7 +456,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
     end
 
     test "match with wildcard domain" do
-      clear_config([:mrf_simple, :banner_removal], ["*.remote.instance"])
+      clear_config([:mrf_simple, :banner_removal], [{"*.remote.instance", ""}])
 
       remote_user = build_remote_user()
       {:ok, filtered} = SimplePolicy.filter(remote_user)
@@ -464,7 +469,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
     setup do: clear_config([:mrf_simple, :reject_deletes], [])
 
     test "it accepts deletions even from rejected servers" do
-      clear_config([:mrf_simple, :reject], ["remote.instance"])
+      clear_config([:mrf_simple, :reject], [{"remote.instance", ""}])
 
       deletion_message = build_remote_deletion_message()
 
@@ -472,7 +477,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
     end
 
     test "it accepts deletions even from non-whitelisted servers" do
-      clear_config([:mrf_simple, :accept], ["non.matching.remote"])
+      clear_config([:mrf_simple, :accept], [{"non.matching.remote", ""}])
 
       deletion_message = build_remote_deletion_message()
 
@@ -481,10 +486,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
   end
 
   describe "when :reject_deletes is not empty but it doesn't have a matching host" do
-    setup do: clear_config([:mrf_simple, :reject_deletes], ["non.matching.remote"])
+    setup do: clear_config([:mrf_simple, :reject_deletes], [{"non.matching.remote", ""}])
 
     test "it accepts deletions even from rejected servers" do
-      clear_config([:mrf_simple, :reject], ["remote.instance"])
+      clear_config([:mrf_simple, :reject], [{"remote.instance", ""}])
 
       deletion_message = build_remote_deletion_message()
 
@@ -492,7 +497,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
     end
 
     test "it accepts deletions even from non-whitelisted servers" do
-      clear_config([:mrf_simple, :accept], ["non.matching.remote"])
+      clear_config([:mrf_simple, :accept], [{"non.matching.remote", ""}])
 
       deletion_message = build_remote_deletion_message()
 
@@ -501,7 +506,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
   end
 
   describe "when :reject_deletes has a matching host" do
-    setup do: clear_config([:mrf_simple, :reject_deletes], ["remote.instance"])
+    setup do: clear_config([:mrf_simple, :reject_deletes], [{"remote.instance", ""}])
 
     test "it rejects the deletion" do
       deletion_message = build_remote_deletion_message()
@@ -511,7 +516,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
   end
 
   describe "when :reject_deletes match with wildcard domain" do
-    setup do: clear_config([:mrf_simple, :reject_deletes], ["*.remote.instance"])
+    setup do: clear_config([:mrf_simple, :reject_deletes], [{"*.remote.instance", ""}])
 
     test "it rejects the deletion" do
       deletion_message = build_remote_deletion_message()
index 61d308b978a20d16d7e8dd5905e2dcc81391a13a..6ab27bc8676f91d9c4bc234fbae7c1240e99776f 100644 (file)
@@ -63,6 +63,15 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do
     end
   end
 
+  describe "instance_list_from_tuples/1" do
+    test "returns a list of instances from a list of {instance, reason} tuples" do
+      list = [{"some.tld", "a reason"}, {"other.tld", "another reason"}]
+      expected = ["some.tld", "other.tld"]
+
+      assert MRF.instance_list_from_tuples(list) == expected
+    end
+  end
+
   describe "describe/0" do
     test "it works as expected with noop policy" do
       clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.NoOpPolicy])
index 89f3ad411d1b5f7a10d3c0575991be6c3a55762c..b50e22bbe794ccf24733b5ab404849ae3793b1e9 100644 (file)
@@ -267,6 +267,80 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
   end
 
   describe "publish/2" do
+    test_with_mock "doesn't publish a non-public activity to quarantined instances.",
+                   Pleroma.Web.Federator.Publisher,
+                   [:passthrough],
+                   [] do
+      Config.put([:instance, :quarantined_instances], [{"domain.com", "some reason"}])
+
+      follower =
+        insert(:user, %{
+          local: false,
+          inbox: "https://domain.com/users/nick1/inbox",
+          ap_enabled: true
+        })
+
+      actor = insert(:user, follower_address: follower.ap_id)
+
+      {:ok, follower, actor} = Pleroma.User.follow(follower, actor)
+      actor = refresh_record(actor)
+
+      note_activity =
+        insert(:followers_only_note_activity,
+          user: actor,
+          recipients: [follower.ap_id]
+        )
+
+      res = Publisher.publish(actor, note_activity)
+
+      assert res == :ok
+
+      assert not called(
+               Pleroma.Web.Federator.Publisher.enqueue_one(Publisher, %{
+                 inbox: "https://domain.com/users/nick1/inbox",
+                 actor_id: actor.id,
+                 id: note_activity.data["id"]
+               })
+             )
+    end
+
+    test_with_mock "Publishes a non-public activity to non-quarantined instances.",
+                   Pleroma.Web.Federator.Publisher,
+                   [:passthrough],
+                   [] do
+      Config.put([:instance, :quarantined_instances], [{"somedomain.com", "some reason"}])
+
+      follower =
+        insert(:user, %{
+          local: false,
+          inbox: "https://domain.com/users/nick1/inbox",
+          ap_enabled: true
+        })
+
+      actor = insert(:user, follower_address: follower.ap_id)
+
+      {:ok, follower, actor} = Pleroma.User.follow(follower, actor)
+      actor = refresh_record(actor)
+
+      note_activity =
+        insert(:followers_only_note_activity,
+          user: actor,
+          recipients: [follower.ap_id]
+        )
+
+      res = Publisher.publish(actor, note_activity)
+
+      assert res == :ok
+
+      assert called(
+               Pleroma.Web.Federator.Publisher.enqueue_one(Publisher, %{
+                 inbox: "https://domain.com/users/nick1/inbox",
+                 actor_id: actor.id,
+                 id: note_activity.data["id"]
+               })
+             )
+    end
+
     test_with_mock "publishes an activity with BCC to all relevant peers.",
                    Pleroma.Web.Federator.Publisher,
                    [:passthrough],
index 13167f50a8405e1598eca733957f07b3f310caa5..d0988619dcade410815840c611ec9e0585f80621 100644 (file)
@@ -157,6 +157,30 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
     end
   end
 
+  describe "Question objects" do
+    setup do
+      user = insert(:user)
+      question = build(:question, user: user)
+      question_activity = build(:question_activity, question: question)
+      activity_data = Map.put(question_activity.data, "object", question.data["id"])
+      meta = [object_data: question.data, local: false]
+
+      {:ok, activity, meta} = ActivityPub.persist(activity_data, meta)
+
+      %{activity: activity, meta: meta}
+    end
+
+    test "enqueues the poll end", %{activity: activity, meta: meta} do
+      {:ok, activity, meta} = SideEffects.handle(activity, meta)
+
+      assert_enqueued(
+        worker: Pleroma.Workers.PollWorker,
+        args: %{op: "poll_end", activity_id: activity.id},
+        scheduled_at: NaiveDateTime.from_iso8601!(meta[:object_data]["closed"])
+      )
+    end
+  end
+
   describe "delete users with confirmation pending" do
     setup do
       user = insert(:user, is_confirmed: false)
index a5dfd39342e0776954aff9b44233518d7ff55fd6..4a10a5bc488e651ffbe2b1deb683f5fb9e62a02d 100644 (file)
@@ -18,6 +18,7 @@ defmodule Pleroma.Web.CommonAPITest do
   alias Pleroma.Web.ActivityPub.Visibility
   alias Pleroma.Web.AdminAPI.AccountView
   alias Pleroma.Web.CommonAPI
+  alias Pleroma.Workers.PollWorker
 
   import Pleroma.Factory
   import Mock
@@ -48,6 +49,12 @@ defmodule Pleroma.Web.CommonAPITest do
 
       assert object.data["type"] == "Question"
       assert object.data["oneOf"] |> length() == 2
+
+      assert_enqueued(
+        worker: PollWorker,
+        args: %{op: "poll_end", activity_id: activity.id},
+        scheduled_at: NaiveDateTime.from_iso8601!(object.data["closed"])
+      )
     end
   end
 
index d478a81ee7dfbb3a4bc2a0e580bc23a9f3bf548a..ed66d370ab3fdc59ee5cf49a1b9798d04e39a20e 100644 (file)
@@ -16,6 +16,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
   alias Pleroma.Web.ActivityPub.ActivityPub
   alias Pleroma.Web.ActivityPub.Utils
   alias Pleroma.Web.CommonAPI
+  alias Pleroma.Workers.ScheduledActivityWorker
 
   import Pleroma.Factory
 
@@ -705,11 +706,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
         |> json_response_and_validate_schema(200)
 
       assert {:ok, %{id: activity_id}} =
-               perform_job(Pleroma.Workers.ScheduledActivityWorker, %{
+               perform_job(ScheduledActivityWorker, %{
                  activity_id: scheduled_id
                })
 
-      assert Repo.all(Oban.Job) == []
+      refute_enqueued(worker: ScheduledActivityWorker)
 
       object =
         Activity
index 496a688d1f79c6908bb5d8e1ac4dfb9a210c3814..8070c03c950032c64f2b833fd3d14cb638a47937 100644 (file)
@@ -196,6 +196,27 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
     test_notifications_rendering([notification], user, [expected])
   end
 
+  test "Poll notification" do
+    user = insert(:user)
+    activity = insert(:question_activity, user: user)
+    {:ok, [notification]} = Notification.create_poll_notifications(activity)
+
+    expected = %{
+      id: to_string(notification.id),
+      pleroma: %{is_seen: false, is_muted: false},
+      type: "poll",
+      account:
+        AccountView.render("show.json", %{
+          user: user,
+          for: user
+        }),
+      status: StatusView.render("show.json", %{activity: activity, for: user}),
+      created_at: Utils.to_masto_date(notification.inserted_at)
+    }
+
+    test_notifications_rendering([notification], user, [expected])
+  end
+
   test "Report notification" do
     reporting_user = insert(:user)
     reported_user = insert(:user)
index ee6fdaae8141c37e76c7d37d72b5993f38be4f4e..9deceb1b5f02cf04842b8c1cd5e4abdb651a04d0 100644 (file)
@@ -150,37 +150,127 @@ defmodule Pleroma.Web.NodeInfoTest do
            )
   end
 
-  test "it shows MRF transparency data if enabled", %{conn: conn} do
-    clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy])
-    clear_config([:mrf, :transparency], true)
+  describe "Quarantined instances" do
+    setup do
+      clear_config([:mrf, :transparency], true)
+      quarantined_instances = [{"example.com", "reason to quarantine"}]
+      clear_config([:instance, :quarantined_instances], quarantined_instances)
+    end
 
-    simple_config = %{"reject" => ["example.com"]}
-    clear_config(:mrf_simple, simple_config)
+    test "shows quarantined instances data if enabled", %{conn: conn} do
+      expected_config = ["example.com"]
 
-    response =
-      conn
-      |> get("/nodeinfo/2.1.json")
-      |> json_response(:ok)
+      response =
+        conn
+        |> get("/nodeinfo/2.1.json")
+        |> json_response(:ok)
+
+      assert response["metadata"]["federation"]["quarantined_instances"] == expected_config
+    end
+
+    test "shows extra information in the quarantined_info field for relevant entries", %{
+      conn: conn
+    } do
+      clear_config([:mrf, :transparency], true)
 
-    assert response["metadata"]["federation"]["mrf_simple"] == simple_config
+      expected_config = %{
+        "quarantined_instances" => %{
+          "example.com" => %{"reason" => "reason to quarantine"}
+        }
+      }
+
+      response =
+        conn
+        |> get("/nodeinfo/2.1.json")
+        |> json_response(:ok)
+
+      assert response["metadata"]["federation"]["quarantined_instances_info"] == expected_config
+    end
   end
 
-  test "it performs exclusions from MRF transparency data if configured", %{conn: conn} do
-    clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy])
-    clear_config([:mrf, :transparency], true)
-    clear_config([:mrf, :transparency_exclusions], ["other.site"])
+  describe "MRF SimplePolicy" do
+    setup do
+      clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy])
+      clear_config([:mrf, :transparency], true)
+    end
 
-    simple_config = %{"reject" => ["example.com", "other.site"]}
-    clear_config(:mrf_simple, simple_config)
+    test "shows MRF transparency data if enabled", %{conn: conn} do
+      simple_config = %{"reject" => [{"example.com", ""}]}
+      clear_config(:mrf_simple, simple_config)
 
-    expected_config = %{"reject" => ["example.com"]}
+      expected_config = %{"reject" => ["example.com"]}
 
-    response =
-      conn
-      |> get("/nodeinfo/2.1.json")
-      |> json_response(:ok)
+      response =
+        conn
+        |> get("/nodeinfo/2.1.json")
+        |> json_response(:ok)
+
+      assert response["metadata"]["federation"]["mrf_simple"] == expected_config
+    end
 
-    assert response["metadata"]["federation"]["mrf_simple"] == expected_config
-    assert response["metadata"]["federation"]["exclusions"] == true
+    test "performs exclusions from MRF transparency data if configured", %{conn: conn} do
+      clear_config([:mrf, :transparency_exclusions], [
+        {"other.site", "We don't want them to know"}
+      ])
+
+      simple_config = %{"reject" => [{"example.com", ""}, {"other.site", ""}]}
+      clear_config(:mrf_simple, simple_config)
+
+      expected_config = %{"reject" => ["example.com"]}
+
+      response =
+        conn
+        |> get("/nodeinfo/2.1.json")
+        |> json_response(:ok)
+
+      assert response["metadata"]["federation"]["mrf_simple"] == expected_config
+      assert response["metadata"]["federation"]["exclusions"] == true
+    end
+
+    test "shows extra information in the mrf_simple_info field for relevant entries", %{
+      conn: conn
+    } do
+      simple_config = %{
+        media_removal: [{"no.media", "LEEWWWDD >//<"}],
+        media_nsfw: [],
+        federated_timeline_removal: [{"no.ftl", ""}],
+        report_removal: [],
+        reject: [
+          {"example.instance", "Some reason"},
+          {"uwu.owo", "awoo to much"},
+          {"no.reason", ""}
+        ],
+        followers_only: [],
+        accept: [],
+        avatar_removal: [],
+        banner_removal: [],
+        reject_deletes: [
+          {"peak.me", "I want to peak at what they don't want me to see, eheh"}
+        ]
+      }
+
+      clear_config(:mrf_simple, simple_config)
+
+      clear_config([:mrf, :transparency_exclusions], [
+        {"peak.me", "I don't want them to know"}
+      ])
+
+      expected_config = %{
+        "media_removal" => %{
+          "no.media" => %{"reason" => "LEEWWWDD >//<"}
+        },
+        "reject" => %{
+          "example.instance" => %{"reason" => "Some reason"},
+          "uwu.owo" => %{"reason" => "awoo to much"}
+        }
+      }
+
+      response =
+        conn
+        |> get("/nodeinfo/2.1.json")
+        |> json_response(:ok)
+
+      assert response["metadata"]["federation"]["mrf_simple_info"] == expected_config
+    end
   end
 end
index c267dba4ef41140f404ae2902662197c086b37aa..4a78425ce4d2e3ccb21683a032b330cdda4826ec 100644 (file)
@@ -142,6 +142,11 @@ defmodule Pleroma.Factory do
     }
   end
 
+  def followers_only_note_factory(attrs \\ %{}) do
+    %Pleroma.Object{data: data} = note_factory(attrs)
+    %Pleroma.Object{data: Map.merge(data, %{"to" => [data["actor"] <> "/followers"]})}
+  end
+
   def audio_factory(attrs \\ %{}) do
     text = sequence(:text, &"lain radio episode #{&1}")
 
@@ -208,6 +213,38 @@ defmodule Pleroma.Factory do
     }
   end
 
+  def question_factory(attrs \\ %{}) do
+    user = attrs[:user] || insert(:user)
+
+    data = %{
+      "id" => Pleroma.Web.ActivityPub.Utils.generate_object_id(),
+      "type" => "Question",
+      "actor" => user.ap_id,
+      "attributedTo" => user.ap_id,
+      "attachment" => [],
+      "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+      "cc" => [user.follower_address],
+      "context" => Pleroma.Web.ActivityPub.Utils.generate_context_id(),
+      "closed" => DateTime.utc_now() |> DateTime.add(86_400) |> DateTime.to_iso8601(),
+      "oneOf" => [
+        %{
+          "type" => "Note",
+          "name" => "chocolate",
+          "replies" => %{"totalItems" => 0, "type" => "Collection"}
+        },
+        %{
+          "type" => "Note",
+          "name" => "vanilla",
+          "replies" => %{"totalItems" => 0, "type" => "Collection"}
+        }
+      ]
+    }
+
+    %Pleroma.Object{
+      data: merge_attributes(data, Map.get(attrs, :data, %{}))
+    }
+  end
+
   def direct_note_activity_factory do
     dm = insert(:direct_note)
 
@@ -267,6 +304,33 @@ defmodule Pleroma.Factory do
     |> Map.merge(attrs)
   end
 
+  def followers_only_note_activity_factory(attrs \\ %{}) do
+    user = attrs[:user] || insert(:user)
+    note = insert(:followers_only_note, user: user)
+
+    data_attrs = attrs[:data_attrs] || %{}
+    attrs = Map.drop(attrs, [:user, :note, :data_attrs])
+
+    data =
+      %{
+        "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
+        "type" => "Create",
+        "actor" => note.data["actor"],
+        "to" => note.data["to"],
+        "object" => note.data,
+        "published" => DateTime.utc_now() |> DateTime.to_iso8601(),
+        "context" => note.data["context"]
+      }
+      |> Map.merge(data_attrs)
+
+    %Pleroma.Activity{
+      data: data,
+      actor: data["actor"],
+      recipients: data["to"]
+    }
+    |> Map.merge(attrs)
+  end
+
   def note_activity_factory(attrs \\ %{}) do
     user = attrs[:user] || insert(:user)
     note = attrs[:note] || insert(:note, user: user)
@@ -396,6 +460,33 @@ defmodule Pleroma.Factory do
     }
   end
 
+  def question_activity_factory(attrs \\ %{}) do
+    user = attrs[:user] || insert(:user)
+    question = attrs[:question] || insert(:question, user: user)
+
+    data_attrs = attrs[:data_attrs] || %{}
+    attrs = Map.drop(attrs, [:user, :question, :data_attrs])
+
+    data =
+      %{
+        "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
+        "type" => "Create",
+        "actor" => question.data["actor"],
+        "to" => question.data["to"],
+        "object" => question.data["id"],
+        "published" => DateTime.utc_now() |> DateTime.to_iso8601(),
+        "context" => question.data["context"]
+      }
+      |> Map.merge(data_attrs)
+
+    %Pleroma.Activity{
+      data: data,
+      actor: data["actor"],
+      recipients: data["to"]
+    }
+    |> Map.merge(attrs)
+  end
+
   def oauth_app_factory do
     %Pleroma.Web.OAuth.App{
       client_name: sequence(:client_name, &"Some client #{&1}"),