Merge remote-tracking branch 'origin/develop' into global-status-expiration
authorEgor Kislitsyn <egor@kislitsyn.com>
Mon, 2 Mar 2020 20:32:34 +0000 (00:32 +0400)
committerEgor Kislitsyn <egor@kislitsyn.com>
Mon, 2 Mar 2020 20:32:34 +0000 (00:32 +0400)
1  2 
CHANGELOG.md
config/config.exs
config/description.exs
docs/configuration/cheatsheet.md
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/common_api/common_api.ex
test/web/activity_pub/activity_pub_test.exs
test/workers/cron/purge_expired_activities_worker_test.exs

diff --combined CHANGELOG.md
index c5558e0c7ec09b617d4420e9c122d488edc17e23,c8f3794a37713f0db64153c334ce9e21e5e6e7e7..ec6b0cb3828d59ace770513724b4db7ea6dc4154
@@@ -4,6 -4,9 +4,9 @@@ All notable changes to this project wil
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
  
  ## [Unreleased]
+ ### Security
+ - Mastodon API: Fix being able to request enourmous amount of statuses in timelines leading to DoS. Now limited to 40 per request.
  ### Removed
  - **Breaking**: Removed 1.0+ deprecated configurations `Pleroma.Upload, :strip_exif` and `:instance, :dedupe_media`
  - **Breaking**: OStatus protocol support
@@@ -21,6 -24,7 +24,7 @@@
  - **Breaking:** attachment links (`config :pleroma, :instance, no_attachment_links` and `config :pleroma, Pleroma.Upload, link_name`) disabled by default
  - **Breaking:** OAuth: defaulted `[:auth, :enforce_oauth_admin_scope_usage]` setting to `true` which demands `admin` OAuth scope to perform admin actions (in addition to `is_admin` flag on User); make sure to use bundled or newer versions of AdminFE & PleromaFE to access admin / moderator features.
  - **Breaking:** Dynamic configuration has been rearchitected. The `:pleroma, :instance, dynamic_configuration` setting has been replaced with `config :pleroma, configurable_from_database`. Please backup your configuration to a file and run the migration task to ensure consistency with the new schema.
+ - **Breaking:** `:instance, no_attachment_links` has been replaced with `:instance, attachment_links` which still takes a boolean value but doesn't use double negative language.
  - Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings)
  - Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler
  - Enabled `:instance, extended_nickname_format` in the default config
@@@ -34,8 -38,7 +38,8 @@@
  - Rate limiter is now disabled for localhost/socket (unless remoteip plug is enabled)
  - Logger: default log level changed from `warn` to `info`.
  - Config mix task `migrate_to_db` truncates `config` table before migrating the config file.
 +- MFR policy to set global expiration for all local Create activities
+ - Default to `prepare: :unnamed` in the database configuration.
  <details>
    <summary>API Changes</summary>
  
@@@ -57,6 -60,7 +61,7 @@@
  - Admin API: Render whole status in grouped reports
  - Mastodon API: User timelines will now respect blocks, unless you are getting the user timeline of somebody you blocked (which would be empty otherwise).
  - Mastodon API: Favoriting / Repeating a post multiple times will now return the identical response every time. Before, executing that action twice would return an error ("already favorited") on the second try.
+ - Mastodon API: Limit timeline requests to 3 per timeline per 500ms per user/ip by default.
  </details>
  
  ### Added
  - User notification settings: Add `privacy_option` option.
  - Support for custom Elixir modules (such as MRF policies)
  - User settings: Add _This account is a_ option.
+ - A new users admin digest email
  - OAuth: admin scopes support (relevant setting: `[:auth, :enforce_oauth_admin_scope_usage]`).
+ - Add an option `authorized_fetch_mode` to require HTTP signatures for AP fetches.
+ - ActivityPub: support for `replies` collection (output for outgoing federation & fetching on incoming federation).
+ - Mix task to refresh counter cache (`mix pleroma.refresh_counter_cache`)
  <details>
    <summary>API Changes</summary>
  
  - Configuration: `feed` option for user atom feed.
  - Pleroma API: Add Emoji reactions
  - Admin API: Add `/api/pleroma/admin/instances/:instance/statuses` - lists all statuses from a given instance
+ - Admin API: Add `/api/pleroma/admin/users/:nickname/statuses` - lists all statuses from a given user
  - Admin API: `PATCH /api/pleroma/users/confirm_email` to confirm email for multiple users, `PATCH /api/pleroma/users/resend_confirmation_email` to resend confirmation email for multiple users
  - ActivityPub: Configurable `type` field of the actors.
  - Mastodon API: `/api/v1/accounts/:id` has `source/pleroma/actor_type` field.
  - Configuration: `feed.logo` option for tag feed.
  - Tag feed: `/tags/:tag.rss` - list public statuses by hashtag.
  - Mastodon API: Add `reacted` property to `emoji_reactions`
+ - Pleroma API: Add reactions for a single emoji.
+ - ActivityPub: `[:activitypub, :note_replies_output_limit]` setting sets the number of note self-replies to output on outgoing federation.
+ - Admin API: `GET /api/pleroma/admin/stats` to get status count by visibility scope
+ - Admin API: `GET /api/pleroma/admin/statuses` - list all statuses (accepts `godmode` and `local_only`)
  </details>
  
  ### Fixed
diff --combined config/config.exs
index d5b298c167d631b371ba7485994735129afd72e9,2cd741213da5394e8ba320c33649801c11cfc73d..0c8e5e1c524a3b0a2e34b55b41d9191134a84288
@@@ -219,6 -219,8 +219,8 @@@ config :pleroma, :instance
      max_expiration: 365 * 24 * 60 * 60
    },
    registrations_open: true,
+   invites_enabled: false,
+   account_activation_required: false,
    federating: true,
    federation_incoming_replies_max_depth: 100,
    federation_reachability_timeout_days: 7,
    mrf_transparency_exclusions: [],
    autofollowed_nicknames: [],
    max_pinned_statuses: 1,
-   no_attachment_links: true,
+   attachment_links: false,
    welcome_user_nickname: nil,
    welcome_message: nil,
    max_report_comment_size: 1000,
@@@ -326,7 -328,9 +328,9 @@@ config :pleroma, :activitypub
    unfollow_blocked: true,
    outgoing_blocks: true,
    follow_handshake_timeout: 500,
-   sign_object_fetches: true
+   note_replies_output_limit: 5,
+   sign_object_fetches: true,
+   authorized_fetch_mode: false
  
  config :pleroma, :streamer,
    workers: 3,
@@@ -361,8 -365,6 +365,8 @@@ config :pleroma, :mrf_keyword
  
  config :pleroma, :mrf_subchain, match_actor: %{}
  
 +config :pleroma, :mrf_activity_expiration, days: 365
 +
  config :pleroma, :mrf_vocabulary,
    accept: [],
    reject: []
@@@ -400,6 -402,8 +404,8 @@@ config :phoenix, :format_encoders, json
  
  config :phoenix, :json_library, Jason
  
+ config :phoenix, :filter_parameters, ["password", "confirm"]
  config :pleroma, :gopher,
    enabled: false,
    ip: {0, 0, 0, 0},
@@@ -482,13 -486,16 +488,16 @@@ config :pleroma, Oban
      transmogrifier: 20,
      scheduled_activities: 10,
      background: 5,
