Fix merge conflicts with upstream
authorSean King <seanking2919@protonmail.com>
Fri, 4 Jun 2021 20:42:44 +0000 (14:42 -0600)
committerSean King <seanking2919@protonmail.com>
Fri, 4 Jun 2021 20:42:44 +0000 (14:42 -0600)
1  2 
CHANGELOG.md
config/config.exs
config/description.exs
docs/configuration/cheatsheet.md
lib/pleroma/user.ex
lib/pleroma/web/o_auth/o_auth_controller.ex
lib/pleroma/web/router.ex
test/pleroma/user_test.exs
test/pleroma/web/plugs/frontend_static_plug_test.exs

diff --combined CHANGELOG.md
index 4f590dbd3600fbfbad51fc9a3199075528c00e8f,2d1ff5b7bc2b1a76fa1b3f6ae16558b0b794cb75..72a67eb8df8b8817d69da4352dd611b3ff6e63ba
@@@ -6,17 -6,27 +6,31 @@@ The format is based on [Keep a Changelo
  
  ## Unreleased
  
 +### Removed
 +
 +- MastoFE
 +
  ### Changed
  
+ - **Breaking:** Configuration: `:chat, enabled` moved to `:shout, enabled` and `:instance, chat_limit` moved to `:shout, limit`
  - The `application` metadata returned with statuses is no longer hardcoded. Apps that want to display these details will now have valid data for new posts after this change.
+ - HTTPSecurityPlug now sends a response header to opt out of Google's FLoC (Federated Learning of Cohorts) targeted advertising.
+ - Email address is now returned if requesting user is the owner of the user account so it can be exposed in client and FE user settings UIs.
  
  ### Added
  
  - MRF (`FollowBotPolicy`): New MRF Policy which makes a designated local Bot account attempt to follow all users in public Notes received by your instance. Users who require approving follower requests or have #nobot in their profile are excluded.
+ - Return OAuth token `id` (primary key) in POST `/oauth/token`.
+ - `AnalyzeMetadata` upload filter for extracting attachment dimensions and generating blurhashes.
+ - Attachment dimensions and blurhashes are federated when available.
+ - Pinned posts federation
+ ### Fixed
+ - Don't crash so hard when email settings are invalid.
+ - Checking activated Upload Filters for required commands.
+ ### Removed
+ - **Breaking**: Remove deprecated `/api/qvitter/statuses/notifications/read` (replaced by `/api/v1/pleroma/notifications/read`)
  
  ## Unreleased (Patch)
  
@@@ -26,6 -36,9 +40,9 @@@
  - Uploading custom instance thumbnail via AdminAPI/AdminFE generated invalid URL to the image
  - Applying ConcurrentLimiter settings via AdminAPI
  - User login failures if their `notification_settings` were in a NULL state.
+ - Mix task `pleroma.user delete_activities` query transaction timeout is now :infinity
+ - MRF (`SimplePolicy`): Embedded objects are now checked. If any embedded object would be rejected, its parent is rejected. This fixes Announces leaking posts from blocked domains.
+ - Fixed some Markdown issues, including trailing slash in links.
  
  ## [2.3.0] - 2020-03-01
  
diff --combined config/config.exs
index 07565c557b96803ed19434bdb61234e6b84f6d22,2f8a18788cfc0329a01b49951bb304380a9982bb..c00d5dca9d78d5d3a15d6a4e34a444d43ea0b5a4
@@@ -41,7 -41,7 +41,7 @@@
  #
  # This configuration file is loaded before any dependency and
  # is restricted to this project.
use Mix.Config
import Config
  
  # General application configuration
  config :pleroma, ecto_repos: [Pleroma.Repo]
@@@ -190,7 -190,6 +190,6 @@@ config :pleroma, :instance
    instance_thumbnail: "/instance/thumbnail.jpeg",
    limit: 5_000,
    description_limit: 5_000,
-   chat_limit: 5_000,
    remote_limit: 100_000,
    upload_limit: 16_000_000,
    avatar_upload_limit: 2_000_000,
@@@ -322,6 -321,9 +321,6 @@@ config :pleroma, :frontend_configuratio
      subjectLineBehavior: "email",
      theme: "pleroma-dark",
      webPushNotifications: false
 -  },
 -  masto_fe: %{
 -    showInstanceSpecificPanel: true
    }
  
  config :pleroma, :assets,
