Merge branch 'develop' into global-status-expiration
authorEgor Kislitsyn <egor@kislitsyn.com>
Fri, 10 Apr 2020 10:20:48 +0000 (14:20 +0400)
committerEgor Kislitsyn <egor@kislitsyn.com>
Fri, 10 Apr 2020 10:20:48 +0000 (14:20 +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

diff --combined CHANGELOG.md
index 79b4bbb5318c5b3ce1fa78fd1bbeb114f7252d0f,bac69ad6a3ea15e5d8f191fd10dc0de742af1938..475fc5d89a72d515fcfbf7b6224ae27e9ee507f4
@@@ -4,21 -4,62 +4,67 @@@ All notable changes to this project wil
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
  
  ## [unreleased]
- ### Changed
- - **Breaking:** BBCode and Markdown formatters will no longer return any `\n` and only use `<br/>` for newlines
- - MFR policy to set global expiration for all local Create activities
 +
  ### Removed
  - **Breaking:** removed `with_move` parameter from notifications timeline.
  
  ### Added
  - NodeInfo: `pleroma:api/v1/notifications:include_types_filter` to the `features` list.
+ - NodeInfo: `pleroma_emoji_reactions` to the `features` list.
  - Configuration: `:restrict_unauthenticated` setting, restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses.
+ - New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires minimum OTP version of 22.2 otherwise Pleroma won’t start. For hackney OTP update is not required.
  <details>
    <summary>API Changes</summary>
  - Mastodon API: Support for `include_types` in `/api/v1/notifications`.
+ - Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint.
  </details>
  
++### Changed
++- MFR policy to set global expiration for all local Create activities
++
++
+ ## [2.0.2] - 2020-04-08
+ ### Added
+ - Support for Funkwhale's `Audio` activity
+ - Admin API: `PATCH /api/pleroma/admin/users/:nickname/update_credentials`
+ ### Fixed
+ - Blocked/muted users still generating push notifications
+ - Input textbox for bio ignoring newlines
+ - OTP: Inability to use PostgreSQL databases with SSL
+ - `user delete_activities` breaking when trying to delete already deleted posts
+ - Incorrect URL for Funkwhale channels
+ ### Upgrade notes
+ 1. Restart Pleroma
+ ## [2.0.1] - 2020-03-15
+ ### Security
+ - Static-FE: Fix remote posts not being sanitized
+ ### Fixed
+ - 500 errors when no `Accept` header is present if Static-FE is enabled
+ - Instance panel not being updated immediately due to wrong `Cache-Control` headers
+ - Statuses posted with BBCode/Markdown having unncessary newlines in Pleroma-FE
+ - OTP: Fix some settings not being migrated to in-database config properly
+ - No `Cache-Control` headers on attachment/media proxy requests
+ - Character limit enforcement being off by 1
+ - Mastodon Streaming API: hashtag timelines not working
+ ### Changed
+ - BBCode and Markdown formatters will no longer return any `\n` and only use `<br/>` for newlines
+ - Mastodon API: Allow registration without email if email verification is not enabled
+ ### Upgrade notes
+ #### Nginx only
+ 1. Remove `proxy_ignore_headers Cache-Control;` and `proxy_hide_header  Cache-Control;` from your config.
+ #### Everyone
+ 1. Run database migrations (inside Pleroma directory):
+   - OTP: `./bin/pleroma_ctl migrate`
+   - From Source: `mix ecto.migrate`
+ 2. Restart Pleroma
  ## [2.0.0] - 2019-03-08
  ### Security
  - Mastodon API: Fix being able to request enourmous amount of statuses in timelines leading to DoS. Now limited to 40 per request.
  - 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.
+ - Admin API: `PATCH /api/pleroma/admin/users/:nickname/credentials` and `GET /api/pleroma/admin/users/:nickname/credentials`
  </details>
  
  ### Added
diff --combined config/config.exs
index 05c55074afd682bc6ae97e6e6076eaad11546385,232a91bf132c8063f8f0f730121bb68eade6ad6f..8670a57b9ed44ce96d3c71b234d46459d2faa354
@@@ -58,20 -58,6 +58,6 @@@ config :pleroma, Pleroma.Captcha
  
  config :pleroma, Pleroma.Captcha.Kocaptcha, endpoint: "https://captcha.kotobank.ch"
  
- config :pleroma, :hackney_pools,
-   federation: [
-     max_connections: 50,
-     timeout: 150_000
-   ],
-   media: [
-     max_connections: 50,
-     timeout: 150_000
-   ],
-   upload: [
-     max_connections: 25,
-     timeout: 300_000
-   ]
  # Upload configuration
  config :pleroma, Pleroma.Upload,
    uploader: Pleroma.Uploaders.Local,
@@@ -184,21 -170,13 +170,13 @@@ config :mime, :types, %
    "application/ld+json" => ["activity+json"]
  }
  