-     attachments_cleanup: 5
+     remote_fetcher: 2,
+     attachments_cleanup: 5,
+     new_users_digest: 1
    ],
    crontab: [
      {"0 0 * * *", Pleroma.Workers.Cron.ClearOauthTokenWorker},
      {"0 * * * *", Pleroma.Workers.Cron.StatsWorker},
      {"* * * * *", Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker},
-     {"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker}
+     {"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker},
+     {"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker}
    ]
  
  config :pleroma, :workers,
@@@ -562,6 -569,8 +571,8 @@@ config :pleroma, Pleroma.Emails.UserEma
      text_muted_color: "#b9b9ba"
    }
  
+ config :pleroma, Pleroma.Emails.NewUsersDigestEmail, enabled: false
  config :prometheus, Pleroma.Web.Endpoint.MetricsExporter, path: "/api/pleroma/app_metrics"
  
  config :pleroma, Pleroma.ScheduledActivity,
@@@ -590,6 -599,7 +601,7 @@@ config :http_signatures
  
  config :pleroma, :rate_limit,
    authentication: {60_000, 15},
+   timeline: {500, 3},
    search: [{1000, 10}, {1000, 30}],
    app_account_creation: {1_800_000, 25},
    relations_actions: {10_000, 10},
@@@ -614,6 -624,10 +626,10 @@@ config :pleroma, :modules, runtime_dir
  
  config :pleroma, configurable_from_database: false
  
+ config :pleroma, Pleroma.Repo,
+   parameters: [gin_fuzzy_search_limit: "500"],
+   prepare: :unnamed
  # Import environment specific config. This must remain at the bottom
  # of this file so it overrides the configuration defined above.
  import_config "#{Mix.env()}.exs"
diff --combined config/description.exs
index f0c6e337708d3f4586b9309221ef63467484aea6,9fdcfcd967883f02dd3772b995247e29e5b5b6be..f113931bd3722eee7aae98afd16efac7fd486c78
@@@ -101,7 -101,7 +101,7 @@@ config :pleroma, :config_description, 
                        %{
                          key: :versions,
                          type: {:list, :atom},
-                         description: "List of TLS version to use",
+                         description: "List of TLS versions to use",
                          suggestions: [:tlsv1, ":tlsv1.1", ":tlsv1.2"]
                        }
                      ]
        %{
          key: :description,
          type: :string,
-         description: "The instance's description, can be seen in nodeinfo and /api/v1/instance",
+         description:
+           "The instance's description. It can be seen in nodeinfo and `/api/v1/instance`",
          suggestions: [
            "Very cool instance"
          ]
        %{
          key: :registrations_open,
          type: :boolean,
-         description: "Enable registrations for anyone, invitations can be enabled when `false`"
+         description:
+           "Enable registrations for anyone. Invitations require this setting to be disabled."
        },
        %{
          key: :invites_enabled,
          type: :boolean,
-         description: "Enable user invitations for admins (depends on `registrations_open: false`)"
+         description:
+           "Enable user invitations for admins (depends on `registrations_open` being disabled)."
        },
        %{
          key: :account_activation_required,
          type: :boolean,
-         description: "Require users to confirm their emails before signing in"
+         description: "Require users to confirm their emails before signing in."
        },
        %{
          key: :federating,
          type: :boolean,
-         description: "Enable federation with other instances"
+         description: "Enable federation with other instances."
        },
        %{
          key: :federation_incoming_replies_max_depth,
          label: "Fed. incoming replies max depth",
          type: :integer,
          description:
-           "Max. depth of reply-to activities fetching on incoming federation, to prevent out-of-memory situations while" <>
+           "Max. depth of reply-to and reply activities fetching on incoming federation, to prevent out-of-memory situations while" <>
              " fetching very long threads. If set to `nil`, threads of any depth will be fetched. Lower this value if you experience out-of-memory crashes.",
          suggestions: [
            100
          key: :extended_nickname_format,
          type: :boolean,
          description:
-           "Set to `true` to use extended local nicknames format (allows underscores/dashes)." <>
+           "Enable to use extended local nicknames format (allows underscores/dashes)." <>
              " This will break federation with older software for theses nicknames."
        },
        %{
          key: :cleanup_attachments,
          type: :boolean,
          description: """
-         "Set to `true` to remove associated attachments when status is removed.
+         Enable to remove associated attachments when status is removed.
          This will not affect duplicates and attachments without status.
          Enabling this will increase load to database when deleting statuses on larger instances.
          """
          ]
        },
        %{
-         key: :no_attachment_links,
+         key: :attachment_links,
          type: :boolean,
-         description:
-           "Set to `true` to disable automatically adding attachment link text to statuses"
+         description: "Enable to automatically add attachment link text to statuses"
        },
        %{
          key: :welcome_message,
          key: :safe_dm_mentions,
          type: :boolean,
          description:
-           "If set to `true`, only mentions at the beginning of a post will be used to address people in direct messages." <>
+           "If enabled, only mentions at the beginning of a post will be used to address people in direct messages." <>
              " This is to prevent accidental mentioning of people when talking about them (e.g. \"@admin please keep an eye on @bad_actor\")." <>
-             " Default: `false`"
+             " Default: disabled"
        },
        %{
          key: :healthcheck,
          type: :boolean,
-         description: "If set to `true`, system data will be shown on /api/pleroma/healthcheck"
+         description: "If enabled, system data will be shown on `/api/pleroma/healthcheck`"
        },
        %{
          key: :remote_post_retention_days,
        %{
          key: :skip_thread_containment,
          type: :boolean,
-         description: "Skip filter out broken threads. Default: `true`"
+         description: "Skip filtering out broken threads. Default: enabled"
        },
        %{
          key: :limit_to_local_content,
              key: :alwaysShowSubjectInput,
              label: "Always show subject input",
              type: :boolean,
-             description: "When set to `false`, auto-hide the subject field when it's empty"
+             description: "When disabled, auto-hide the subject field if it's empty"
            },
            %{
              key: :logoMask,
              label: "Logo mask",
              type: :boolean,
              description:
-               "By default it assumes logo used will be monochrome-with-alpha one, this is done to be compatible with both light and dark themes, " <>
-                 "so that white logo designed with dark theme in mind won't be invisible over light theme, this is done via CSS3 Masking. " <>
-                 "Basically - it will take alpha channel of the image and fill non-transparent areas of it with solid color. " <>
-                 "If you really want colorful logo - it can be done by setting logoMask to false."
+               "By default it assumes logo used will be monochrome with alpha channel to be compatible with both light and dark themes. " <>
+                 "If you want a colorful logo you must disable logoMask."
            },
            %{
              key: :logoMargin,
            %{
              key: :stickers,
              type: :boolean,
-             description: "Enables/disables stickers."
+             description: "Enables stickers."
            },
            %{
              key: :enableEmojiPicker,
              label: "Emoji picker",
              type: :boolean,
-             description: "Enables/disables emoji picker."
+             description: "Enables emoji picker."
            }
          ]
        },
        %{
          key: :media_removal,
          type: {:list, :string},
-         description: "List of instances to remove medias from",
+         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 put medias as NSFW (sensitive) from",
+         description: "List of instances to tag all media as NSFW (sensitive) from",
          suggestions: ["example.com", "*.example.com"]
        },
        %{
        }
      ]
    },
 +  %{
 +    group: :pleroma,
 +    key: :mrf_activity_expiration,
 +    label: "MRF Activity Expiration Policy",
 +    type: :group,
 +    description: "Adds expiration to all local Create activities",
 +    children: [
 +      %{
 +        key: :days,
 +        type: :integer,
 +        description: "Default global expiration time for all local Create activities (in days)",
 +        suggestions: [90, 365]
 +      }
 +    ]
 +  },
    %{
      group: :pleroma,
      key: :mrf_subchain,
          key: :reject,
          type: [:string, :regex],
          description:
-           "A list of patterns which result in message being rejected, each pattern can be a string or a regular expression.",
+           "A list of patterns which result in message being rejected. Each pattern can be a string or a regular expression.",
          suggestions: ["foo", ~r/foo/iu]
        },
        %{
          key: :federated_timeline_removal,
          type: [:string, :regex],
          description:
-           "A list of patterns which result in message being removed from federated timelines (a.k.a unlisted), each pattern can be a string or a regular expression.",
+           "A list of patterns which result in message being removed from federated timelines (a.k.a unlisted). Each pattern can be a string or a regular expression.",
          suggestions: ["foo", ~r/foo/iu]
        },
        %{
          key: :replace,
          type: [{:tuple, :string, :string}, {:tuple, :regex, :string}],
          description:
-           "A list of tuples containing {pattern, replacement}, pattern can be a string or a regular expression.",
+           "A list of tuples containing {pattern, replacement}. Each pattern can be a string or a regular expression.",
          suggestions: [{"foo", "bar"}, {~r/foo/iu, "bar"}]
        }
      ]
        %{
          key: :actors,
          type: {:list, :string},
-         description: "A list of actors, for which to drop any posts mentioning",
+         description: "A list of actors for which any post mentioning them will be dropped.",
          suggestions: ["actor1", "actor2"]
        }
      ]
        }
      ]
    },