@@@ -454,7 -456,9 +453,9 @@@ config :pleroma, :media_preview_proxy
    image_quality: 85,
    min_content_length: 100 * 1024
  
- config :pleroma, :chat, enabled: true
+ config :pleroma, :shout,
+   enabled: true,
+   limit: 5_000
  
  config :phoenix, :format_encoders, json: Jason
  
diff --combined config/description.exs
index bc3f11dded1cbea02880d8624fa83705296386d2,934a62a6293d77479affb6649320045d13ab8e24..94907aaba35b71bcb994b6df995fb3d3a744ca77
@@@ -1,4 -1,4 +1,4 @@@
use Mix.Config
import Config
  
  websocket_config = [
    path: "/websocket",
@@@ -544,14 -544,6 +544,6 @@@ config :pleroma, :config_description, 
            5_000
          ]
        },
-       %{
-         key: :chat_limit,
-         type: :integer,
-         description: "Character limit of the instance chat messages",
-         suggestions: [
-           5_000
-         ]
-       },
        %{
          key: :remote_limit,
          type: :integer,
        %{
          key: :allow_relay,
          type: :boolean,
-         description: "Enable Pleroma's Relay, which makes it possible to follow a whole instance"
+         description:
+           "Permits remote instances to subscribe to all public posts of your instance. (Important!) This may increase the visibility of your instance."
        },
        %{
          key: :public,
      type: :group,
      description:
        "This form can be used to configure a keyword list that keeps the configuration data for any " <>
 -        "kind of frontend. By default, settings for pleroma_fe and masto_fe are configured. If you want to " <>
 +        "kind of frontend. By default, settings for pleroma_fe are configured. If you want to " <>
          "add your own configuration your settings all fields must be complete.",
      children: [
        %{
              alwaysShowSubjectInput: true,
              background: "/static/aurora_borealis.jpg",
              collapseMessageWithSubject: false,
-             disableChat: false,
              greentext: false,
              hideFilteredStatuses: false,
              hideMutedPosts: false,
              description:
                "When a message has a subject (aka Content Warning), collapse it by default"
            },
-           %{
-             key: :disableChat,
-             label: "PleromaFE Chat",
-             type: :boolean,
-             description: "Disables PleromaFE Chat component"
-           },
            %{
              key: :greentext,
              label: "Greentext",
              suggestions: ["pleroma-dark"]
            }
          ]
 -      },
 -      %{
 -        key: :masto_fe,
 -        label: "Masto FE",
 -        type: :map,
 -        description: "Settings for Masto FE",
 -        suggestions: [
 -          %{
 -            showInstanceSpecificPanel: true
 -          }
 -        ],
 -        children: [
 -          %{
 -            key: :showInstanceSpecificPanel,
 -            label: "Show instance specific panel",
 -            type: :boolean,
 -            description: "Whenether to show the instance's specific panel"
 -          }
 -        ]
        }
      ]
    },
    },
    %{
      group: :pleroma,
-     key: :chat,
+     key: :shout,
      type: :group,
-     description: "Pleroma chat settings",
+     description: "Pleroma shout settings",
      children: [
        %{
          key: :enabled,
-         type: :boolean
+         type: :boolean,
+         description: "Enables the backend Shoutbox chat feature."
+       },
+       %{
+         key: :limit,
+         type: :integer,
+         description: "Shout message character limit.",
+         suggestions: [
+           5_000
+         ]
        }
      ]
    },
index 5c3313a2cfec7dbeaf7db62e27156243bc440b33,5b49185dc2c056dfc3cc4353f03f8fef893b079c..59061aaa66a8413c58786fe9debda8a8104eb865
@@@ -8,9 -8,10 +8,10 @@@ For from source installations Pleroma c
  
  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.
  
- ## :chat
+ ## :shout
  
- * `enabled` - Enables the backend chat. Defaults to `true`.
+ * `enabled` - Enables the backend Shoutbox chat feature. Defaults to `true`.
+ * `limit` - Shout character limit. Defaults to `5_000`
  
  ## :instance
  * `name`: The instance’s name.
@@@ -19,7 -20,6 +20,6 @@@
  * `description`: The instance’s description, can be seen in nodeinfo and ``/api/v1/instance``.
  * `limit`: Posts character limit (CW/Subject included in the counter).
  * `description_limit`: The character limit for image descriptions.