- config :tesla, adapter: Tesla.Adapter.Hackney
+ config :tesla, adapter: Tesla.Adapter.Gun
  # Configures http settings, upstream proxy etc.
  config :pleroma, :http,
    proxy_url: nil,
    send_user_agent: true,
    user_agent: :default,
-   adapter: [
-     ssl_options: [
-       # Workaround for remote server certificate chain issues
-       partial_chain: &:hackney_connect.partial_chain/1,
-       # We don't support TLS v1.3 yet
-       versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"]
-     ]
-   ]
+   adapter: []
  
  config :pleroma, :instance,
    name: "Pleroma",
@@@ -365,8 -343,6 +343,8 @@@ config :pleroma, :mrf_keyword
  
  config :pleroma, :mrf_subchain, match_actor: %{}
  
 +config :pleroma, :mrf_activity_expiration, days: 365
 +
  config :pleroma, :mrf_vocabulary,
    accept: [],
    reject: []
@@@ -626,6 -602,49 +604,49 @@@ config :pleroma, Pleroma.Repo
    parameters: [gin_fuzzy_search_limit: "500"],
    prepare: :unnamed
  
+ config :pleroma, :connections_pool,
+   checkin_timeout: 250,
+   max_connections: 250,
+   retry: 1,
+   retry_timeout: 1000,
+   await_up_timeout: 5_000
+ config :pleroma, :pools,
+   federation: [
+     size: 50,
+     max_overflow: 10,
+     timeout: 150_000
+   ],
+   media: [
+     size: 50,
+     max_overflow: 10,
+     timeout: 150_000
+   ],
+   upload: [
+     size: 25,
+     max_overflow: 5,
+     timeout: 300_000
+   ],
+   default: [
+     size: 10,
+     max_overflow: 2,
+     timeout: 10_000
+   ]
+ config :pleroma, :hackney_pools,
+   federation: [
+     max_connections: 50,
+     timeout: 150_000
+   ],
+   media: [
+     max_connections: 50,
+     timeout: 150_000
+   ],
+   upload: [
+     max_connections: 25,
+     timeout: 300_000
+   ]
  config :pleroma, :restrict_unauthenticated,
    timelines: %{local: false, federated: false},
    profiles: %{local: false, remote: false},
diff --combined config/description.exs
index 9b2acea7eb8c939ce3f3277850bb3d7eb7f53f3d,642f1a3ce9d2be466a15fe24495dfb189bf8ed0b..b34794da1e47fbec8d683db1b0fac6ff11341192
@@@ -1346,21 -1346,6 +1346,21 @@@ config :pleroma, :config_description, 
        }
      ]
    },
 +  %{
 +    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: :relations_actions,
          type: [:tuple, {:list, :tuple}],
-         description: "For actions on relations with all users (follow, unfollow)",
+         description: "For actions on relationships with all users (follow, unfollow)",
          suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]]
        },
        %{
        }
      ]
    },