-   %{
-     group: :pleroma,
-     key: Pleroma.Web.Endpoint,
-     type: :group,
-     description: "Phoenix endpoint configuration",
-     children: [
-       %{
-         key: :http,
-         label: "HTTP",
-         type: {:keyword, :integer, :tuple},
-         description: "http protocol configuration",
-         suggestions: [
-           port: 8080,
-           ip: {127, 0, 0, 1}
-         ],
-         children: [
-           %{
-             key: :dispatch,
-             type: {:list, :tuple},
-             description: "dispatch settings",
-             suggestions: [
-               {:_,
-                [
-                  {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
-                  {"/websocket", Phoenix.Endpoint.CowboyWebSocket,
-                   {Phoenix.Transports.WebSocket,
-                    {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, websocket_config}}},
-                  {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
-                ]}
-               # end copied from config.exs
-             ]
-           },
-           %{
-             key: :ip,
-             label: "IP",
-             type: :tuple,
-             description: "ip",
-             suggestions: [
-               {0, 0, 0, 0}
-             ]
-           },
-           %{
-             key: :port,
-             type: :integer,
-             description: "port",
-             suggestions: [
-               2020
-             ]
-           }
-         ]
-       },
-       %{
-         key: :url,
-         label: "URL",
-         type: {:keyword, :string, :integer},
-         description: "configuration for generating urls",
-         suggestions: [
-           host: "example.com",
-           port: 2020,
-           scheme: "https"
-         ],
-         children: [
-           %{
-             key: :host,
-             type: :string,
-             description: "Host",
-             suggestions: [
-               "example.com"
-             ]
-           },
-           %{
-             key: :port,
-             type: :integer,
-             description: "port",
-             suggestions: [
-               2020
-             ]
-           },
-           %{
-             key: :scheme,
-             type: :string,
-             description: "Scheme",
-             suggestions: [
-               "https",
-               "https"
-             ]
-           }
-         ]
-       },
-       %{
-         key: :instrumenters,
-         type: {:list, :module},
-         suggestions: [Pleroma.Web.Endpoint.Instrumenter]
-       },
-       %{
-         key: :protocol,
-         type: :string,
-         suggestions: ["https"]
-       },
-       %{
-         key: :secret_key_base,
-         type: :string,
-         suggestions: ["aK4Abxf29xU9TTDKre9coZPUgevcVCFQJe/5xP/7Lt4BEif6idBIbjupVbOrbKxl"]
-       },
-       %{
-         key: :signing_salt,
-         type: :string,
-         suggestions: ["CqaoopA2"]
-       },
-       %{
-         key: :render_errors,
-         type: :keyword,
-         suggestions: [view: Pleroma.Web.ErrorView, accepts: ~w(json)],
-         children: [
-           %{
-             key: :view,
-             type: :module,
-             suggestions: [Pleroma.Web.ErrorView]
-           },
-           %{
-             key: :accepts,
-             type: {:list, :string},
-             suggestions: ["json"]
-           }
-         ]
-       },
-       %{
-         key: :pubsub,
-         type: :keyword,
-         suggestions: [name: Pleroma.PubSub, adapter: Phoenix.PubSub.PG2],
-         children: [
-           %{
-             key: :name,
-             type: :module,
-             suggestions: [Pleroma.PubSub]
-           },
-           %{
-             key: :adapter,
-             type: :module,
-             suggestions: [Phoenix.PubSub.PG2]
-           }
-         ]
-       },
-       %{
-         key: :secure_cookie_flag,
-         type: :boolean
-       },
-       %{
-         key: :extra_cookie_attrs,
-         type: {:list, :string},
-         suggestions: ["SameSite=Lax"]
-       }
-     ]
-   },
    %{
      group: :pleroma,
      key: :activitypub,
          type: :boolean,
          description: "Sign object fetches with HTTP signatures"
        },
+       %{
+         key: :note_replies_output_limit,
+         type: :integer,
+         description:
+           "The number of Note replies' URIs to be included with outgoing federation (`5` to match Mastodon hardcoded value, `0` to disable the output)."
+       },
        %{
          key: :follow_handshake_timeout,
          type: :integer,
          type: :string,
          description:
            "A mailto link for the administrative contact." <>
-             " It's best if this email is not a personal email address, but rather a group email so that if a person leaves an organization," <>
-             " is unavailable for an extended period, or otherwise can't respond, someone else on the list can.",
-         suggestions: ["Subject"]
+             " It's best if this email is not a personal email address, but rather a group email to the instance moderation team.",
+         suggestions: ["mailto:moderators@pleroma.com"]
        },
        %{
          key: :public_key,
          key: :admin_token,
          type: :string,
          description: "Token",
-         suggestions: ["some_random_token"]
+         suggestions: ["We recommend a secure random string or UUID"]
        }
      ]
    },
            "Background jobs queues (keys: queues, values: max numbers of concurrent jobs)",
          suggestions: [
            activity_expiration: 10,
+           attachments_cleanup: 5,
            background: 5,
            federator_incoming: 50,
            federator_outgoing: 50,
              description: "Activity expiration queue",
              suggestions: [10]
            },
+           %{
+             key: :attachments_cleanup,
+             type: :integer,
+             description: "Attachment deletion queue",
+             suggestions: [5]
+           },
            %{
              key: :background,
              type: :integer,
              suggestions: [50]
            }
          ]
+       },
+       %{
+         key: :crontab,
+         type: {:list, :tuple},
+         description: "Settings for cron background jobs",
+         suggestions: [
+           {"0 0 * * *", Pleroma.Workers.Cron.ClearOauthTokenWorker},
+           {"0 * * * *", Pleroma.Workers.Cron.StatsWorker},
+           {"* * * * *", Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker},
+           {"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker},
+           {"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker}
+         ]
        }
      ]
    },
          key: :unfurl_nsfw,
          label: "Unfurl NSFW",
          type: :boolean,
-         description: "If set to `true` NSFW attachments will be shown in previews"
+         description: "When enabled NSFW attachments will be shown in previews"
        }
      ]
    },
        %{
          key: :enabled,
          type: :boolean,
-         description: "Enables/disables RichMedia."
+         description: "Enables RichMedia parsing of URLs."
        },
        %{
          key: :ignore_hosts,
        %{
          key: :enabled,
          type: :boolean,
-         description:
-           "If enabled, when a new user is federated with, fetch some of their latest posts"
+         description: "Fetch posts when a new user is federated with"
        },
        %{
          key: :pages,
        %{
          key: :class,
          type: [:string, false],
-         description: "Specify the class to be added to the generated link. `False` to clear",
+         description: "Specify the class to be added to the generated link. Disable to clear",
          suggestions: ["auto-linker", false]
        },
        %{
          key: :rel,
          type: [:string, false],
-         description: "Override the rel attribute. `False` to clear",
+         description: "Override the rel attribute. Disable to clear",
          suggestions: ["ugc", "noopener noreferrer", false]
        },
        %{
          key: :ssl,
          label: "SSL",
          type: :boolean,
-         description: "`True` to use SSL, usually implies the port 636"
+         description: "Enable to use SSL, usually implies the port 636"
        },
        %{
          key: :sslopts,
          key: :tls,
          label: "TLS",
          type: :boolean,
-         description: "`True` to start TLS, usually implies the port 389"
+         description: "Enable to use STARTTLS, usually implies the port 389"
        },
        %{
          key: :tlsopts,
          type: :boolean,
          description:
            "OAuth admin scope requirement toggle. " <>
-             "If `true`, admin actions explicitly demand admin OAuth scope(s) presence in OAuth token " <>
-             "(client app must support admin scopes). If `false` and token doesn't have admin scope(s)," <>
+             "If enabled, admin actions explicitly demand admin OAuth scope(s) presence in OAuth token " <>
+             "(client app must support admin scopes). If disabled and token doesn't have admin scope(s)," <>
              "`is_admin` user flag grants access to admin-specific actions."
        },
        %{
          key: :oauth_consumer_strategies,
          type: {:list, :string},
          description:
-           "The list of enabled OAuth consumer strategies; by default it's set by OAUTH_CONSUMER_STRATEGIES environment variable." <>
+           "The list of enabled OAuth consumer strategies. By default it's set by OAUTH_CONSUMER_STRATEGIES environment variable." <>
              " Each entry in this space-delimited string should be of format \"strategy\" or \"strategy:dependency\"" <>
              " (e.g. twitter or keycloak:ueberauth_keycloak_strategy in case dependency is named differently than ueberauth_<strategy>).",
          suggestions: ["twitter", "keycloak:ueberauth_keycloak_strategy"]
        }
      ]
    },
+   %{
+     group: :pleroma,
+     key: Pleroma.Emails.NewUsersDigestEmail,
+     type: :group,
+     description: "New users admin email digest",
+     children: [
+       %{
+         key: :enabled,
+         type: :boolean,
+         description: "enables new users admin digest email when `true`",
+         suggestions: [false]
+       }
+     ]
+   },
    %{
      group: :pleroma,
      key: :oauth2,
        %{
          key: :clean_expired_tokens,
          type: :boolean,
-         description: "Enable a background job to clean expired oauth tokens. Default: `false`."
+         description: "Enable a background job to clean expired oauth tokens. Default: disabled."
        }
      ]
    },
        }
      ]
    },
-   %{
-     group: :pleroma,
-     key: :database,
-     type: :group,
-     description: "Database related settings",
-     children: [
-       %{
-         key: :rum_enabled,
-         type: :boolean,
-         description: "If RUM indexes should be used. Default: `false`"
-       }
-     ]
-   },
    %{
      group: :pleroma,
      key: :rate_limit,
          description: "For the search requests (account & status search etc.)",
          suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]]
        },