- * `chat_limit`: Character limit of the instance chat messages.
  * `remote_limit`: Hard character limit beyond which remote posts will be dropped.
  * `upload_limit`: File size limit of uploads (except for avatar, background, banner).
  * `avatar_upload_limit`: File size limit of user’s profile avatars.
@@@ -37,7 -37,7 +37,7 @@@
  * `federating`: Enable federation with other instances.
  * `federation_incoming_replies_max_depth`: Max. depth of reply-to 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.
  * `federation_reachability_timeout_days`: Timeout (in days) of each external federation target being unreachable prior to pausing federating to it.
- * `allow_relay`: Enable Pleroma’s Relay, which makes it possible to follow a whole instance.
+ * `allow_relay`: Permits remote instances to subscribe to all public posts of your instance. This may increase the visibility of your instance.
  * `public`: Makes the client API in authenticated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network. Note that there is a dependent setting restricting or allowing unauthenticated access to specific resources, see `restrict_unauthenticated` for more details.
  * `quarantined_instances`: List of ActivityPub instances where private (DMs, followers-only) activities will not be send.
  * `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML).
@@@ -246,7 -246,7 +246,7 @@@ Notes
  
  ### :frontend_configurations
  
 -This can be used to configure a keyword list that keeps the configuration data for any kind of frontend. By default, settings for `pleroma_fe` and `masto_fe` are configured. You can find the documentation for `pleroma_fe` configuration into [Pleroma-FE configuration and customization for instance administrators](/frontend/CONFIGURATION/#options).
 +This can be used to configure a keyword list that keeps the configuration data for any kind of frontend. By default, settings for `pleroma_fe` are configured. You can find the documentation for `pleroma_fe` configuration into [Pleroma-FE configuration and customization for instance administrators](/frontend/CONFIGURATION/#options).
  
  Frontends can access these settings at `/api/v1/pleroma/frontend_configurations`
  
@@@ -257,7 -257,10 +257,7 @@@ config :pleroma, :frontend_configuratio
    pleroma_fe: %{
      theme: "pleroma-dark",
      # ... see /priv/static/static/config.json for the available keys.
 -},
 -  masto_fe: %{
 -    showInstanceSpecificPanel: true
 -  }
 +}
  ```
  
  These settings **need to be complete**, they will override the defaults.
diff --combined lib/pleroma/user.ex
index a3d41fcd064fc41c55dadb101e265b4296aaec90,9365fae2b7a58ff98b013844420a6b797653a963..46bac283c79902a3b66d9f4790a26a2ae9e9fb81
@@@ -27,13 -27,13 +27,13 @@@ defmodule Pleroma.User d
    alias Pleroma.Repo
    alias Pleroma.User
    alias Pleroma.UserRelationship
-   alias Pleroma.Web
    alias Pleroma.Web.ActivityPub.ActivityPub
    alias Pleroma.Web.ActivityPub.Builder
    alias Pleroma.Web.ActivityPub.Pipeline
    alias Pleroma.Web.ActivityPub.Utils
    alias Pleroma.Web.CommonAPI
    alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
+   alias Pleroma.Web.Endpoint
    alias Pleroma.Web.OAuth
    alias Pleroma.Web.RelMe
    alias Pleroma.Workers.BackgroundWorker
@@@ -99,6 -99,7 +99,7 @@@
      field(:local, :boolean, default: true)
      field(:follower_address, :string)
      field(:following_address, :string)
+     field(:featured_address, :string)
      field(:search_rank, :float, virtual: true)
      field(:search_type, :integer, virtual: true)
      field(:tags, {:array, :string}, default: [])
      field(:is_moderator, :boolean, default: false)
      field(:is_admin, :boolean, default: false)
      field(:show_role, :boolean, default: true)
 -    field(:mastofe_settings, :map, default: nil)
      field(:uri, ObjectValidators.Uri, default: nil)
      field(:hide_followers_count, :boolean, default: false)
      field(:hide_follows_count, :boolean, default: false)
      field(:hide_followers, :boolean, default: false)
      field(:hide_follows, :boolean, default: false)
      field(:hide_favorites, :boolean, default: true)