+   %{
+     group: :pleroma,
+     key: :connections_pool,
+     type: :group,
+     description: "Advanced settings for `gun` connections pool",
+     children: [
+       %{
+         key: :checkin_timeout,
+         type: :integer,
+         description: "Timeout to checkin connection from pool. Default: 250ms.",
+         suggestions: [250]
+       },
+       %{
+         key: :max_connections,
+         type: :integer,
+         description: "Maximum number of connections in the pool. Default: 250 connections.",
+         suggestions: [250]
+       },
+       %{
+         key: :retry,
+         type: :integer,
+         description:
+           "Number of retries, while `gun` will try to reconnect if connection goes down. Default: 1.",
+         suggestions: [1]
+       },
+       %{
+         key: :retry_timeout,
+         type: :integer,
+         description:
+           "Time between retries when `gun` will try to reconnect in milliseconds. Default: 1000ms.",
+         suggestions: [1000]
+       },
+       %{
+         key: :await_up_timeout,
+         type: :integer,
+         description: "Timeout while `gun` will wait until connection is up. Default: 5000ms.",
+         suggestions: [5000]
+       }
+     ]
+   },
+   %{
+     group: :pleroma,
+     key: :pools,
+     type: :group,
+     description: "Advanced settings for `gun` workers pools",
+     children: [
+       %{
+         key: :federation,
+         type: :keyword,
+         description: "Settings for federation pool.",
+         children: [
+           %{
+             key: :size,
+             type: :integer,
+             description: "Number workers in the pool.",
+             suggestions: [50]
+           },
+           %{
+             key: :max_overflow,
+             type: :integer,
+             description: "Number of additional workers if pool is under load.",
+             suggestions: [10]
+           },
+           %{
+             key: :timeout,
+             type: :integer,
+             description: "Timeout while `gun` will wait for response.",
+             suggestions: [150_000]
+           }
+         ]
+       },
+       %{
+         key: :media,
+         type: :keyword,
+         description: "Settings for media pool.",
+         children: [
+           %{
+             key: :size,
+             type: :integer,
+             description: "Number workers in the pool.",
+             suggestions: [50]
+           },
+           %{
+             key: :max_overflow,
+             type: :integer,
+             description: "Number of additional workers if pool is under load.",
+             suggestions: [10]
+           },
+           %{
+             key: :timeout,
+             type: :integer,
+             description: "Timeout while `gun` will wait for response.",
+             suggestions: [150_000]
+           }
+         ]
+       },
+       %{
+         key: :upload,
+         type: :keyword,
+         description: "Settings for upload pool.",
+         children: [
+           %{
+             key: :size,
+             type: :integer,
+             description: "Number workers in the pool.",
+             suggestions: [25]
+           },
+           %{
+             key: :max_overflow,
+             type: :integer,
+             description: "Number of additional workers if pool is under load.",
+             suggestions: [5]
+           },
+           %{
+             key: :timeout,
+             type: :integer,
+             description: "Timeout while `gun` will wait for response.",
+             suggestions: [300_000]
+           }
+         ]
+       },
+       %{
+         key: :default,
+         type: :keyword,
+         description: "Settings for default pool.",
+         children: [
+           %{
+             key: :size,
+             type: :integer,
+             description: "Number workers in the pool.",
+             suggestions: [10]
+           },
+           %{
+             key: :max_overflow,
+             type: :integer,
+             description: "Number of additional workers if pool is under load.",
+             suggestions: [2]
+           },
+           %{
+             key: :timeout,
+             type: :integer,
+             description: "Timeout while `gun` will wait for response.",
+             suggestions: [10_000]
+           }
+         ]
+       }
+     ]
+   },
+   %{
+     group: :pleroma,
+     key: :hackney_pools,
+     type: :group,
+     description: "Advanced settings for `hackney` connections pools",
+     children: [
+       %{
+         key: :federation,
+         type: :keyword,
+         description: "Settings for federation pool.",
+         children: [
+           %{
+             key: :max_connections,
+             type: :integer,
+             description: "Number workers in the pool.",
+             suggestions: [50]
+           },
+           %{
+             key: :timeout,
+             type: :integer,
+             description: "Timeout while `hackney` will wait for response.",
+             suggestions: [150_000]
+           }
+         ]
+       },
+       %{
+         key: :media,
+         type: :keyword,
+         description: "Settings for media pool.",
+         children: [
+           %{
+             key: :max_connections,
+             type: :integer,
+             description: "Number workers in the pool.",
+             suggestions: [50]
+           },
+           %{
+             key: :timeout,
+             type: :integer,
+             description: "Timeout while `hackney` will wait for response.",
+             suggestions: [150_000]
+           }
+         ]
+       },
+       %{
+         key: :upload,
+         type: :keyword,
+         description: "Settings for upload pool.",
+         children: [
+           %{
+             key: :max_connections,
+             type: :integer,
+             description: "Number workers in the pool.",
+             suggestions: [25]
+           },
+           %{
+             key: :timeout,
+             type: :integer,
+             description: "Timeout while `hackney` will wait for response.",
+             suggestions: [300_000]
+           }
+         ]
+       }
+     ]
+   },
    %{
      group: :pleroma,
      key: :restrict_unauthenticated,
index ee1909d6602aef318b084c9da7d358014fb941f9,681ab6b93ddf4162b3deba2ad33df2782b5c777d..8b8988fa75102cd602448485c84cbc405ebcc87e
@@@ -35,7 -35,7 +35,7 @@@ To add configuration to your config fil
  * `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)).
@@@ -45,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).
@@@ -146,10 -145,6 +146,10 @@@ 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
@@@ -374,8 -369,7 +374,7 @@@ Available caches
  * `proxy_url`: an upstream proxy to fetch posts and/or media with, (default: `nil`)
  * `send_user_agent`: should we include a user agent with HTTP requests? (default: `true`)
  * `user_agent`: what user agent should we use? (default: `:default`), must be string or `:default`
- * `adapter`: array of hackney options
+ * `adapter`: array of adapter options
  
  ### :hackney_pools
  
@@@ -394,6 -388,42 +393,42 @@@ For each pool, the options are
  * `timeout` - retention duration for connections
  
  
+ ### :connections_pool
+ *For `gun` adapter*
+ Advanced settings for connections pool. Pool with opened connections. These connections can be reused in worker pools.
+ For big instances it's recommended to increase `config :pleroma, :connections_pool, max_connections: 500` up to 500-1000.
+ It will increase memory usage, but federation would work faster.
+ * `:checkin_timeout` - timeout to checkin connection from pool. Default: 250ms.
+ * `:max_connections` - maximum number of connections in the pool. Default: 250 connections.
+ * `:retry` - number of retries, while `gun` will try to reconnect if connection goes down. Default: 1.
+ * `:retry_timeout` - time between retries when `gun` will try to reconnect in milliseconds. Default: 1000ms.
+ * `:await_up_timeout` - timeout while `gun` will wait until connection is up. Default: 5000ms.
+ ### :pools
+ *For `gun` adapter*
+ Advanced settings for workers pools.
+ There are four pools used:
+ * `:federation` for the federation jobs.
+   You may want this pool max_connections to be at least equal to the number of federator jobs + retry queue jobs.
+ * `:media` for rich media, media proxy
+ * `:upload` for uploaded media (if using a remote uploader and `proxy_remote: true`)
+ * `:default` for other requests
+ For each pool, the options are:
+ * `:size` - how much workers the pool can hold
+ * `:timeout` - timeout while `gun` will wait for response
+ * `:max_overflow` - additional workers if pool is under load
  ## Captcha
  
  ### Pleroma.Captcha
index 7d8bb1270b52d95fd7b8bc0d8c756dabc9e4cdc9,19286fd01a29f9f75dfb8ed78e9a2d94694368d3..710aa70202ea5bc3da39cbf246d5f8fd55363960
@@@ -5,7 -5,6 +5,7 @@@
  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
  
    def increase_poll_votes_if_vote(_create_data), do: :noop
  
+   @spec persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()}
+   def persist(object, meta) do
+     with local <- Keyword.fetch!(meta, :local),
+          {recipients, _, _} <- get_recipients(object),
+          {:ok, activity} <-
+            Repo.insert(%Activity{
+              data: object,
+              local: local,
+              recipients: recipients,
+              actor: object["actor"]
+            }) do
+       {:ok, activity, meta}
+     end
+   end
    @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),
           {: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),
    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 <- maybe_federate(activity) do
        {:ok, activity}
      else
        {:error, error} -> Repo.rollback(error)
      end
    end
  
+   defp do_delete(%Object{data: %{"type" => "Tombstone", "id" => ap_id}}, _) do
+     activity =
+       ap_id
+       |> Activity.Queries.by_object_id()
+       |> Activity.Queries.by_type("Delete")
+       |> Repo.one()
+     {:ok, activity}
+   end
    @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
  
    defp fetch_activities_query_ap_ids_ops(opts) do
      source_user = opts["muting_user"]
-     ap_id_relations = if source_user, do: [:mute, :reblog_mute], else: []
+     ap_id_relationships = if source_user, do: [:mute, :reblog_mute], else: []
  
-     ap_id_relations =
-       ap_id_relations ++
+     ap_id_relationships =
+       ap_id_relationships ++
          if opts["blocking_user"] && opts["blocking_user"] == source_user do
            [:block]
          else
            []
          end
  
-     preloaded_ap_ids = User.outgoing_relations_ap_ids(source_user, ap_id_relations)
+     preloaded_ap_ids = User.outgoing_relationships_ap_ids(source_user, ap_id_relationships)
  
      restrict_blocked_opts = Map.merge(%{"blocked_users_ap_ids" => preloaded_ap_ids[:block]}, opts)
      restrict_muted_opts = Map.merge(%{"muted_users_ap_ids" => preloaded_ap_ids[:mute]}, opts)
      end
    end
  
+   @spec get_actor_url(any()) :: binary() | nil
+   defp get_actor_url(url) when is_binary(url), do: url
+   defp get_actor_url(%{"href" => href}) when is_binary(href), do: href
+   defp get_actor_url(url) when is_list(url) do
+     url
+     |> List.first()
+     |> get_actor_url()
+   end
+   defp get_actor_url(_url), do: nil
    defp object_to_user_data(data) do
      avatar =
        data["icon"]["url"] &&
  
      user_data = %{
        ap_id: data["id"],
+       uri: get_actor_url(data["url"]),
        ap_enabled: true,
        source_data: data,
        banner: banner,
index 6b41c387c72a1f8123b51fee46f5c6145c618d6a,636cf3301e14c5f8a7e5a3f06d2da83f2c9dea57..51447f897434d216098925dcdf6e90a1e91bb33d
@@@ -12,6 -12,8 +12,8 @@@ defmodule Pleroma.Web.CommonAPI d
    alias Pleroma.User
    alias Pleroma.UserRelationship
    alias Pleroma.Web.ActivityPub.ActivityPub
+   alias Pleroma.Web.ActivityPub.Builder
+   alias Pleroma.Web.ActivityPub.Pipeline
    alias Pleroma.Web.ActivityPub.Utils
    alias Pleroma.Web.ActivityPub.Visibility
  
@@@ -19,6 -21,7 +21,7 @@@
    import Pleroma.Web.CommonAPI.Utils
  
    require Pleroma.Constants
+   require Logger
  
    def follow(follower, followed) do
      timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
      end
    end
  
-   def favorite(id_or_ap_id, user) do
-     with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)},
-          object <- Object.normalize(activity),
-          like_activity <- Utils.get_existing_like(user.ap_id, object) do
-       if like_activity do
-         {:ok, like_activity, object}
-       else
-         ActivityPub.like(user, object)
-       end
+   @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
+   def favorite(%User{} = user, id) do
+     case favorite_helper(user, id) do
+       {:ok, _} = res ->
+         res
+       {:error, :not_found} = res ->
+         res
+       {:error, e} ->
+         Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
+         {:error, dgettext("errors", "Could not favorite")}
+     end
+   end
+   def favorite_helper(user, id) do
+     with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
+          {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
+          {_, {:ok, %Activity{} = activity, _meta}} <-
+            {:common_pipeline,
+             Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
+       {:ok, activity}
      else
-       {:find_activity, _} -> {:error, :not_found}
-       _ -> {:error, dgettext("errors", "Could not favorite")}
+       {:find_object, _} ->
+         {:error, :not_found}
+       {:common_pipeline,
+        {
+          :error,
+          {
+            :validate_object,
+            {
+              :error,
+              changeset
+            }
+          }
+        }} = e ->
+         if {:object, {"already liked by this actor", []}} in changeset.errors do
+           {:ok, :already_liked}
+         else
+           {:error, e}
+         end
+       e ->
+         {:error, e}
      end
    end
  
  
    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)
    def thread_muted?(%{id: nil} = _user, _activity), do: false
  
    def thread_muted?(user, activity) do
-     ThreadMute.check_muted(user.id, activity.data["context"]) != []
+     ThreadMute.exists?(user.id, activity.data["context"])
    end
  
    def report(user, %{"account_id" => account_id} = data) do
index 31d441d38c20482d33eda4950f23fc51728da8e1,17e7b97deffcbfe93c7eeeabd7550e4dea6c1e2b..d7226f5a361c7272f04ba7a53b3d3d2e53122443
@@@ -1425,6 -1425,12 +1425,12 @@@ defmodule Pleroma.Web.ActivityPub.Activ
        assert Repo.get(Object, object.id).data["type"] == "Tombstone"
      end
  
+     test "it doesn't fail when an activity was already deleted" do
+       {:ok, delete} = insert(:note_activity) |> Object.normalize() |> ActivityPub.delete()
+       assert {:ok, ^delete} = delete |> Object.normalize() |> ActivityPub.delete()
+     end
      test "decrements user note count only for public activities" do
        user = insert(:user, note_count: 10)
  
        {:ok, a4} = CommonAPI.post(user2, %{"status" => "Agent Smith "})
        {:ok, a5} = CommonAPI.post(user1, %{"status" => "Red or Blue "})
  
-       {:ok, _, _} = CommonAPI.favorite(a4.id, user)
-       {:ok, _, _} = CommonAPI.favorite(a3.id, other_user)
-       {:ok, _, _} = CommonAPI.favorite(a3.id, user)
-       {:ok, _, _} = CommonAPI.favorite(a5.id, other_user)
-       {:ok, _, _} = CommonAPI.favorite(a5.id, user)
-       {:ok, _, _} = CommonAPI.favorite(a4.id, other_user)
-       {:ok, _, _} = CommonAPI.favorite(a1.id, user)
-       {:ok, _, _} = CommonAPI.favorite(a1.id, other_user)
+       {:ok, _} = CommonAPI.favorite(user, a4.id)
+       {:ok, _} = CommonAPI.favorite(other_user, a3.id)
+       {:ok, _} = CommonAPI.favorite(user, a3.id)
+       {:ok, _} = CommonAPI.favorite(other_user, a5.id)
+       {:ok, _} = CommonAPI.favorite(user, a5.id)
+       {:ok, _} = CommonAPI.favorite(other_user, a4.id)
+       {:ok, _} = CommonAPI.favorite(user, a1.id)
+       {:ok, _} = CommonAPI.favorite(other_user, a1.id)
        result = ActivityPub.fetch_favourites(user)
  
        assert Enum.map(result, & &1.id) == [a1.id, a5.id, a3.id, a4.id]
                 ActivityPub.move(old_user, new_user)
      end
    end
 +
 +  describe "global activity expiration" do
 +    setup 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