+       %{
+         key: :timeline,
+         type: [:tuple, {:list, :tuple}],
+         description: "For requests to timelines (each timeline has it's own limiter)",
+         suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]]
+       },
        %{
          key: :app_account_creation,
          type: [:tuple, {:list, :tuple}],
        }
      ]
    },
-   %{
-     group: :prometheus,
-     key: Pleroma.Web.Endpoint.MetricsExporter,
-     type: :group,
-     description: "Prometheus settings",
-     children: [
-       %{
-         key: :path,
-         type: :string,
-         description: "API endpoint with metrics",
-         suggestions: ["/api/pleroma/app_metrics"]
-       }
-     ]
-   },
    %{
      group: :http_signatures,
      type: :group,
        %{
          key: :enabled,
          type: :boolean,
-         description: "Enable/disable the plug. Default: `false`."
+         description: "Enable/disable the plug. Default: disabled."
        },
        %{
          key: :headers,
        %{
          key: :enabled,
          type: :boolean,
-         description: "Enables the rendering of static HTML. Defaults to `false`."
+         description: "Enables the rendering of static HTML. Default: disabled."
        }
      ]
    },
      group: :pleroma,
      key: :feed,
      type: :group,
-     description: "Configure feed rendering.",
+     description: "Configure feed rendering",
      children: [
        %{
          key: :post_title,
      group: :pleroma,
      key: :modules,
      type: :group,
-     description: "Custom Runtime Modules.",
+     description: "Custom Runtime Modules",
      children: [
        %{
          key: :runtime_dir,
    },
    %{
      group: :pleroma,
+     key: :streamer,
      type: :group,
-     description: "Allow instance configuration from database.",
+     description: "Settings for notifications streamer",
      children: [
        %{
-         key: :configurable_from_database,
-         type: :boolean,
-         description:
-           "Allow transferring configuration to DB with the subsequent customization from Admin api. Defaults to `false`"
+         key: :workers,
+         type: :integer,
+         description: "Number of workers to send notifications.",
+         suggestions: [3]
+       },
+       %{
+         key: :overflow_workers,
+         type: :integer,
+         description: "Maximum number of workers created if pool is empty.",
+         suggestions: [2]
        }
      ]
    }
index f50c8bab71b5522843794c36239133e5679fdef3,05fd6ceb1f956f3edcb689a46792027dc977928b..3fd372b950b5cae60cfc46c0851d80409b517ee1
@@@ -2,9 -2,11 +2,11 @@@
  
  This is a cheat sheet for Pleroma configuration file, any setting possible to configure should be listed here.
  
Pleroma configuration works by first importing the base config (`config/config.exs` on source installs, compiled-in on OTP releases), then overriding it by the environment config (`config/$MIX_ENV.exs` on source installs, N/A to OTP releases) and then overriding it by user config (`config/$MIX_ENV.secret.exs` on source installs, typically `/etc/pleroma/config.exs` on OTP releases).
For OTP installations the configuration is typically stored in `/etc/pleroma/config.exs`.
  
- You shouldn't edit the base config directly to avoid breakages and merge conflicts, but it can be used as a reference if you don't understand how an option is supposed to be formatted, the latest version of it can be viewed [here](https://git.pleroma.social/pleroma/pleroma/blob/develop/config/config.exs).
+ For from source installations Pleroma configuration works by first importing the base config `config/config.exs`, then overriding it by the environment config `config/$MIX_ENV.exs` and then overriding it by user config `config/$MIX_ENV.secret.exs`. In from source installations you should always make the changes to the user config and NEVER to the base config to avoid breakages and merge conflicts. So for production you change/add configuration to `config/prod.secret.exs`.
+ To add configuration to your config file, you can copy it from the base config. The latest version of it can be viewed [here](https://git.pleroma.social/pleroma/pleroma/blob/develop/config/config.exs). You can also use this file if you don't know how an option is supposed to be formatted.
  
  ## :instance
  * `name`: The instance’s name.
@@@ -33,7 -35,7 +35,7 @@@
  * `rewrite_policy`: Message Rewrite Policy, either one or a list. Here are the ones available by default:
      * `Pleroma.Web.ActivityPub.MRF.NoOpPolicy`: Doesn’t modify activities (default).
      * `Pleroma.Web.ActivityPub.MRF.DropPolicy`: Drops all activities. It generally doesn’t makes sense to use in production.
 -    * `Pleroma.Web.ActivityPub.MRF.SimplePolicy`: Restrict the visibility of activities from certains instances (See [`:mrf_simple`](#mrf_simple)).
 +    * `Pleroma.Web.ActivityPub.MRF.SimplePolicy`: Restrict the visibility of activities from certain instances (See [`:mrf_simple`](#mrf_simple)).
      * `Pleroma.Web.ActivityPub.MRF.TagPolicy`: Applies policies to individual users based on tags, which can be set using pleroma-fe/admin-fe/any other app that supports Pleroma Admin API. For example it allows marking posts from individual users nsfw (sensitive).
      * `Pleroma.Web.ActivityPub.MRF.SubchainPolicy`: Selectively runs other MRF policies when messages match (See [`:mrf_subchain`](#mrf_subchain)).
      * `Pleroma.Web.ActivityPub.MRF.RejectNonPublic`: Drops posts with non-public visibility settings (See [`:mrf_rejectnonpublic`](#mrf_rejectnonpublic)).
@@@ -43,8 -45,7 +45,8 @@@
      * `Pleroma.Web.ActivityPub.MRF.MentionPolicy`: Drops posts mentioning configurable users. (See [`:mrf_mention`](#mrf_mention)).
      * `Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`: Restricts activities to a configured set of vocabulary. (See [`:mrf_vocabulary`](#mrf_vocabulary)).
      * `Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy`: Rejects or delists posts based on their age when received. (See [`:mrf_object_age`](#mrf_object_age)).
 -* `public`: Makes the client API in authentificated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network.
 +    * `Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy`: Adds expiration to all local Create activities (see [`:mrf_activity_expiration`](#mrf_activity_expiration)).
 +* `public`: Makes the client API in authenticated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network.
  * `quarantined_instances`: List of ActivityPub instances where private(DMs, followers-only) activities will not be send.
  * `managed_config`: Whenether the config for pleroma-fe is configured in [:frontend_configurations](#frontend_configurations) or in ``static/config.json``.
  * `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML).
@@@ -143,19 -144,20 +145,24 @@@ config :pleroma, :mrf_user_allowlist
    * `:strip_followers` removes followers from the ActivityPub recipient list, ensuring they won't be delivered to home timelines
    * `:reject` rejects the message entirely
  
 +#### :mrf_activity_expiration
 +
 +* `days`: Default global expiration time for all local Create activities (in days)
 +
  ### :activitypub
- * ``unfollow_blocked``: Whether blocks result in people getting unfollowed
- * ``outgoing_blocks``: Whether to federate blocks to other instances
- * ``deny_follow_blocked``: Whether to disallow following an account that has blocked the user in question
- * ``sign_object_fetches``: Sign object fetches with HTTP signatures
+ * `unfollow_blocked`: Whether blocks result in people getting unfollowed
+ * `outgoing_blocks`: Whether to federate blocks to other instances
+ * `deny_follow_blocked`: Whether to disallow following an account that has blocked the user in question
+ * `sign_object_fetches`: Sign object fetches with HTTP signatures
+ * `authorized_fetch_mode`: Require HTTP signatures for AP fetches
  
  ### :fetch_initial_posts
- * `enabled`: if enabled, when a new user is federated with, fetch some of their latest posts
- * `pages`: the amount of pages to fetch
+ !!! warning
+     Be careful with this setting, fetching posts may lead to new users being discovered whose posts will then also be fetched. This can lead to serious load on your instance and database.
+ * `enabled`: If enabled, when a new user is discovered by your instance, fetch some of their latest posts.
+ * `pages`: The amount of pages to fetch
  
  ## Pleroma.ScheduledActivity
  
@@@ -347,6 -349,7 +354,7 @@@ Means that
  Supported rate limiters:
  
  * `:search` - Account/Status search.
+ * `:timeline` - Timeline requests (each timeline has it's own limiter).
  * `:app_account_creation` - Account registration from the API.
  * `:relations_actions` - Following/Unfollowing in general.
  * `:relation_id_action` - Following/Unfollowing for a specific user.
@@@ -506,6 -509,10 +514,10 @@@ Email notifications settings
  - `:logo` - a path to a custom logo. Set it to `nil` to use the default Pleroma logo.
  - `:styling` - a map with color settings for email templates.
  
+ ### Pleroma.Emails.NewUsersDigestEmail
+ - `:enabled` - a boolean, enables new users admin digest email when `true`. Defaults to `false`.
  ## Background jobs
  
  ### Oban
index 408f6c9667abb25761b314402405955158425043,04b853dcf6cb9543ea2e326ddcc9a4d5c9428923..07121a798650b27f97e1af77543de71942c6a03f
@@@ -5,8 -5,8 +5,9 @@@
  defmodule Pleroma.Web.ActivityPub.ActivityPub do
    alias Pleroma.Activity
    alias Pleroma.Activity.Ir.Topics
 +  alias Pleroma.ActivityExpiration
    alias Pleroma.Config
+   alias Pleroma.Constants
    alias Pleroma.Conversation
    alias Pleroma.Conversation.Participation
    alias Pleroma.Notification
  
    def increase_poll_votes_if_vote(_create_data), do: :noop
  
+   @spec insert(map(), boolean(), boolean(), boolean()) :: {:ok, Activity.t()} | {:error, any()}
    def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when is_map(map) do
      with nil <- Activity.normalize(map),
           map <- lazy_put_activity_defaults(map, fake),
           {:containment, :ok} <- {:containment, Containment.contain_child(map)},
           {:ok, map, object} <- insert_full_object(map) do
        {:ok, activity} =
 -        Repo.insert(%Activity{
 +        %Activity{
            data: map,
            local: local,
            actor: map["actor"],
            recipients: recipients
 -        })
 +        }
 +        |> Repo.insert()
 +        |> maybe_create_activity_expiration()
  
        # Splice in the child object if we have one.
        activity =
      end
    end
  
 +  defp maybe_create_activity_expiration({:ok, %{data: %{"expires_at" => expires_at}} = activity}) do
 +    with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
 +      {:ok, activity}
 +    end
 +  end
 +
 +  defp maybe_create_activity_expiration(result), do: result
 +
    defp create_or_bump_conversation(activity, actor) do
      with {:ok, conversation} <- Conversation.create_or_bump_for(activity),
           %User{} = user <- User.get_cached_by_ap_id(actor),
      :noop
    end
  
-   def create(%{to: to, actor: actor, context: context, object: object} = params, fake \\ false) do
+   @spec create(map(), boolean()) :: {:ok, Activity.t()} | {:error, any()}
+   def create(params, fake \\ false) do
+     with {:ok, result} <- Repo.transaction(fn -> do_create(params, fake) end) do
+       result
+     end
+   end
+   defp do_create(%{to: to, actor: actor, context: context, object: object} = params, fake) do
      additional = params[:additional] || %{}
      # only accept false as false value
      local = !(params[:local] == false)
      published = params[:published]
-     quick_insert? = Pleroma.Config.get([:env]) == :benchmark
+     quick_insert? = Config.get([:env]) == :benchmark
  
      with create_data <-
             make_create_data(
          {:ok, activity}
  
        {:error, message} ->
-         {:error, message}
+         Repo.rollback(message)
      end
    end
  
+   @spec listen(map()) :: {:ok, Activity.t()} | {:error, any()}
    def listen(%{to: to, actor: actor, context: context, object: object} = params) do
      additional = params[:additional] || %{}
      # only accept false as false value
           {:ok, activity} <- insert(listen_data, local),
           :ok <- maybe_federate(activity) do
        {:ok, activity}
-     else
-       {:error, message} ->
-         {:error, message}
      end
    end
  
+   @spec accept(map()) :: {:ok, Activity.t()} | {:error, any()}
    def accept(params) do
      accept_or_reject("Accept", params)
    end
  
+   @spec reject(map()) :: {:ok, Activity.t()} | {:error, any()}
    def reject(params) do
      accept_or_reject("Reject", params)
    end
  
+   @spec accept_or_reject(String.t(), map()) :: {:ok, Activity.t()} | {:error, any()}
    def accept_or_reject(type, %{to: to, actor: actor, object: object} = params) do
      local = Map.get(params, :local, true)
      activity_id = Map.get(params, :activity_id, nil)
      end
    end
  
+   @spec update(map()) :: {:ok, Activity.t()} | {:error, any()}
    def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
      local = !(params[:local] == false)
      activity_id = params[:activity_id]
      end
    end
  
+   @spec react_with_emoji(User.t(), Object.t(), String.t(), keyword()) ::
+           {:ok, Activity.t(), Object.t()} | {:error, any()}
    def react_with_emoji(user, object, emoji, options \\ []) do
+     with {:ok, result} <-
+            Repo.transaction(fn -> do_react_with_emoji(user, object, emoji, options) end) do
+       result
+     end
+   end
+   defp do_react_with_emoji(user, object, emoji, options) do
      with local <- Keyword.get(options, :local, true),
           activity_id <- Keyword.get(options, :activity_id, nil),
           true <- Pleroma.Emoji.is_unicode_emoji?(emoji),
           :ok <- maybe_federate(activity) do
        {:ok, activity, object}
      else
-       e -> {:error, e}
+       false -> {:error, false}
+       {:error, error} -> Repo.rollback(error)
      end
    end
  
+   @spec unreact_with_emoji(User.t(), String.t(), keyword()) ::
+           {:ok, Activity.t(), Object.t()} | {:error, any()}
    def unreact_with_emoji(user, reaction_id, options \\ []) do
+     with {:ok, result} <-
+            Repo.transaction(fn -> do_unreact_with_emoji(user, reaction_id, options) end) do
+       result
+     end
+   end
+   defp do_unreact_with_emoji(user, reaction_id, options) do
      with local <- Keyword.get(options, :local, true),
           activity_id <- Keyword.get(options, :activity_id, nil),
           user_ap_id <- user.ap_id,
           :ok <- maybe_federate(activity) do
        {:ok, activity, object}
      else
-       e -> {:error, e}
+       {:error, error} -> Repo.rollback(error)
      end
    end
  
    # TODO: This is weird, maybe we shouldn't check here if we can make the activity.
-   def like(
-         %User{ap_id: ap_id} = user,
-         %Object{data: %{"id" => _}} = object,
-         activity_id \\ nil,
-         local \\ true
-       ) do
+   @spec like(User.t(), Object.t(), String.t() | nil, boolean()) ::
+           {:ok, Activity.t(), Object.t()} | {:error, any()}
+   def like(user, object, activity_id \\ nil, local \\ true) do
+     with {:ok, result} <- Repo.transaction(fn -> do_like(user, object, activity_id, local) end) do
+       result
+     end
+   end
+   defp do_like(
+          %User{ap_id: ap_id} = user,
+          %Object{data: %{"id" => _}} = object,
+          activity_id,
+          local
+        ) do
      with nil <- get_existing_like(ap_id, object),
           like_data <- make_like_data(user, object, activity_id),
           {:ok, activity} <- insert(like_data, local),
           :ok <- maybe_federate(activity) do
        {:ok, activity, object}
      else
-       %Activity{} = activity -> {:ok, activity, object}
-       error -> {:error, error}
+       %Activity{} = activity ->
+         {:ok, activity, object}
+       {:error, error} ->
+         Repo.rollback(error)
      end
    end
  
+   @spec unlike(User.t(), Object.t(), String.t() | nil, boolean()) ::
+           {:ok, Activity.t(), Activity.t(), Object.t()} | {:ok, Object.t()} | {:error, any()}
    def unlike(%User{} = actor, %Object{} = object, activity_id \\ nil, local \\ true) do
+     with {:ok, result} <-
+            Repo.transaction(fn -> do_unlike(actor, object, activity_id, local) end) do
+       result
+     end
+   end
+   defp do_unlike(actor, object, activity_id, local) do
      with %Activity{} = like_activity <- get_existing_like(actor.ap_id, object),
           unlike_data <- make_unlike_data(actor, like_activity, activity_id),
           {:ok, unlike_activity} <- insert(unlike_data, local),
           :ok <- maybe_federate(unlike_activity) do
        {:ok, unlike_activity, like_activity, object}
      else
-       _e -> {:ok, object}
+       nil -> {:ok, object}
+       {:error, error} -> Repo.rollback(error)
      end
    end
  
+   @spec announce(User.t(), Object.t(), String.t() | nil, boolean(), boolean()) ::
+           {:ok, Activity.t(), Object.t()} | {:error, any()}
    def announce(
          %User{ap_id: _} = user,
          %Object{data: %{"id" => _}} = object,
          local \\ true,
          public \\ true
        ) do
+     with {:ok, result} <-
+            Repo.transaction(fn -> do_announce(user, object, activity_id, local, public) end) do
+       result
+     end
+   end
+   defp do_announce(user, object, activity_id, local, public) do
      with true <- is_announceable?(object, user, public),
           announce_data <- make_announce_data(user, object, activity_id, public),
           {:ok, activity} <- insert(announce_data, local),
           :ok <- maybe_federate(activity) do
        {:ok, activity, object}
      else
-       error -> {:error, error}
+       false -> {:error, false}
+       {:error, error} -> Repo.rollback(error)
      end
    end
  
+   @spec unannounce(User.t(), Object.t(), String.t() | nil, boolean()) ::
+           {:ok, Activity.t(), Object.t()} | {:ok, Object.t()} | {:error, any()}
    def unannounce(
          %User{} = actor,
          %Object{} = object,
          activity_id \\ nil,
          local \\ true
        ) do
+     with {:ok, result} <-
+            Repo.transaction(fn -> do_unannounce(actor, object, activity_id, local) end) do
+       result
+     end
+   end
+   defp do_unannounce(actor, object, activity_id, local) do
      with %Activity{} = announce_activity <- get_existing_announce(actor.ap_id, object),
           unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id),
           {:ok, unannounce_activity} <- insert(unannounce_data, local),
           {:ok, object} <- remove_announce_from_object(announce_activity, object) do
        {:ok, unannounce_activity, object}
      else
-       _e -> {:ok, object}
+       nil -> {:ok, object}
+       {:error, error} -> Repo.rollback(error)
      end
    end
  
+   @spec follow(User.t(), User.t(), String.t() | nil, boolean()) ::
+           {:ok, Activity.t()} | {:error, any()}
    def follow(follower, followed, activity_id \\ nil, local \\ true) do
+     with {:ok, result} <-
+            Repo.transaction(fn -> do_follow(follower, followed, activity_id, local) end) do
+       result
+     end
+   end
+   defp do_follow(follower, followed, activity_id, local) do
      with data <- make_follow_data(follower, followed, activity_id),
           {:ok, activity} <- insert(data, local),
           :ok <- maybe_federate(activity),
           _ <- User.set_follow_state_cache(follower.ap_id, followed.ap_id, activity.data["state"]) do
        {:ok, activity}
+     else
+       {:error, error} -> Repo.rollback(error)
      end
    end
  
+   @spec unfollow(User.t(), User.t(), String.t() | nil, boolean()) ::
+           {:ok, Activity.t()} | nil | {:error, any()}
    def unfollow(follower, followed, activity_id \\ nil, local \\ true) do
+     with {:ok, result} <-
+            Repo.transaction(fn -> do_unfollow(follower, followed, activity_id, local) end) do
+       result
+     end
+   end
+   defp do_unfollow(follower, followed, activity_id, local) do
      with %Activity{} = follow_activity <- fetch_latest_follow(follower, followed),
           {:ok, follow_activity} <- update_follow_state(follow_activity, "cancelled"),
           unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id),
           {:ok, activity} <- insert(unfollow_data, local),
           :ok <- maybe_federate(activity) do
        {:ok, activity}
+     else
+       nil -> nil
+       {:error, error} -> Repo.rollback(error)
+     end
+   end
+   @spec delete(User.t() | Object.t(), keyword()) :: {:ok, User.t() | Object.t()} | {:error, any()}
+   def delete(entity, options \\ []) do
+     with {:ok, result} <- Repo.transaction(fn -> do_delete(entity, options) end) do
+       result
      end
    end
  
-   def delete(%User{ap_id: ap_id, follower_address: follower_address} = user) do
+   defp do_delete(%User{ap_id: ap_id, follower_address: follower_address} = user, _) do
      with data <- %{
             "to" => [follower_address],
             "type" => "Delete",
      end
    end
  
-   def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, options \\ []) do
+   defp do_delete(%Object{data: %{"id" => id, "actor" => actor}} = object, options) do
      local = Keyword.get(options, :local, true)
      activity_id = Keyword.get(options, :activity_id, nil)
      actor = Keyword.get(options, :actor, actor)
           {:ok, _actor} <- decrease_note_count_if_public(user, object),
           :ok <- maybe_federate(activity) do
        {:ok, activity}
+     else
+       {:error, error} ->
+         Repo.rollback(error)
      end
    end
  
-   @spec block(User.t(), User.t(), String.t() | nil, boolean) :: {:ok, Activity.t() | nil}
+   @spec block(User.t(), User.t(), String.t() | nil, boolean()) ::
+           {:ok, Activity.t()} | {:error, any()}
    def block(blocker, blocked, activity_id \\ nil, local \\ true) do
+     with {:ok, result} <-
+            Repo.transaction(fn -> do_block(blocker, blocked, activity_id, local) end) do
+       result
+     end
+   end
+   defp do_block(blocker, blocked, activity_id, local) do
      outgoing_blocks = Config.get([:activitypub, :outgoing_blocks])
      unfollow_blocked = Config.get([:activitypub, :unfollow_blocked])
  
           :ok <- maybe_federate(activity) do
        {:ok, activity}
      else
-       _e -> {:ok, nil}
+       {:error, error} -> Repo.rollback(error)
      end
    end
  
+   @spec unblock(User.t(), User.t(), String.t() | nil, boolean()) ::
+           {:ok, Activity.t()} | {:error, any()} | nil
    def unblock(blocker, blocked, activity_id \\ nil, local \\ true) do
+     with {:ok, result} <-
+            Repo.transaction(fn -> do_unblock(blocker, blocked, activity_id, local) end) do
+       result
+     end
+   end
+   defp do_unblock(blocker, blocked, activity_id, local) do
      with %Activity{} = block_activity <- fetch_latest_block(blocker, blocked),
           unblock_data <- make_unblock_data(blocker, blocked, block_activity, activity_id),
           {:ok, activity} <- insert(unblock_data, local),
           :ok <- maybe_federate(activity) do
        {:ok, activity}
+     else
+       nil -> nil
+       {:error, error} -> Repo.rollback(error)
      end
    end
  
-   @spec flag(map()) :: {:ok, Activity.t()} | any
+   @spec flag(map()) :: {:ok, Activity.t()} | {:error, any()}
    def flag(
          %{
            actor: actor,
      end
    end
  
+   @spec move(User.t(), User.t(), boolean()) :: {:ok, Activity.t()} | {:error, any()}
    def move(%User{} = origin, %User{} = target, local \\ true) do
      params = %{
        "type" => "Move",
    end
  
    defp fetch_activities_for_context_query(context, opts) do
-     public = [Pleroma.Constants.as_public()]
+     public = [Constants.as_public()]
  
      recipients =
        if opts["user"],
      |> Repo.one()
    end
  
+   @spec fetch_public_activities(map(), Pagination.type()) :: [Activity.t()]
    def fetch_public_activities(opts \\ %{}, pagination \\ :keyset) do
      opts = Map.drop(opts, ["user"])
  
-     [Pleroma.Constants.as_public()]
+     [Constants.as_public()]
      |> fetch_activities_query(opts)
      |> restrict_unlisted()
      |> Pagination.fetch_paginated(opts, pagination)
      |> Enum.reverse()
    end
  
-   def fetch_instance_activities(params) do
+   def fetch_statuses(reading_user, params) do
      params =
        params
        |> Map.put("type", ["Create", "Announce"])
-       |> Map.put("instance", params["instance"])
  
-     fetch_activities([Pleroma.Constants.as_public()], params, :offset)
+     recipients =
+       user_activities_recipients(%{
+         "godmode" => params["godmode"],
+         "reading_user" => reading_user
+       })
+     fetch_activities(recipients, params, :offset)
      |> Enum.reverse()
    end
  
  
    defp user_activities_recipients(%{"reading_user" => reading_user}) do
      if reading_user do
-       [Pleroma.Constants.as_public()] ++ [reading_user.ap_id | User.following(reading_user)]
+       [Constants.as_public()] ++ [reading_user.ap_id | User.following(reading_user)]
      else
-       [Pleroma.Constants.as_public()]
+       [Constants.as_public()]
      end
    end
  
          fragment(
            "not (coalesce(?->'cc', '{}'::jsonb) \\?| ?)",
            activity.data,
-           ^[Pleroma.Constants.as_public()]
+           ^[Constants.as_public()]
          )
      )
    end
    @doc """
    Fetch favorites activities of user with order by sort adds to favorites
    """
-   @spec fetch_favourites(User.t(), map(), atom()) :: list(Activity.t())
+   @spec fetch_favourites(User.t(), map(), Pagination.type()) :: list(Activity.t())
    def fetch_favourites(user, params \\ %{}, pagination \\ :keyset) do
      user.ap_id
      |> Activity.Queries.by_actor()
        where:
          fragment("? && ?", activity.recipients, ^recipients) or
            (fragment("? && ?", activity.recipients, ^recipients_with_public) and
-              ^Pleroma.Constants.as_public() in activity.recipients)
+              ^Constants.as_public() in activity.recipients)
      )
    end
  
      |> Enum.reverse()
    end
  
+   @spec upload(Upload.source(), keyword()) :: {:ok, Object.t()} | {:error, any()}
    def upload(file, opts \\ []) do
      with {:ok, data} <- Upload.store(file, opts) do
        obj_data =
    defp normalize_counter(_), do: 0
  
    defp maybe_update_follow_information(data) do
-     with {:enabled, true} <-
-            {:enabled, Pleroma.Config.get([:instance, :external_user_synchronization])},
+     with {:enabled, true} <- {:enabled, Config.get([:instance, :external_user_synchronization])},
           {:ok, info} <- fetch_follow_information_for_user(data) do
        info = Map.merge(data[:info] || %{}, info)
        Map.put(data, :info, info)
index 03921de275a6a509c9a496c8d277210fbfe3fa02,027b3dc306c0715a81dc51b9b15ddeede441620c..4ec13aafad21c840c059ba385650beae6d91e6cb
@@@ -1,5 -1,5 +1,5 @@@
  # Pleroma: A lightweight social networking server
- # Copyright Â© 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # Copyright Â© 2017-2020 Pleroma Authors <https://pleroma.social/>
  # SPDX-License-Identifier: AGPL-3.0-only
  
  defmodule Pleroma.Web.CommonAPI do
  
    def post(user, %{"status" => _} = data) do
      with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
 -      draft.changes
 -      |> ActivityPub.create(draft.preview?)
 -      |> maybe_create_activity_expiration(draft.expires_at)
 +      ActivityPub.create(draft.changes, draft.preview?)
      end
    end
  
 -  defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
 -    with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
 -      {:ok, activity}
 -    end
 -  end
 -
 -  defp maybe_create_activity_expiration(result, _), do: result
 -
    # Updates the emojis for a user based on their profile
    def update(user) do
      emoji = emoji_from_profile(user)
index 2cd908a8744c68a7786a46f39faf09489a14ef27,3dd3dd04dcd878959cbba77e38b9398d5cc3ce12..8c599faf3a5d3075d08eb9f2a4514e696b7b35aa
@@@ -1,5 -1,5 +1,5 @@@
  # Pleroma: A lightweight social networking server
- # Copyright Â© 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # Copyright Â© 2017-2020 Pleroma Authors <https://pleroma.social/>
  # SPDX-License-Identifier: AGPL-3.0-only
  
  defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
@@@ -8,6 -8,7 +8,7 @@@
  
    alias Pleroma.Activity
    alias Pleroma.Builders.ActivityBuilder
+   alias Pleroma.Config
    alias Pleroma.Notification
    alias Pleroma.Object
    alias Pleroma.User
@@@ -15,6 -16,7 +16,7 @@@
    alias Pleroma.Web.ActivityPub.Utils
    alias Pleroma.Web.AdminAPI.AccountView
    alias Pleroma.Web.CommonAPI
+   alias Pleroma.Web.Federator
  
    import Pleroma.Factory
    import Tesla.Mock
  
    describe "insertion" do
      test "drops activities beyond a certain limit" do
-       limit = Pleroma.Config.get([:instance, :remote_limit])
+       limit = Config.get([:instance, :remote_limit])
  
        random_text =
          :crypto.strong_rand_bytes(limit + 1)
    end
  
    describe "create activities" do
+     test "it reverts create" do
+       user = insert(:user)
+       with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
+         assert {:error, :reverted} =
+                  ActivityPub.create(%{
+                    to: ["user1", "user2"],
+                    actor: user,
+                    context: "",
+                    object: %{
+                      "to" => ["user1", "user2"],
+                      "type" => "Note",
+                      "content" => "testing"
+                    }
+                  })
+       end
+       assert Repo.aggregate(Activity, :count, :id) == 0
+       assert Repo.aggregate(Object, :count, :id) == 0
+     end
      test "removes doubled 'to' recipients" do
        user = insert(:user)
  
    end
  
    describe "react to an object" do
-     test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do
-       Pleroma.Config.put([:instance, :federating], true)
+     test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
+       Config.put([:instance, :federating], true)
        user = insert(:user)
        reactor = insert(:user)
        {:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
  
        {:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
  
-       assert called(Pleroma.Web.Federator.publish(reaction_activity))
+       assert called(Federator.publish(reaction_activity))
      end
  
      test "adds an emoji reaction activity to the db" do
                 ["☕", [third_user.ap_id]]
               ]
      end
+     test "reverts emoji reaction on error" do
+       [user, reactor] = insert_list(2, :user)
+       {:ok, activity} = CommonAPI.post(user, %{"status" => "Status"})
+       object = Object.normalize(activity)
+       with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
+         assert {:error, :reverted} = ActivityPub.react_with_emoji(reactor, object, "😀")
+       end
+       object = Object.get_by_ap_id(object.data["id"])
+       refute object.data["reaction_count"]
+       refute object.data["reactions"]
+     end
    end
  
    describe "unreacting to an object" do
-     test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do
-       Pleroma.Config.put([:instance, :federating], true)
+     test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
+       Config.put([:instance, :federating], true)
        user = insert(:user)
        reactor = insert(:user)
        {:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
  
        {:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
  
-       assert called(Pleroma.Web.Federator.publish(reaction_activity))
+       assert called(Federator.publish(reaction_activity))
  
        {:ok, unreaction_activity, _object} =
          ActivityPub.unreact_with_emoji(reactor, reaction_activity.data["id"])
  
-       assert called(Pleroma.Web.Federator.publish(unreaction_activity))
+       assert called(Federator.publish(unreaction_activity))
      end
  
      test "adds an undo activity to the db" do
        assert object.data["reaction_count"] == 0
        assert object.data["reactions"] == []
      end
+     test "reverts emoji unreact on error" do
+       [user, reactor] = insert_list(2, :user)
+       {:ok, activity} = CommonAPI.post(user, %{"status" => "Status"})
+       object = Object.normalize(activity)
+       {:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "😀")
+       with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
+         assert {:error, :reverted} =
+                  ActivityPub.unreact_with_emoji(reactor, reaction_activity.data["id"])
+       end
+       object = Object.get_by_ap_id(object.data["id"])
+       assert object.data["reaction_count"] == 1
+       assert object.data["reactions"] == [["😀", [reactor.ap_id]]]
+     end
    end
  
    describe "like an object" do
-     test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do
-       Pleroma.Config.put([:instance, :federating], true)
+     test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
+       Config.put([:instance, :federating], true)
        note_activity = insert(:note_activity)
        assert object_activity = Object.normalize(note_activity)
  
        user = insert(:user)
  
        {:ok, like_activity, _object} = ActivityPub.like(user, object_activity)
-       assert called(Pleroma.Web.Federator.publish(like_activity))
+       assert called(Federator.publish(like_activity))
      end
  
      test "returns exist activity if object already liked" do
        assert like_activity == like_activity_exist
      end
  
+     test "reverts like activity on error" do
+       note_activity = insert(:note_activity)
+       object = Object.normalize(note_activity)
+       user = insert(:user)
+       with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
+         assert {:error, :reverted} = ActivityPub.like(user, object)
+       end
+       assert Repo.aggregate(Activity, :count, :id) == 1
+       assert Repo.get(Object, object.id) == object
+     end
      test "adds a like activity to the db" do
        note_activity = insert(:note_activity)
        assert object = Object.normalize(note_activity)
    end
  
    describe "unliking" do
-     test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do
-       Pleroma.Config.put([:instance, :federating], true)
+     test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
+       Config.put([:instance, :federating], true)
  
        note_activity = insert(:note_activity)
        object = Object.normalize(note_activity)
        user = insert(:user)
  
        {:ok, object} = ActivityPub.unlike(user, object)
-       refute called(Pleroma.Web.Federator.publish())
+       refute called(Federator.publish())
  
        {:ok, _like_activity, object} = ActivityPub.like(user, object)
        assert object.data["like_count"] == 1
        {:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object)
        assert object.data["like_count"] == 0
  
-       assert called(Pleroma.Web.Federator.publish(unlike_activity))
+       assert called(Federator.publish(unlike_activity))
+     end
+     test "reverts unliking on error" do
+       note_activity = insert(:note_activity)
+       object = Object.normalize(note_activity)
+       user = insert(:user)
+       {:ok, like_activity, object} = ActivityPub.like(user, object)
+       assert object.data["like_count"] == 1
+       with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
+         assert {:error, :reverted} = ActivityPub.unlike(user, object)
+       end
+       assert Object.get_by_ap_id(object.data["id"]) == object
+       assert object.data["like_count"] == 1
+       assert Activity.get_by_id(like_activity.id)
      end
  
      test "unliking a previously liked object" do
        assert announce_activity.data["actor"] == user.ap_id
        assert announce_activity.data["context"] == object.data["context"]
      end
+     test "reverts annouce from object on error" do
+       note_activity = insert(:note_activity)
+       object = Object.normalize(note_activity)
+       user = insert(:user)
+       with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
+         assert {:error, :reverted} = ActivityPub.announce(user, object)
+       end
+       reloaded_object = Object.get_by_ap_id(object.data["id"])
+       assert reloaded_object == object
+       refute reloaded_object.data["announcement_count"]
+       refute reloaded_object.data["announcements"]
+     end
    end
  
    describe "announcing a private object" do
        user = insert(:user)
  
        # Unannouncing an object that is not announced does nothing
-       {:ok, object} = ActivityPub.unannounce(user, object)
-       # assert object.data["announcement_count"] == 0
+       {:ok, object} = ActivityPub.unannounce(user, object)
+       refute object.data["announcement_count"]
  
        {:ok, announce_activity, object} = ActivityPub.announce(user, object)
        assert object.data["announcement_count"] == 1
  
        assert Activity.get_by_id(announce_activity.id) == nil
      end
+     test "reverts unannouncing on error" do
+       note_activity = insert(:note_activity)
+       object = Object.normalize(note_activity)
+       user = insert(:user)
+       {:ok, _announce_activity, object} = ActivityPub.announce(user, object)
+       assert object.data["announcement_count"] == 1
+       with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
+         assert {:error, :reverted} = ActivityPub.unannounce(user, object)
+       end
+       object = Object.get_by_ap_id(object.data["id"])
+       assert object.data["announcement_count"] == 1
+     end
    end
  
    describe "uploading files" do
    end
  
    describe "following / unfollowing" do
+     test "it reverts follow activity" do
+       follower = insert(:user)
+       followed = insert(:user)
+       with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
+         assert {:error, :reverted} = ActivityPub.follow(follower, followed)
+       end
+       assert Repo.aggregate(Activity, :count, :id) == 0
+       assert Repo.aggregate(Object, :count, :id) == 0
+     end
+     test "it reverts unfollow activity" do
+       follower = insert(:user)
+       followed = insert(:user)
+       {:ok, follow_activity} = ActivityPub.follow(follower, followed)
+       with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
+         assert {:error, :reverted} = ActivityPub.unfollow(follower, followed)
+       end
+       activity = Activity.get_by_id(follow_activity.id)
+       assert activity.data["type"] == "Follow"
+       assert activity.data["actor"] == follower.ap_id
+       assert activity.data["object"] == followed.ap_id
+     end
      test "creates a follow activity" do
        follower = insert(:user)
        followed = insert(:user)
    end
  
    describe "blocking / unblocking" do
+     test "reverts block activity on error" do
+       [blocker, blocked] = insert_list(2, :user)
+       with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
+         assert {:error, :reverted} = ActivityPub.block(blocker, blocked)
+       end
+       assert Repo.aggregate(Activity, :count, :id) == 0
+       assert Repo.aggregate(Object, :count, :id) == 0
+     end
      test "creates a block activity" do
        blocker = insert(:user)
        blocked = insert(:user)
        assert activity.data["object"] == blocked.ap_id
      end
  
+     test "reverts unblock activity on error" do
+       [blocker, blocked] = insert_list(2, :user)
+       {:ok, block_activity} = ActivityPub.block(blocker, blocked)
+       with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
+         assert {:error, :reverted} = ActivityPub.unblock(blocker, blocked)
+       end
+       assert block_activity.data["type"] == "Block"
+       assert block_activity.data["actor"] == blocker.ap_id
+       assert Repo.aggregate(Activity, :count, :id) == 1
+       assert Repo.aggregate(Object, :count, :id) == 1
+     end
      test "creates an undo activity for the last block" do
        blocker = insert(:user)
        blocked = insert(:user)
    end
  
    describe "deletion" do
+     clear_config([:instance, :rewrite_policy])
+     test "it reverts deletion on error" do
+       note = insert(:note_activity)
+       object = Object.normalize(note)
+       with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
+         assert {:error, :reverted} = ActivityPub.delete(object)
+       end
+       assert Repo.aggregate(Activity, :count, :id) == 1
+       assert Repo.get(Object, object.id) == object
+       assert Activity.get_by_id(note.id) == note
+     end
      test "it creates a delete activity and deletes the original object" do
        note = insert(:note_activity)
        object = Object.normalize(note)
      end
  
      test "it passes delete activity through MRF before deleting the object" do
-       rewrite_policy = Pleroma.Config.get([:instance, :rewrite_policy])
        Pleroma.Config.put([:instance, :rewrite_policy], Pleroma.Web.ActivityPub.MRF.DropPolicy)
  
-       on_exit(fn -> Pleroma.Config.put([:instance, :rewrite_policy], rewrite_policy) end)
        note = insert(:note_activity)
        object = Object.normalize(note)
  
    end
  
    describe "update" do
+     clear_config([:instance, :max_pinned_statuses])
      test "it creates an update activity with the new user data" do
        user = insert(:user)
        {:ok, user} = User.ensure_keys_present(user)
    end
  
    test "returned pinned statuses" do
-     Pleroma.Config.put([:instance, :max_pinned_statuses], 3)
+     Config.put([:instance, :max_pinned_statuses], 3)
      user = insert(:user)
  
      {:ok, activity_one} = CommonAPI.post(user, %{"status" => "HI!!!"})
                 ActivityPub.move(old_user, new_user)
      end
    end
 +
 +  describe "global activity expiration" do
 +    clear_config([:instance, :rewrite_policy])
 +
 +    test "creates an activity expiration for local Create activities" do
 +      Pleroma.Config.put(
 +        [:instance, :rewrite_policy],
 +        Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy
 +      )
 +
 +      {:ok, %{id: id_create}} = ActivityBuilder.insert(%{"type" => "Create", "context" => "3hu"})
 +      {:ok, _follow} = ActivityBuilder.insert(%{"type" => "Follow", "context" => "3hu"})
 +
 +      assert [%{activity_id: ^id_create}] = Pleroma.ActivityExpiration |> Repo.all()
 +    end
 +  end
  end
index c6c7ff388fa1eac2d867f094992b38406b4a66da,56c5aa4096274b3a6fceae1af4f7a2e04bc5c209..11b696bd80b1f3cdf10969e691060bc7b95e6f49
@@@ -1,5 -1,5 +1,5 @@@
  # Pleroma: A lightweight social networking server
- # Copyright Â© 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # Copyright Â© 2017-2020 Pleroma Authors <https://pleroma.social/>
  # SPDX-License-Identifier: AGPL-3.0-only
  
  defmodule Pleroma.Workers.Cron.PurgeExpiredActivitiesWorkerTest do
@@@ -12,7 -12,6 +12,7 @@@
    import ExUnit.CaptureLog
  
    clear_config([ActivityExpiration, :enabled])
 +  clear_config([:instance, :rewrite_policy])
  
    test "deletes an expiration activity" do
      Pleroma.Config.put([ActivityExpiration, :enabled], true)
      refute Pleroma.Repo.get(Pleroma.ActivityExpiration, expiration.id)
    end
  
 +  test "works with ActivityExpirationPolicy" do
 +    Pleroma.Config.put([ActivityExpiration, :enabled], true)
 +
 +    Pleroma.Config.put(
 +      [:instance, :rewrite_policy],
 +      Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy
 +    )
 +
 +    user = insert(:user)
 +
 +    days = Pleroma.Config.get([:mrf_activity_expiration, :days], 365)
 +
 +    {:ok, %{id: id} = activity} = Pleroma.Web.CommonAPI.post(user, %{"status" => "cofe"})
 +
 +    past_date =
 +      NaiveDateTime.utc_now() |> Timex.shift(days: -days) |> NaiveDateTime.truncate(:second)
 +
 +    activity
 +    |> Repo.preload(:expiration)
 +    |> Map.get(:expiration)
 +    |> Ecto.Changeset.change(%{scheduled_at: past_date})
 +    |> Repo.update!()
 +
 +    Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker.perform(:ops, :pid)
 +
 +    assert [%{data: %{"type" => "Delete", "deleted_activity_id" => ^id}}] =
 +             Pleroma.Repo.all(Pleroma.Activity)
 +  end
 +
    describe "delete_activity/1" do
      test "adds log message if activity isn't find" do
        assert capture_log([level: :error], fn ->