-     field(:pinned_activities, {:array, :string}, default: [])
      field(:email_notifications, :map, default: %{"digest" => false})
      field(:mascot, :map, default: nil)
      field(:emoji, :map, default: %{})
      field(:accepts_chat_messages, :boolean, default: nil)
      field(:last_active_at, :naive_datetime)
      field(:disclose_client, :boolean, default: true)
+     field(:pinned_objects, :map, default: %{})
  
      embeds_one(
        :notification_settings,
  
        _ ->
          unless options[:no_default] do
-           Config.get([:assets, :default_user_avatar], "#{Web.base_url()}/images/avi.png")
+           Config.get([:assets, :default_user_avatar], "#{Endpoint.url()}/images/avi.png")
          end
      end
    end
    def banner_url(user, options \\ []) do
      case user.banner do
        %{"url" => [%{"href" => href} | _]} -> href
-       _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
+       _ -> !options[:no_default] && "#{Endpoint.url()}/images/banner.png"
      end
    end
  
    # Should probably be renamed or removed
-   def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
+   @spec ap_id(User.t()) :: String.t()
+   def ap_id(%User{nickname: nickname}), do: "#{Endpoint.url()}/users/#{nickname}"
  
+   @spec ap_followers(User.t()) :: String.t()
    def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
    def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
  
    def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
    def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
  
+   @spec ap_featured_collection(User.t()) :: String.t()
+   def ap_featured_collection(%User{featured_address: fa}) when is_binary(fa), do: fa
+   def ap_featured_collection(%User{} = user), do: "#{ap_id(user)}/collections/featured"
    defp truncate_fields_param(params) do
      if Map.has_key?(params, :fields) do
        Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
          :uri,
          :follower_address,
          :following_address,
+         :featured_address,
          :hide_followers,
          :hide_follows,
          :hide_followers_count,
          :invisible,
          :actor_type,
          :also_known_as,
-         :accepts_chat_messages
+         :accepts_chat_messages,
+         :pinned_objects
        ]
      )
      |> cast(params, [:name], empty_values: [])
      |> validate_format(:nickname, local_nickname_regex())
      |> put_ap_id()
      |> unique_constraint(:ap_id)
-     |> put_following_and_follower_address()
+     |> put_following_and_follower_and_featured_address()
    end
  
    def register_changeset(struct, params \\ %{}, opts \\ []) do
      |> put_password_hash
      |> put_ap_id()
      |> unique_constraint(:ap_id)
-     |> put_following_and_follower_address()
+     |> put_following_and_follower_and_featured_address()
    end
  
    def maybe_validate_required_email(changeset, true), do: changeset
      put_change(changeset, :ap_id, ap_id)
    end
  
-   defp put_following_and_follower_address(changeset) do
-     followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
+   defp put_following_and_follower_and_featured_address(changeset) do
+     user = %User{nickname: get_field(changeset, :nickname)}
+     followers = ap_followers(user)
+     following = ap_following(user)
+     featured = ap_featured_collection(user)
  
      changeset
      |> put_change(:follower_address, followers)
+     |> put_change(:following_address, following)
+     |> put_change(:featured_address, featured)
    end
  
    defp autofollow_users(user) do
        ap_enabled: false,
        is_moderator: false,
        is_admin: false,
 -      mastofe_settings: nil,
        mascot: nil,
        emoji: %{},
        pleroma_settings_store: %{},
      |> update_and_set_cache()
    end
  
 -  def mastodon_settings_update(user, settings) do
 -    user
 -    |> cast(%{mastofe_settings: settings}, [:mastofe_settings])
 -    |> validate_required([:mastofe_settings])
 -    |> update_and_set_cache()
 -  end
 -
    @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
    def confirmation_changeset(user, set_confirmation: confirmed?) do
      params =
      cast(user, %{is_approved: approved?}, [:is_approved])
    end
  
-   def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
-     if id not in user.pinned_activities do
-       max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0)
-       params = %{pinned_activities: user.pinned_activities ++ [id]}
-       # if pinned activity was scheduled for deletion, we remove job
-       if expiration = Pleroma.Workers.PurgeExpiredActivity.get_expiration(id) do
-         Oban.cancel_job(expiration.id)
-       end
+   @spec add_pinned_object_id(User.t(), String.t()) :: {:ok, User.t()} | {:error, term()}
+   def add_pinned_object_id(%User{} = user, object_id) do
+     if !user.pinned_objects[object_id] do
+       params = %{pinned_objects: Map.put(user.pinned_objects, object_id, NaiveDateTime.utc_now())}
  
        user
