Merge branch 'release/2.4.1' into chores/2.4.1-develop
authorHaelwenn (lanodan) Monnier <contact@hacktivis.me>
Sat, 28 Aug 2021 16:49:04 +0000 (18:49 +0200)
committerHaelwenn (lanodan) Monnier <contact@hacktivis.me>
Sat, 28 Aug 2021 16:49:04 +0000 (18:49 +0200)
57 files changed:
CHANGELOG.md
README.md
config/config.exs
config/description.exs
config/dev.exs
docs/configuration/cheatsheet.md
docs/configuration/mrf.md
docs/installation/yunohost_en.md [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/builder.ex
lib/pleroma/web/activity_pub/mrf.ex
lib/pleroma/web/activity_pub/mrf/keyword_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/object_validator.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/common_api/activity_draft.ex
lib/pleroma/web/common_api/utils.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/plugs/user_is_staff_plug.ex [new file with mode: 0644]
lib/pleroma/web/push/impl.ex
lib/pleroma/web/push/subscription.ex
lib/pleroma/web/router.ex
lib/pleroma/workers/poll_worker.ex [new file with mode: 0644]
mix.exs
priv/gettext/pl/LC_MESSAGES/errors.po
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/builder_test.exs [new file with mode: 0644]
test/pleroma/web/activity_pub/mrf/simple_policy_test.exs
test/pleroma/web/activity_pub/mrf_test.exs
test/pleroma/web/activity_pub/object_validators/chat_validation_test.exs
test/pleroma/web/activity_pub/publisher_test.exs
test/pleroma/web/activity_pub/side_effects_test.exs
test/pleroma/web/activity_pub/transmogrifier/audio_handling_test.exs
test/pleroma/web/activity_pub/transmogrifier/video_handling_test.exs
test/pleroma/web/admin_api/controllers/report_controller_test.exs
test/pleroma/web/common_api/utils_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/pleroma/web/plugs/user_is_staff_plug_test.exs [new file with mode: 0644]
test/support/factory.ex

index 5f26d48ff4a9d7a7dd9afa7500f7dcc026e55496..00335065850416d5ad5781cfb41a50ecdf454cc8 100644 (file)
@@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 ### Added
 
 ### Fixed
+- Subscription(Bell) Notifications: Don't create from Pipeline Ingested replies
 
 ### Removed
 
@@ -39,6 +40,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.
@@ -54,6 +56,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 ba152208963723e5161b21c231cbed1d18cc2e78..25fde90b9400aaea888618bb1964fb128e5b13f1 100644 (file)
--- a/README.md
+++ b/README.md
@@ -30,7 +30,7 @@ If your platform is not supported, or you just want to be able to edit the sourc
 - [OpenBSD (fi)](https://docs-develop.pleroma.social/backend/installation/openbsd_fi/)
 
 ### OS/Distro packages
-Currently Pleroma is not packaged by any OS/Distros, but if you want to package it for one, we can guide you through the process on our [community channels](#community-channels). If you want to change default options in your Pleroma package, please **discuss it with us first**.
+Currently Pleroma is packaged for [YunoHost](https://yunohost.org). If you want to package Pleroma for any OS/Distros, we can guide you through the process on our [community channels](#community-channels). If you want to change default options in your Pleroma package, please **discuss it with us first**.
 
 ### Docker
 While we don’t provide docker files, other people have written very good ones. Take a look at <https://github.com/angristan/docker-pleroma> or <https://glitch.sh/sn0w/pleroma-docker>.
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 6b7ffb0e9064f7c3a19b19622c69398239219955..ab3e83c12027a96efdbbc22430094dac3a0b905e 100644 (file)
@@ -62,6 +62,7 @@ if File.exists?("./config/dev.secret.exs") do
   import_config "dev.secret.exs"
 else
   IO.puts(
+    :stderr,
     "!!! RUNNING IN LOCALHOST DEV MODE! !!!\nFEDERATION WON'T WORK UNTIL YOU CONFIGURE A dev.secret.exs"
   )
 end
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
diff --git a/docs/installation/yunohost_en.md b/docs/installation/yunohost_en.md
new file mode 100644 (file)
index 0000000..4c34e85
--- /dev/null
@@ -0,0 +1,9 @@
+# Installing on Yunohost
+
+[YunoHost](https://yunohost.org) is a server operating system aimed at self-hosting. The YunoHost community maintains a package of Pleroma which allows you to install Pleroma on YunoHost. You can install it via the normal way through the admin web interface, or through the CLI. More information can be found at [the repo of the package](https://github.com/YunoHost-Apps/pleroma_ynh).
+
+## Questions
+
+Questions and problems related to the YunoHost parts can be done through the [regular YunoHost channels](https://yunohost.org/en/help).
+
+For questions about Pleroma, ask in [#pleroma:libera.chat](https://matrix.to/#/#pleroma:libera.chat) via Matrix or **#pleroma** on **libera.chat** via IRC.
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 cde4777103d3330aa3356f604698c1f6fe79345b..647ccf432d9ef60fe953e05332c42025000137c5 100644 (file)
@@ -15,6 +15,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do
   alias Pleroma.Web.ActivityPub.Relay
   alias Pleroma.Web.ActivityPub.Utils
   alias Pleroma.Web.ActivityPub.Visibility
+  alias Pleroma.Web.CommonAPI.ActivityDraft
 
   require Pleroma.Constants
 
@@ -125,6 +126,37 @@ defmodule Pleroma.Web.ActivityPub.Builder do
      |> Pleroma.Maps.put_if_present("context", context), []}
   end
 
+  @spec note(ActivityDraft.t()) :: {:ok, map(), keyword()}
+  def note(%ActivityDraft{} = draft) do
+    data =
+      %{
+        "type" => "Note",
+        "to" => draft.to,
+        "cc" => draft.cc,
+        "content" => draft.content_html,
+        "summary" => draft.summary,
+        "sensitive" => draft.sensitive,
+        "context" => draft.context,
+        "attachment" => draft.attachments,
+        "actor" => draft.user.ap_id,
+        "tag" => Keyword.values(draft.tags) |> Enum.uniq()
+      }
+      |> add_in_reply_to(draft.in_reply_to)
+      |> Map.merge(draft.extra)
+
+    {:ok, data, []}
+  end
+
+  defp add_in_reply_to(object, nil), do: object
+
+  defp add_in_reply_to(object, in_reply_to) do
+    with %Object{} = in_reply_to_object <- Object.normalize(in_reply_to, fetch: false) do
+      Map.put(object, "inReplyTo", in_reply_to_object.data["id"])
+    else
+      _ -> object
+    end
+  end
+
   def chat_message(actor, recipient, content, opts \\ []) do
     basic = %{
       "id" => Utils.generate_object_id(),
index 41592e71e45dec858bddc23b6204b52a9586f928..e4ee8fe827fae7fdf827a517cb247fa708c1e054 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 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 6e40d8b72ce1ad854b35dab9a6a2fc2a089e213c..187cd0cfd6010145ef6c1f2ed4fedaf0082d871e 100644 (file)
@@ -213,6 +213,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
 
   def stringify_keys(object) when is_map(object) do
     object
+    |> Enum.filter(fn {_, v} -> v != nil end)
     |> Map.new(fn {key, val} -> {to_string(key), stringify_keys(val)} end)
   end
 
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 b82a89896b4e5518cbd4f95a4a8c04b8143c7e2b..701181a1419e2f12bce3932fba3d0014832019da 100644 (file)
@@ -23,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
 
@@ -194,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)
@@ -388,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"]))
@@ -423,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"],
@@ -435,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 c691d71d28ff6ca32db11abad6dc71d7813a31a6..b4e3e37aec3bff82db68577af9a768a690dad18d 100644 (file)
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
   alias Pleroma.Activity
   alias Pleroma.Conversation.Participation
   alias Pleroma.Object
+  alias Pleroma.Web.ActivityPub.Builder
   alias Pleroma.Web.CommonAPI
   alias Pleroma.Web.CommonAPI.Utils
 
@@ -213,8 +214,10 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
 
     emoji = Map.merge(emoji, summary_emoji)
 
+    {:ok, note_data, _meta} = Builder.note(draft)
+
     object =
-      Utils.make_note_data(draft)
+      note_data
       |> Map.put("emoji", emoji)
       |> Map.put("source", draft.status)
       |> Map.put("generator", draft.params[:generator])
index 10eb48250e9fb63a4d5f17536fe54cf74743adfa..b6feaf32a6304ac66a37690838cbeb531a27e85a 100644 (file)
@@ -291,33 +291,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do
     |> Formatter.html_escape("text/html")
   end
 
-  def make_note_data(%ActivityDraft{} = draft) do
-    %{
-      "type" => "Note",
-      "to" => draft.to,
-      "cc" => draft.cc,
-      "content" => draft.content_html,
-      "summary" => draft.summary,
-      "sensitive" => draft.sensitive,
-      "context" => draft.context,
-      "attachment" => draft.attachments,
-      "actor" => draft.user.ap_id,
-      "tag" => Keyword.values(draft.tags) |> Enum.uniq()
-    }
-    |> add_in_reply_to(draft.in_reply_to)
-    |> Map.merge(draft.extra)
-  end
-
-  defp add_in_reply_to(object, nil), do: object
-
-  defp add_in_reply_to(object, in_reply_to) do
-    with %Object{} = in_reply_to_object <- Object.normalize(in_reply_to, fetch: false) do
-      Map.put(object, "inReplyTo", in_reply_to_object.data["id"])
-    else
-      _ -> object
-    end
-  end
-
   def format_naive_asctime(date) do
     date |> DateTime.from_naive!("Etc/UTC") |> format_asctime
   end
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)
diff --git a/lib/pleroma/web/plugs/user_is_staff_plug.ex b/lib/pleroma/web/plugs/user_is_staff_plug.ex
new file mode 100644 (file)
index 0000000..49c2d9c
--- /dev/null
@@ -0,0 +1,23 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.UserIsStaffPlug do
+  import Pleroma.Web.TranslationHelpers
+  import Plug.Conn
+
+  alias Pleroma.User
+
+  def init(options) do
+    options
+  end
+
+  def call(%{assigns: %{user: %User{is_admin: true}}} = conn, _), do: conn
+  def call(%{assigns: %{user: %User{is_moderator: true}}} = conn, _), do: conn
+
+  def call(conn, _) do
+    conn
+    |> render_error(:forbidden, "User is not a staff member.")
+    |> halt()
+  end
+end
index 83cbdc870bc877f9b68f65d6db472ba7a332fda0..28e13ef9c791cbfe16e4737099572cfa898cd786 100644 (file)
@@ -124,8 +124,8 @@ defmodule Pleroma.Web.Push.Impl do
 
   def format_body(activity, actor, object, mastodon_type \\ nil)
 
-  def format_body(_activity, actor, %{data: %{"type" => "ChatMessage", "content" => content}}, _) do
-    case content do
+  def format_body(_activity, actor, %{data: %{"type" => "ChatMessage"} = data}, _) do
+    case data["content"] do
       nil -> "@#{actor.nickname}: (Attachment)"
       content -> "@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}"
     end
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)
index efca7078a178344c091e17ac61f748bee68e65ba..74ee23c06c17d4108e1330de1b81fe9f56ca02ca 100644 (file)
@@ -96,10 +96,14 @@ defmodule Pleroma.Web.Router do
     plug(Pleroma.Web.Plugs.AdminSecretAuthenticationPlug)
     plug(:after_auth)
     plug(Pleroma.Web.Plugs.EnsureAuthenticatedPlug)
-    plug(Pleroma.Web.Plugs.UserIsAdminPlug)
+    plug(Pleroma.Web.Plugs.UserIsStaffPlug)
     plug(Pleroma.Web.Plugs.IdempotencyPlug)
   end
 
+  pipeline :require_admin do
+    plug(Pleroma.Web.Plugs.UserIsAdminPlug)
+  end
+
   pipeline :mastodon_html do
     plug(:browser)
     plug(:authenticate)
@@ -160,7 +164,7 @@ defmodule Pleroma.Web.Router do
   end
 
   scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
-    pipe_through(:admin_api)
+    pipe_through([:admin_api, :require_admin])
 
     put("/users/disable_mfa", AdminAPIController, :disable_mfa)
     put("/users/tag", AdminAPIController, :tag_users)
@@ -265,7 +269,7 @@ defmodule Pleroma.Web.Router do
 
   scope "/api/v1/pleroma/emoji", Pleroma.Web.PleromaAPI do
     scope "/pack" do
-      pipe_through(:admin_api)
+      pipe_through([:admin_api, :require_admin])
 
       post("/", EmojiPackController, :create)
       patch("/", EmojiPackController, :update)
@@ -280,7 +284,7 @@ defmodule Pleroma.Web.Router do
 
     # Modifying packs
     scope "/packs" do
-      pipe_through(:admin_api)
+      pipe_through([:admin_api, :require_admin])
 
       get("/import", EmojiPackController, :import_from_filesystem)
       get("/remote", EmojiPackController, :remote)
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/mix.exs b/mix.exs
index 39c79c83b9b00282185944dab7fae8d96a90240b..89c3f847372fe021bb6cb354855e383c453fd4c8 100644 (file)
--- a/mix.exs
+++ b/mix.exs
@@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do
   def project do
     [
       app: :pleroma,
-      version: version("2.4.1"),
+      version: version("2.4.51"),
       elixir: "~> 1.9",
       elixirc_paths: elixirc_paths(Mix.env()),
       compilers: [:phoenix, :gettext] ++ Mix.compilers(),
index 653ea00a162077982760468a31587ba1826711f4..4d689902f1c18f00b30008f00a4fb626119a5512 100644 (file)
@@ -3,8 +3,8 @@ msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
 "POT-Creation-Date: 2020-05-13 16:37+0000\n"
-"PO-Revision-Date: 2020-07-09 14:40+0000\n"
-"Last-Translator: Ben Is <srsbzns@cock.li>\n"
+"PO-Revision-Date: 2021-08-13 15:42+0000\n"
+"Last-Translator: marcin mikołajczak <me@mkljczk.pl>\n"
 "Language-Team: Polish <https://translate.pleroma.social/projects/pleroma/"
 "pleroma/pl/>\n"
 "Language: pl\n"
@@ -13,7 +13,7 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
 "|| n%100>=20) ? 1 : 2;\n"
-"X-Generator: Weblate 4.0.4\n"
+"X-Generator: Weblate 4.6.2\n"
 
 ## This file is a PO Template file.
 ##
@@ -68,49 +68,49 @@ msgstr[2] "powinno mieć %{count} znaków"
 
 msgid "should have %{count} item(s)"
 msgid_plural "should have %{count} item(s)"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
+msgstr[0] "powinno zawierać %{count} element"
+msgstr[1] "powinno zawierać %{count} elementy"
+msgstr[2] "powinno zawierać %{count} elementów"
 
 msgid "should be at least %{count} character(s)"
 msgid_plural "should be at least %{count} character(s)"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
+msgstr[0] "powinno zawierać przynajmniej %{count} znak"
+msgstr[1] "powinno zawierać przynajmniej %{count} znaki"
+msgstr[2] "powinno zawierać przynajmniej %{count} znaków"
 
 msgid "should have at least %{count} item(s)"
 msgid_plural "should have at least %{count} item(s)"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
+msgstr[0] "powinno zawierać przynajmniej %{count} element"
+msgstr[1] "powinno zawierać przynajmniej %{count} elementy"
+msgstr[2] "powinno zawierać przynajmniej %{count} elementów"
 
 msgid "should be at most %{count} character(s)"
 msgid_plural "should be at most %{count} character(s)"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
+msgstr[0] "powinno zawierać najwyżej %{count} znak"
+msgstr[1] "powinno zawierać najwyżej %{count} znaki"
+msgstr[2] "powinno zawierać najwyżej %{count} znaków"
 
 msgid "should have at most %{count} item(s)"
 msgid_plural "should have at most %{count} item(s)"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
+msgstr[0] "powinno zawierać najwyżej %{count} element"
+msgstr[1] "powinno zawierać najwyżej %{count} elementy"
+msgstr[2] "powinno zawierać najwyżej %{count} elementów"
 
 ## From Ecto.Changeset.validate_number/3
 msgid "must be less than %{number}"
-msgstr ""
+msgstr "musi wynosić mniej niż %{number}"
 
 msgid "must be greater than %{number}"
-msgstr ""
+msgstr "musi wynosić więcej niż %{number}"
 
 msgid "must be less than or equal to %{number}"
-msgstr ""
+msgstr "musi być mniejsze lub równe %{number}"
 
 msgid "must be greater than or equal to %{number}"
-msgstr ""
+msgstr "musi być większe lub równe %{number}"
 
 msgid "must be equal to %{number}"
-msgstr ""
+msgstr "musi być równe %{number}"
 
 #: lib/pleroma/web/common_api/common_api.ex:421
 #, elixir-format
@@ -152,7 +152,7 @@ msgstr "Nie znaleziono użytkownika"
 #: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:114
 #, elixir-format
 msgid "Can't get favorites"
-msgstr ""
+msgstr "Nie można uzyskać ulubionych"
 
 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:437
 #, elixir-format
@@ -172,7 +172,7 @@ msgstr "Komentarz może mieć co najwyżej %{max_size} znaków"
 #: lib/pleroma/config/config_db.ex:222
 #, elixir-format
 msgid "Config with params %{params} not found"
-msgstr ""
+msgstr "Nie znaleziono konfiguracji z parametrami %{params}"
 
 #: lib/pleroma/web/common_api/common_api.ex:95
 #, elixir-format
@@ -213,38 +213,38 @@ msgstr "Nie udało się cofnąć powtórzenia"
 #: lib/pleroma/web/common_api/common_api.ex:437
 #, elixir-format
 msgid "Could not update state"
-msgstr ""
+msgstr "Nie można zaktualizować stanu"
 
 #: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:202
 #, elixir-format
 msgid "Error."
-msgstr ""
+msgstr "Błąd."
 
 #: lib/pleroma/web/twitter_api/twitter_api.ex:106
 #, elixir-format
 msgid "Invalid CAPTCHA"
-msgstr ""
+msgstr "Niewłaściwa CAPTCHA"
 
 #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:117
 #: lib/pleroma/web/oauth/oauth_controller.ex:569
 #, elixir-format
 msgid "Invalid credentials"
-msgstr ""
+msgstr "Nieprawidłowe dane uwierzytelniania"
 
 #: lib/pleroma/plugs/ensure_authenticated_plug.ex:38
 #, elixir-format
 msgid "Invalid credentials."
-msgstr ""
+msgstr "Nieprawidłowe dane uwierzytelniania."
 
 #: lib/pleroma/web/common_api/common_api.ex:265
 #, elixir-format
 msgid "Invalid indices"
-msgstr ""
+msgstr "Nieprawidłowe indeksy"
 
 #: lib/pleroma/web/admin_api/admin_api_controller.ex:1147
 #, elixir-format
 msgid "Invalid parameters"
-msgstr ""
+msgstr "Nieprawidłowe parametry"
 
 #: lib/pleroma/web/common_api/utils.ex:411
 #, elixir-format
@@ -307,7 +307,7 @@ msgstr "Coś się zepsuło"
 #: lib/pleroma/web/common_api/activity_draft.ex:107
 #, elixir-format
 msgid "The message visibility must be direct"
-msgstr ""
+msgstr "Widoczność wiadomości musi być „Bezpośrednia”"
 
 #: lib/pleroma/web/common_api/utils.ex:566
 #, elixir-format
@@ -317,17 +317,17 @@ msgstr "Ten status przekracza limit znaków"
 #: lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex:31
 #, elixir-format
 msgid "This resource requires authentication."
-msgstr ""
+msgstr "Ten zasób wymaga uwierzytelnienia."
 
 #: lib/pleroma/plugs/rate_limiter/rate_limiter.ex:206
 #, elixir-format
 msgid "Throttled"
-msgstr ""
+msgstr "Ograniczono"
 
 #: lib/pleroma/web/common_api/common_api.ex:266
 #, elixir-format
 msgid "Too many choices"
-msgstr ""
+msgstr "Zbyt wiele wyborów"
 
 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:442
 #, elixir-format
@@ -349,17 +349,18 @@ msgstr "Twoje konto jest obecnie nieaktywne"
 #: lib/pleroma/web/oauth/oauth_controller.ex:332
 #, elixir-format
 msgid "Your login is missing a confirmed e-mail address"
-msgstr ""
+msgstr "Twój adres e-mail nie został potwierdzony"
 
 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:389
 #, elixir-format
 msgid "can't read inbox of %{nickname} as %{as_nickname}"
-msgstr ""
+msgstr "Nie można odczytać skrzynki odbiorczej %{nickname} jako %{as_nickname}"
 
 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:472
 #, elixir-format
 msgid "can't update outbox of %{nickname} as %{as_nickname}"
 msgstr ""
+"Nie można zaktualizować skrzynki nadawczcej %{nickname} jako %{as_nickname}"
 
 #: lib/pleroma/web/common_api/common_api.ex:388
 #, elixir-format
@@ -405,12 +406,12 @@ msgstr "Nie udało się"
 #: lib/pleroma/web/oauth/oauth_controller.ex:411
 #, elixir-format
 msgid "Failed to authenticate: %{message}."
-msgstr ""
+msgstr "Nie udało się uwierzytelnić: %{message}."
 
 #: lib/pleroma/web/oauth/oauth_controller.ex:442
 #, elixir-format
 msgid "Failed to set up user account."
-msgstr ""
+msgstr "Nie udało się skonfigurować konta użytkownika."
 
 #: lib/pleroma/plugs/oauth_scopes_plug.ex:38
 #, elixir-format
@@ -431,7 +432,7 @@ msgstr "Nieprawidłowa nazwa użytkownika lub hasło"
 #: lib/pleroma/web/twitter_api/twitter_api.ex:118
 #, elixir-format
 msgid "Invalid answer data"
-msgstr ""
+msgstr "Nieprawidłowe dane odpowiedzi"
 
 #: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:128
 #, elixir-format
@@ -441,7 +442,7 @@ msgstr "Nieobsługiwana wersja schematu Nodeinfo"
 #: lib/pleroma/web/oauth/oauth_controller.ex:169
 #, elixir-format
 msgid "This action is outside the authorized scopes"
-msgstr ""
+msgstr "Ta akcja wykracza poza dozwolone zakresy"
 
 #: lib/pleroma/web/oauth/fallback_controller.ex:14
 #, elixir-format
@@ -477,12 +478,12 @@ msgstr "Błąd CAPTCHA"
 #: lib/pleroma/web/common_api/common_api.ex:200
 #, elixir-format
 msgid "Could not add reaction emoji"
-msgstr ""
+msgstr "Nie można dodać reakcji emoji"
 
 #: lib/pleroma/web/common_api/common_api.ex:211
 #, elixir-format
 msgid "Could not remove reaction emoji"
-msgstr ""
+msgstr "Nie można usunąć reakcji emoji"
 
 #: lib/pleroma/web/twitter_api/twitter_api.ex:129
 #, elixir-format
@@ -535,6 +536,8 @@ msgstr "Wymagany reset hasła"
 #, elixir-format
 msgid "Security violation: OAuth scopes check was neither handled nor explicitly skipped."
 msgstr ""
+"Naruszenie bezpieczeństwa: sprawdzanie zakresów OAuth nie zostało ani "
+"wykonane, ani celowo pominięte."
 
 #: lib/pleroma/plugs/ensure_authenticated_plug.ex:28
 #, elixir-format
@@ -569,7 +572,7 @@ msgstr "Nieoczekiwany błąd podczas zmieniania metadanych paczki."
 #: lib/pleroma/plugs/user_is_admin_plug.ex:21
 #, elixir-format
 msgid "User is not an admin."
-msgstr ""
+msgstr "Użytkownik nie jest administratorem."
 
 #: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61
 #, elixir-format
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)
diff --git a/test/pleroma/web/activity_pub/builder_test.exs b/test/pleroma/web/activity_pub/builder_test.exs
new file mode 100644 (file)
index 0000000..3fe32bc
--- /dev/null
@@ -0,0 +1,48 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.BuilderTest do
+  alias Pleroma.Web.ActivityPub.Builder
+  alias Pleroma.Web.CommonAPI.ActivityDraft
+  use Pleroma.DataCase
+
+  import Pleroma.Factory
+
+  describe "note/1" do
+    test "returns note data" do
+      user = insert(:user)
+      note = insert(:note)
+      user2 = insert(:user)
+      user3 = insert(:user)
+
+      draft = %ActivityDraft{
+        user: user,
+        to: [user2.ap_id],
+        context: "2hu",
+        content_html: "<h1>This is :moominmamma: note</h1>",
+        in_reply_to: note.id,
+        tags: [name: "jimm"],
+        summary: "test summary",
+        cc: [user3.ap_id],
+        extra: %{"custom_tag" => "test"}
+      }
+
+      expected = %{
+        "actor" => user.ap_id,
+        "attachment" => [],
+        "cc" => [user3.ap_id],
+        "content" => "<h1>This is :moominmamma: note</h1>",
+        "context" => "2hu",
+        "sensitive" => false,
+        "summary" => "test summary",
+        "tag" => ["jimm"],
+        "to" => [user2.ap_id],
+        "type" => "Note",
+        "custom_tag" => "test"
+      }
+
+      assert {:ok, ^expected, []} = Builder.note(draft)
+    end
+  end
+end
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 320854187d753d57034d3e229d31f412f8be70b0..def2a10b4a2b85c25c618006fd379345e495d1a7 100644 (file)
@@ -68,7 +68,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatValidationTest do
     test "validates for a basic object we build", %{valid_chat_message: valid_chat_message} do
       assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
 
-      assert Map.put(valid_chat_message, "attachment", nil) == object
+      assert valid_chat_message == object
       assert match?(%{"firefox" => _}, object["emoji"])
     end
 
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 a929f828db62f81b1f965ea8c2c3f7986e2ff2a7..a0942ce8b8c6a7f49fd1385cd80448ebe803a122 100644 (file)
@@ -73,16 +73,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.AudioHandlingTest do
              %{
                "mediaType" => "audio/ogg",
                "type" => "Link",
-               "name" => nil,
-               "blurhash" => nil,
                "url" => [
                  %{
                    "href" =>
                      "https://channels.tests.funkwhale.audio/api/v1/listen/3901e5d8-0445-49d5-9711-e096cf32e515/?upload=42342395-0208-4fee-a38d-259a6dae0871&download=false",
                    "mediaType" => "audio/ogg",
-                   "type" => "Link",
-                   "width" => nil,
-                   "height" => nil
+                   "type" => "Link"
                  }
                ]
              }
index 62b4a2cb37ba2d6f9a1274472897e551e41100da..fc3ec7450f880c750259cc7972a15ee07441e250 100644 (file)
@@ -53,16 +53,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.VideoHandlingTest do
              %{
                "type" => "Link",
                "mediaType" => "video/mp4",
-               "name" => nil,
-               "blurhash" => nil,
                "url" => [
                  %{
                    "href" =>
                      "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4",
                    "mediaType" => "video/mp4",
-                   "type" => "Link",
-                   "width" => nil,
-                   "height" => nil
+                   "type" => "Link"
                  }
                ]
              }
@@ -78,16 +74,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.VideoHandlingTest do
              %{
                "type" => "Link",
                "mediaType" => "video/mp4",
-               "name" => nil,
-               "blurhash" => nil,
                "url" => [
                  %{
                    "href" =>
                      "https://framatube.org/static/webseed/6050732a-8a7a-43d4-a6cd-809525a1d206-1080.mp4",
                    "mediaType" => "video/mp4",
-                   "type" => "Link",
-                   "width" => nil,
-                   "height" => nil
+                   "type" => "Link"
                  }
                ]
              }
@@ -110,16 +102,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.VideoHandlingTest do
              %{
                "type" => "Link",
                "mediaType" => "video/mp4",
-               "name" => nil,
-               "blurhash" => nil,
                "url" => [
                  %{
                    "href" =>
                      "https://peertube.stream/static/streaming-playlists/hls/abece3c3-b9c6-47f4-8040-f3eed8c602e6/abece3c3-b9c6-47f4-8040-f3eed8c602e6-1080-fragmented.mp4",
                    "mediaType" => "video/mp4",
-                   "type" => "Link",
-                   "width" => nil,
-                   "height" => nil
+                   "type" => "Link"
                  }
                ]
              }
index 6a2986b5ffd7c303af4f9d1d77615c0322923b8c..8102845d57a8ee61d038016c2f251bdafb3182dc 100644 (file)
@@ -305,7 +305,7 @@ defmodule Pleroma.Web.AdminAPI.ReportControllerTest do
         |> get("/api/pleroma/admin/reports")
 
       assert json_response(conn, :forbidden) ==
-               %{"error" => "User is not an admin."}
+               %{"error" => "User is not a staff member."}
     end
 
     test "returns 403 when requested by anonymous" do
index b0e567ff0b250acf99905623d62847414cc50758..d8fec35208bd47d41703a66a3e39f35936ccc0ff 100644 (file)
@@ -681,41 +681,6 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
     end
   end
 
-  describe "make_note_data/1" do
-    test "returns note data" do
-      user = insert(:user)
-      note = insert(:note)
-      user2 = insert(:user)
-      user3 = insert(:user)
-
-      draft = %ActivityDraft{
-        user: user,
-        to: [user2.ap_id],
-        context: "2hu",
-        content_html: "<h1>This is :moominmamma: note</h1>",
-        in_reply_to: note.id,
-        tags: [name: "jimm"],
-        summary: "test summary",
-        cc: [user3.ap_id],
-        extra: %{"custom_tag" => "test"}
-      }
-
-      assert Utils.make_note_data(draft) == %{
-               "actor" => user.ap_id,
-               "attachment" => [],
-               "cc" => [user3.ap_id],
-               "content" => "<h1>This is :moominmamma: note</h1>",
-               "context" => "2hu",
-               "sensitive" => false,
-               "summary" => "test summary",
-               "tag" => ["jimm"],
-               "to" => [user2.ap_id],
-               "type" => "Note",
-               "custom_tag" => "test"
-             }
-    end
-  end
-
   describe "maybe_add_attachments/3" do
     test "returns parsed results when attachment_links is false" do
       assert Utils.maybe_add_attachments(
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
diff --git a/test/pleroma/web/plugs/user_is_staff_plug_test.exs b/test/pleroma/web/plugs/user_is_staff_plug_test.exs
new file mode 100644 (file)
index 0000000..a0c4061
--- /dev/null
@@ -0,0 +1,47 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.UserIsStaffPlugTest do
+  use Pleroma.Web.ConnCase, async: true
+
+  alias Pleroma.Web.Plugs.UserIsStaffPlug
+  import Pleroma.Factory
+
+  test "accepts a user that is an admin" do
+    user = insert(:user, is_admin: true)
+
+    conn = assign(build_conn(), :user, user)
+
+    ret_conn = UserIsStaffPlug.call(conn, %{})
+
+    assert conn == ret_conn
+  end
+
+  test "accepts a user that is a moderator" do
+    user = insert(:user, is_moderator: true)
+
+    conn = assign(build_conn(), :user, user)
+
+    ret_conn = UserIsStaffPlug.call(conn, %{})
+
+    assert conn == ret_conn
+  end
+
+  test "denies a user that isn't a staff member" do
+    user = insert(:user)
+
+    conn =
+      build_conn()
+      |> assign(:user, user)
+      |> UserIsStaffPlug.call(%{})
+
+    assert conn.status == 403
+  end
+
+  test "denies when a user isn't set" do
+    conn = UserIsStaffPlug.call(build_conn(), %{})
+
+    assert conn.status == 403
+  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}"),