-       |> cast(params, [:pinned_activities])
-       |> validate_length(:pinned_activities,
-         max: max_pinned_statuses,
-         message: "You have already pinned the maximum number of statuses"
-       )
+       |> cast(params, [:pinned_objects])
+       |> validate_change(:pinned_objects, fn :pinned_objects, pinned_objects ->
+         max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0)
+         if Enum.count(pinned_objects) <= max_pinned_statuses do
+           []
+         else
+           [pinned_objects: "You have already pinned the maximum number of statuses"]
+         end
+       end)
      else
        change(user)
      end
      |> update_and_set_cache()
    end
  
-   def remove_pinnned_activity(user, %Pleroma.Activity{id: id, data: data}) do
-     params = %{pinned_activities: List.delete(user.pinned_activities, id)}
-     # if pinned activity was scheduled for deletion, we reschedule it for deletion
-     if data["expires_at"] do
-       # MRF.ActivityExpirationPolicy used UTC timestamps for expires_at in original implementation
-       {:ok, expires_at} =
-         data["expires_at"] |> Pleroma.EctoType.ActivityPub.ObjectValidators.DateTime.cast()
-       Pleroma.Workers.PurgeExpiredActivity.enqueue(%{
-         activity_id: id,
-         expires_at: expires_at
-       })
-     end
+   @spec remove_pinned_object_id(User.t(), String.t()) :: {:ok, t()} | {:error, term()}
+   def remove_pinned_object_id(%User{} = user, object_id) do
      user
-     |> cast(params, [:pinned_activities])
+     |> cast(
+       %{pinned_objects: Map.delete(user.pinned_objects, object_id)},
+       [:pinned_objects]
+     )
      |> update_and_set_cache()
    end
  
index ae040c6eb94b7767d0b6d6bba4ce33a5bcacda7e,42f4d768f0b3643d075a30844bcaca52168c167c..73d48549710fcf014e50cfc9008ef2738fa453fd
@@@ -427,7 -427,7 +427,7 @@@ defmodule Pleroma.Web.OAuth.OAuthContro
        |> Map.put("state", state)
  
      # Handing the request to Ueberauth
-     redirect(conn, to: o_auth_path(conn, :request, provider, params))
+     redirect(conn, to: Routes.o_auth_path(conn, :request, provider, params))
    end
  
    def request(%Plug.Conn{} = conn, params) do
      end
    end
  
++<<<<<<< HEAD
++=======
+   # Special case: Local MastodonFE
+   defp redirect_uri(%Plug.Conn{} = conn, "."), do: Routes.auth_url(conn, :login)
++>>>>>>> 0c56f9de0d607b88fd107e0bd13ef286f0629346
    defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri
  
    defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id)
index 936053ee2fcfc65ea9eccfed9ef098728e3f6a16,efca7078a178344c091e17ac61f748bee68e65ba..9695667b6428b354e86f279fcee528714b964b42
@@@ -100,6 -100,12 +100,6 @@@ defmodule Pleroma.Web.Router d
      plug(Pleroma.Web.Plugs.IdempotencyPlug)
    end
  
 -  pipeline :mastodon_html do
 -    plug(:browser)
 -    plug(:authenticate)
 -    plug(:after_auth)
 -  end
 -
    pipeline :pleroma_html do
      plug(:browser)
      plug(:authenticate)
      plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug)
    end
  
+   pipeline :static_fe do
+     plug(Pleroma.Web.Plugs.StaticFEPlug)
+   end
    scope "/api/v1/pleroma", Pleroma.Web.TwitterAPI do
      pipe_through(:pleroma_api)
  
      get("/timelines/list/:list_id", TimelineController, :list)
    end
  
 -  scope "/api/web", Pleroma.Web do
 -    pipe_through(:authenticated_api)
 -
 -    # Backend-obscure settings blob for MastoFE, don't parse/reuse elsewhere
 -    put("/settings", MastoFEController, :put_settings)
 -  end
 -
    scope "/api/v1", Pleroma.Web.MastodonAPI do
      pipe_through(:app_api)
  
  
      get("/oauth_tokens", TwitterAPI.Controller, :oauth_tokens)
      delete("/oauth_tokens/:id", TwitterAPI.Controller, :revoke_token)
-     post(
-       "/qvitter/statuses/notifications/read",
-       TwitterAPI.Controller,
-       :mark_notifications_as_read
-     )
    end
  
    scope "/", Pleroma.Web do
      # Note: html format is supported only if static FE is enabled
      # Note: http signature is only considered for json requests (no auth for non-json requests)
-     pipe_through([:accepts_html_json, :http_signature, Pleroma.Web.Plugs.StaticFEPlug])
+     pipe_through([:accepts_html_json, :http_signature, :static_fe])
  
      get("/objects/:uuid", OStatus.OStatusController, :object)
      get("/activities/:uuid", OStatus.OStatusController, :activity)
    scope "/", Pleroma.Web do
      # Note: html format is supported only if static FE is enabled
      # Note: http signature is only considered for json requests (no auth for non-json requests)
-     pipe_through([:accepts_html_xml_json, :http_signature, Pleroma.Web.Plugs.StaticFEPlug])
+     pipe_through([:accepts_html_xml_json, :http_signature, :static_fe])
  
      # Note: returns user _profile_ for json requests, redirects to user _feed_ for non-json ones
      get("/users/:nickname", Feed.UserController, :feed_redirect, as: :user_feed)
  
    scope "/", Pleroma.Web do
      # Note: html format is supported only if static FE is enabled
-     pipe_through([:accepts_html_xml, Pleroma.Web.Plugs.StaticFEPlug])
+     pipe_through([:accepts_html_xml, :static_fe])
  
      get("/users/:nickname/feed", Feed.UserController, :feed, as: :user_feed)
    end
      # The following two are S2S as well, see `ActivityPub.fetch_follow_information_for_user/1`:
      get("/users/:nickname/followers", ActivityPubController, :followers)
      get("/users/:nickname/following", ActivityPubController, :following)
+     get("/users/:nickname/collections/featured", ActivityPubController, :pinned)
    end
  
    scope "/", Pleroma.Web.ActivityPub do
      get("/:version", Nodeinfo.NodeinfoController, :nodeinfo)
    end
  
-   scope "/proxy/", Pleroma.Web.MediaProxy do
-     get("/preview/:sig/:url", MediaProxyController, :preview)
-     get("/preview/:sig/:url/:filename", MediaProxyController, :preview)
-     get("/:sig/:url", MediaProxyController, :remote)
-     get("/:sig/:url/:filename", MediaProxyController, :remote)
 -  scope "/", Pleroma.Web do
 -    pipe_through(:api)
 -
 -    get("/web/manifest.json", MastoFEController, :manifest)
 -  end
 -
 -  scope "/", Pleroma.Web do
 -    pipe_through(:mastodon_html)
 -
 -    get("/web/login", MastodonAPI.AuthController, :login)
 -    delete("/auth/sign_out", MastodonAPI.AuthController, :logout)
 -
 -    post("/auth/password", MastodonAPI.AuthController, :password_reset)
 -
 -    get("/web/*path", MastoFEController, :index)
 -
 -    get("/embed/:id", EmbedController, :show)
 -  end
 -
+   scope "/proxy/", Pleroma.Web do
+     get("/preview/:sig/:url", MediaProxy.MediaProxyController, :preview)
+     get("/preview/:sig/:url/:filename", MediaProxy.MediaProxyController, :preview)
+     get("/:sig/:url", MediaProxy.MediaProxyController, :remote)
+     get("/:sig/:url/:filename", MediaProxy.MediaProxyController, :remote)
    end
  
    if Pleroma.Config.get(:env) == :dev do
  
      options("/*path", RedirectController, :empty)
    end
+   # TODO: Change to Phoenix.Router.routes/1 for Phoenix 1.6.0+
+   def get_api_routes do
+     __MODULE__.__routes__()
+     |> Enum.reject(fn r -> r.plug == Pleroma.Web.Fallback.RedirectController end)
+     |> Enum.map(fn r ->
+       r.path
+       |> String.split("/", trim: true)
+       |> List.first()
+     end)
+     |> Enum.uniq()
+   end
  end
index faaf86fcb88db6d78026367f4b29e2f8a23e6fb7,abc471d13ffddf1faba6359d6133b5fb1b026110..f8777f0f751057ed3295afd0c1f8b8711e7a642c
@@@ -151,7 -151,7 +151,7 @@@ defmodule Pleroma.UserTest d
    test "ap_id returns the activity pub id for the user" do
      user = UserBuilder.build()
  
-     expected_ap_id = "#{Pleroma.Web.base_url()}/users/#{user.nickname}"
+     expected_ap_id = "#{Pleroma.Web.Endpoint.url()}/users/#{user.nickname}"
  
      assert expected_ap_id == User.ap_id(user)
    end
        )
      end
  
+     test "it fails gracefully with invalid email config" do
+       cng = User.register_changeset(%User{}, @full_user_data)
+       # Disable the mailer but enable all the things that want to send emails
+       clear_config([Pleroma.Emails.Mailer, :enabled], false)
+       clear_config([:instance, :account_activation_required], true)
+       clear_config([:instance, :account_approval_required], true)
+       clear_config([:welcome, :email, :enabled], true)
+       clear_config([:welcome, :email, :sender], "lain@lain.com")
+       # The user is still created
+       assert {:ok, %User{nickname: "nick"}} = User.register(cng)
+       # No emails are sent
+       ObanHelpers.perform_all()
+       refute_email_sent()
+     end
      test "it requires an email, name, nickname and password, bio is optional when account_activation_required is enabled" do
        clear_config([:instance, :account_activation_required], true)
  
          ap_enabled: true,
          is_moderator: true,
          is_admin: true,
 -        mastofe_settings: %{"a" => "b"},
          mascot: %{"a" => "b"},
          emoji: %{"a" => "b"},
          pleroma_settings_store: %{"q" => "x"},
               ap_enabled: false,
               is_moderator: false,
               is_admin: false,
 -             mastofe_settings: nil,
               mascot: nil,
               emoji: %{},
               pleroma_settings_store: %{},
      assert User.active_user_count(6) == 3
      assert User.active_user_count(1) == 1
    end
+   describe "pins" do
+     setup do
+       user = insert(:user)
+       [user: user, object_id: object_id_from_created_activity(user)]
+     end
+     test "unique pins", %{user: user, object_id: object_id} do
+       assert {:ok, %{pinned_objects: %{^object_id => pinned_at1} = pins} = updated_user} =
+                User.add_pinned_object_id(user, object_id)
+       assert Enum.count(pins) == 1
+       assert {:ok, %{pinned_objects: %{^object_id => pinned_at2} = pins}} =
+                User.add_pinned_object_id(updated_user, object_id)
+       assert pinned_at1 == pinned_at2
+       assert Enum.count(pins) == 1
+     end
+     test "respects max_pinned_statuses limit", %{user: user, object_id: object_id} do
+       clear_config([:instance, :max_pinned_statuses], 1)
+       {:ok, updated} = User.add_pinned_object_id(user, object_id)
+       object_id2 = object_id_from_created_activity(user)
+       {:error, %{errors: errors}} = User.add_pinned_object_id(updated, object_id2)
+       assert Keyword.has_key?(errors, :pinned_objects)
+     end
+     test "remove_pinned_object_id/2", %{user: user, object_id: object_id} do
+       assert {:ok, updated} = User.add_pinned_object_id(user, object_id)
+       {:ok, after_remove} = User.remove_pinned_object_id(updated, object_id)
+       assert after_remove.pinned_objects == %{}
+     end
+   end
+   defp object_id_from_created_activity(user) do
+     %{id: id} = insert(:note_activity, user: user)
+     %{object: %{data: %{"id" => object_id}}} = Activity.get_by_id_with_object(id)
+     object_id
+   end
  end
index 9b6821ff9fa6de971effc6b4b8877f2f2085883e,4152cdefe90ebd572254a5954dc98aeff768f33e..a9342e6f02306906a19ce36fa1365aea9a9fd408
@@@ -94,12 -94,15 +94,12 @@@ defmodule Pleroma.Web.Plugs.FrontendSta
        "internal",
        ".well-known",
        "nodeinfo",
 -      "web",
 -      "auth",
 -      "embed",
        "proxy",
        "test",
        "user_exists",
        "check_password"
      ]
  
-     assert expected_routes == Pleroma.Web.get_api_routes()
+     assert expected_routes == Pleroma.Web.Router.get_api_routes()
    end
  end