Merge branch 'develop' into stable
authorFloatingGhost <hannah@coffee-and-dreams.uk>
Fri, 14 Apr 2023 17:09:32 +0000 (18:09 +0100)
committerFloatingGhost <hannah@coffee-and-dreams.uk>
Fri, 14 Apr 2023 17:09:32 +0000 (18:09 +0100)
51 files changed:
CHANGELOG.md
Dockerfile
README.md
config/config.exs
config/custom_emoji.txt
config/description.exs
config/test.exs
docs/docs/configuration/cheatsheet.md
docs/docs/installation/generic_dependencies.include
docs/docs/installation/yunohost_en.md [new file with mode: 0644]
elixir_buildpack.config
lib/mix/tasks/pleroma/diagnostics.ex
lib/pleroma/activity.ex
lib/pleroma/instances/instance.ex
lib/pleroma/upload.ex
lib/pleroma/user.ex
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/activity_pub/activity_pub_controller.ex
lib/pleroma/web/activity_pub/publisher.ex
lib/pleroma/web/akkoma_api/controllers/frontend_settings_controller.ex
lib/pleroma/web/akkoma_api/controllers/frontend_switcher.ex [new file with mode: 0644]
lib/pleroma/web/akkoma_api/views/frontend_switcher.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/operations/frontend_settings_operation.ex
lib/pleroma/web/api_spec/operations/timeline_operation.ex
lib/pleroma/web/fallback/redirect_controller.ex
lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
lib/pleroma/web/mastodon_api/views/instance_view.ex
lib/pleroma/web/mastodon_api/views/status_view.ex
lib/pleroma/web/nodeinfo/nodeinfo.ex
lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
lib/pleroma/web/o_status/o_status_controller.ex
lib/pleroma/web/plugs/frontend_static.ex
lib/pleroma/web/plugs/http_security_plug.ex
lib/pleroma/web/plugs/instance_static.ex
lib/pleroma/web/router.ex
lib/pleroma/web/templates/akkoma_api/frontend_switcher/switch.html.eex [new file with mode: 0644]
mix.exs
mix.lock
priv/static/emoji/hehe.png [new file with mode: 0644]
priv/static/emoji/nothehe.png [new file with mode: 0644]
test/pleroma/upload_test.exs
test/pleroma/user_test.exs
test/pleroma/web/activity_pub/activity_pub_controller_test.exs
test/pleroma/web/activity_pub/activity_pub_test.exs
test/pleroma/web/mastodon_api/controllers/media_controller_test.exs
test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs
test/pleroma/web/mastodon_api/update_credentials_test.exs
test/pleroma/web/mastodon_api/views/account_view_test.exs
test/pleroma/web/node_info_test.exs
test/pleroma/web/plugs/frontend_static_plug_test.exs
test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs

index c12deb72bae8a0db88d9c32b206a38d765e44ee6..07a4496b4583330c0d58f630991e37a9c24cdd77 100644 (file)
@@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file.
 
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
+## 2023.04
+
+## Added
+- Nodeinfo keys for unauthenticated timeline visibility
+- Option to disable federated timeline
+- Option to make the bubble timeline publicly accessible
+- Ability to swap between installed standard frontends
+  - *mastodon frontends are still not counted as standard frontends due to the complexity in serving them correctly*. 
+
+### Upgrade Notes
+- Elixir 1.14 is now required. If your distribution does not package this, you can
+  use [asdf](https://asdf-vm.com/). At time of writing, elixir 1.14.3 / erlang 25.3
+  is confirmed to work.
+
 ## 2023.03
 
 ## Fixed
@@ -19,6 +33,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
 ### Removed
 - Possibility of using the `style` parameter on `span` elements. This will break certain MFM parameters.
+- Option for "default" image description.
 
 ## 2023.02
 
index 0551a4c9ea160d17da5cbf7e19bab3aed75d2da3..c6506c48c5aa2920ddc6a9b1569aa2ea31626fb8 100644 (file)
@@ -1,4 +1,4 @@
-FROM hexpm/elixir:1.13.4-erlang-24.3.4.5-alpine-3.15.6
+FROM hexpm/elixir:1.14.3-erlang-25.3-alpine-3.17.2
 
 ENV MIX_ENV=prod
 ENV ERL_EPMD_ADDRESS=127.0.0.1
index 8d35212aa898c25f76baaf9d6c945f4f4e5bb1c1..e4aa25715ee23070d1dfe25ebd5c5a8406b18680 100644 (file)
--- a/README.md
+++ b/README.md
@@ -54,6 +54,9 @@ If your platform is not supported, or you just want to be able to edit the sourc
 ### Docker
 Docker installation is supported via [this setup](https://docs.akkoma.dev/stable/installation/docker_en/)
 
+### Packages
+Akkoma is packaged for [YunoHost](https://yunohost.org) and can be found and installed from the [YunoHost app catalogue](https://yunohost.org/#/apps).
+
 ### Compilation Troubleshooting
 If you ever encounter compilation issues during the updating of Akkoma, you can try these commands and see if they fix things:
 
index 5eaa8ce760dc1ef696ea7fac0e3aa3f11e6c09ad..95c576385044e78bedc679d25edfad40a5771767 100644 (file)
@@ -65,7 +65,6 @@ config :pleroma, Pleroma.Upload,
   link_name: false,
   proxy_remote: false,
   filename_display_max_length: 30,
-  default_description: nil,
   base_url: nil
 
 config :pleroma, Pleroma.Uploaders.Local, uploads: "uploads"
@@ -261,7 +260,8 @@ config :pleroma, :instance,
   privileged_staff: false,
   local_bubble: [],
   max_frontend_settings_json_chars: 100_000,
-  export_prometheus_metrics: true
+  export_prometheus_metrics: true,
+  federated_timeline_available: true
 
 config :pleroma, :welcome,
   direct_message: [
@@ -745,6 +745,9 @@ config :pleroma, :frontends,
   primary: %{"name" => "pleroma-fe", "ref" => "stable"},
   admin: %{"name" => "admin-fe", "ref" => "stable"},
   mastodon: %{"name" => "mastodon-fe", "ref" => "akkoma"},
+  pickable: [
+    "pleroma-fe/stable"
+  ],
   swagger: %{
     "name" => "swagger-ui",
     "ref" => "stable",
@@ -810,7 +813,7 @@ config :pleroma, :majic_pool, size: 2
 private_instance? = :if_instance_is_private
 
 config :pleroma, :restrict_unauthenticated,
-  timelines: %{local: private_instance?, federated: private_instance?},
+  timelines: %{local: private_instance?, federated: private_instance?, bubble: true},
   profiles: %{local: private_instance?, remote: private_instance?},
   activities: %{local: private_instance?, remote: private_instance?}
 
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..7b2e51265f538ddfabf8ad9c9e90a1b4c3e9c354 100644 (file)
@@ -0,0 +1,2 @@
+hehe, /emoji/hehe.png, Akkoma
+nothehe, /emoji/nothehe.png, Akkoma
index 2a2d70a7b4a327a26fc92ac970db8d7cd9cf2c56..bd20cb2392b9dc1f4727f4f910cbcd7a2a648927 100644 (file)
@@ -969,6 +969,12 @@ config :pleroma, :config_description, [
         key: :export_prometheus_metrics,
         type: :boolean,
         description: "Enable prometheus metrics (at /api/v1/akkoma/metrics)"
+      },
+      %{
+        key: :federated_timeline_available,
+        type: :boolean,
+        description:
+          "Let people view the 'firehose' feed of all public statuses from all instances."
       }
     ]
   },
@@ -2993,6 +2999,11 @@ config :pleroma, :config_description, [
             key: :federated,
             type: :boolean,
             description: "Disallow viewing the whole known network timeline."
+          },
+          %{
+            key: :bubble,
+            type: :boolean,
+            description: "Disallow viewing the bubble timeline."
           }
         ]
       },
@@ -3148,6 +3159,12 @@ config :pleroma, :config_description, [
         description:
           "A map containing available frontends and parameters for their installation.",
         children: frontend_options
+      },
+      %{
+        key: :pickable,
+        type: {:list, :string},
+        description:
+          "A list containing all frontends users can pick as their preference, format is :name/:ref, e.g pleroma-fe/stable."
       }
     ]
   },
index 3056dbd0319f26f56d16755907cc6b63c714dad5..4448eeb73600c61618b0d56bf68a44b645823da2 100644 (file)
@@ -23,8 +23,7 @@ config :pleroma, :auth, oauth_consumer_strategies: []
 
 config :pleroma, Pleroma.Upload,
   filters: [],
-  link_name: false,
-  default_description: :filename
+  link_name: false
 
 config :pleroma, Pleroma.Uploaders.Local, uploads: "test/uploads"
 
index 4e84b9a44a20c4cd81ca9b60b1830779500f0c61..1c4d9ec5dd527884635841a0745027f9b6ba7e1f 100644 (file)
@@ -562,7 +562,6 @@ the source code is here: [kocaptcha](https://github.com/koto-bank/kocaptcha). Th
 * `proxy_remote`: If you're using a remote uploader, Akkoma will proxy media requests instead of redirecting to it.
 * `proxy_opts`: Proxy options, see `Pleroma.ReverseProxy` documentation.
 * `filename_display_max_length`: Set max length of a filename to display. 0 = no limit. Default: 30.
-* `default_description`: Sets which default description an image has if none is set explicitly. Options: nil (default) - Don't set a default, :filename - use the filename of the file, a string (e.g. "attachment") - Use this string
 
 !!! warning
     `strip_exif` has been replaced by `Pleroma.Upload.Filter.Mogrify`.
index 68c61129ad2392589cbf3f619cc6e07df152c3c8..d8cf9f9dafdd7476e0b17bd303f072279c8290da 100644 (file)
@@ -1,8 +1,8 @@
 ## Required dependencies
 
 * PostgreSQL 9.6+
-* Elixir 1.12+ (1.13+ recommended)
-* Erlang OTP 22.2+
+* Elixir 1.14+
+* Erlang OTP 24+
 * git
 * file / libmagic
 * gcc (clang might also work)
diff --git a/docs/docs/installation/yunohost_en.md b/docs/docs/installation/yunohost_en.md
new file mode 100644 (file)
index 0000000..0d3adb4
--- /dev/null
@@ -0,0 +1,9 @@
+# Installing on Yunohost
+
+[YunoHost](https://yunohost.org) is a server operating system aimed at self-hosting. The YunoHost community maintains a package of Akkoma which allows you to install Akkoma on YunoHost. You can install it via the normal way through the admin web interface, or through the CLI. More information can be found at [the repo of the package](https://github.com/YunoHost-Apps/akkoma_ynh).
+
+## Questions
+
+Questions and problems related to the YunoHost parts can be done through the [YunoHost channels](https://yunohost.org/en/help).
+
+For questions about Akkoma, check out the [Akkoma community channels](../../#community-channels).
index 946408c12fd6e9bf254895a7a24de88a4785ecd5..ee9e051a66c894468c4d03d28d12827cc8ce8226 100644 (file)
@@ -1,2 +1,2 @@
-elixir_version=1.9.4
-erlang_version=22.3.4.1
+elixir_version=1.14.3
+erlang_version=25.3
index 3914540ca9597d1a1fe9d37534d43992298f0391..87be38b7834d2f27c6501a0d723d708de5ad7388 100644 (file)
@@ -82,4 +82,46 @@ defmodule Mix.Tasks.Pleroma.Diagnostics do
     Ecto.Adapters.SQL.explain(Repo, :all, query, analyze: true, timeout: :infinity)
     |> IO.puts()
   end
+
+  def run(["notifications", nickname]) do
+    start_pleroma()
+    user = Repo.get_by!(User, nickname: nickname)
+    account_ap_id = user.ap_id
+    options = %{account_ap_id: user.ap_id}
+
+    query =
+      user
+      |> Pleroma.Notification.for_user_query(options)
+      |> where([n, a], a.actor == ^account_ap_id)
+      |> limit(20)
+
+    Ecto.Adapters.SQL.explain(Repo, :all, query, analyze: true, timeout: :infinity)
+    |> IO.puts()
+  end
+
+  def run(["known_network", nickname]) do
+    start_pleroma()
+    user = Repo.get_by!(User, nickname: nickname)
+
+    params =
+      %{}
+      |> Map.put(:type, ["Create"])
+      |> Map.put(:local_only, false)
+      |> Map.put(:blocking_user, user)
+      |> Map.put(:muting_user, user)
+      |> Map.put(:reply_filtering_user, user)
+      # Restricts unfederated content to authenticated users
+      |> Map.put(:includes_local_public, not is_nil(user))
+      |> Map.put(:restrict_unlisted, true)
+
+    query =
+      Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_query(
+        [Pleroma.Constants.as_public()],
+        params
+      )
+      |> limit(20)
+
+    Ecto.Adapters.SQL.explain(Repo, :all, query, analyze: true, timeout: :infinity)
+    |> IO.puts()
+  end
 end
index c5b51474269fb7afd76494cdf003c512db472752..c94667fb9f177be171898c9b6b4d42766a9bb1a0 100644 (file)
@@ -277,6 +277,13 @@ defmodule Pleroma.Activity do
 
   def get_create_by_object_ap_id_with_object(_), do: nil
 
+  def get_local_create_by_object_ap_id(ap_id) when is_binary(ap_id) do
+    ap_id
+    |> create_by_object_ap_id()
+    |> where(local: true)
+    |> Repo.one()
+  end
+
   @spec create_by_id_with_object(String.t()) :: t() | nil
   def create_by_id_with_object(id) do
     get_by_id_with_opts(id, preload: [:object], filter: [type: "Create"])
index 6ddfa5042ae386fa642d47fdf67a99c2d8bfe466..5c70748b6da2d5060951bffc685a9dd3f5e3f007 100644 (file)
@@ -162,7 +162,7 @@ defmodule Pleroma.Instances.Instance do
     %Instance{
       host: Pleroma.Web.Endpoint.host(),
       favicon: Pleroma.Web.Endpoint.url() <> "/favicon.png",
-      nodeinfo: Pleroma.Web.Nodeinfo.NodeinfoController.raw_nodeinfo()
+      nodeinfo: Pleroma.Web.Nodeinfo.Nodeinfo.get_nodeinfo("2.1")
     }
   end
 
index 3b5419db74309e650e08bd78f9386fa78e59d16c..2f65540be3aec7cc5c9dfdc1dc59d97e9d3d3890 100644 (file)
@@ -65,15 +65,6 @@ defmodule Pleroma.Upload do
         }
   defstruct [:id, :name, :tempfile, :content_type, :width, :height, :blurhash, :path]
 
-  defp get_description(opts, upload) do
-    case {opts[:description], Pleroma.Config.get([Pleroma.Upload, :default_description])} do
-      {description, _} when is_binary(description) -> description
-      {_, :filename} -> upload.name
-      {_, str} when is_binary(str) -> str
-      _ -> ""
-    end
-  end
-
   @spec store(source, options :: [option()]) :: {:ok, Map.t()} | {:error, any()}
   @doc "Store a file. If using a `Plug.Upload{}` as the source, be sure to use `Majic.Plug` to ensure its content_type and filename is correct."
   def store(upload, opts \\ []) do
@@ -82,7 +73,7 @@ defmodule Pleroma.Upload do
     with {:ok, upload} <- prepare_upload(upload, opts),
          upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"},
          {:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload),
-         description = get_description(opts, upload),
+         description = Map.get(opts, :description) || "",
          {_, true} <-
            {:description_limit,
             String.length(description) <= Pleroma.Config.get([:instance, :description_limit])},
index f94202af5df22510d19f30fb73a5133ae923e9c7..ead37ccca00a03f070f99cef5357fc186acc2362 100644 (file)
@@ -366,21 +366,21 @@ defmodule Pleroma.User do
   def invisible?(_), do: false
 
   def avatar_url(user, options \\ []) do
-    case user.avatar do
-      %{"url" => [%{"href" => href} | _]} ->
-        href
-
-      _ ->
-        unless options[:no_default] do
-          Config.get([:assets, :default_user_avatar], "#{Endpoint.url()}/images/avi.png")
-        end
-    end
+    default = Config.get([:assets, :default_user_avatar], "#{Endpoint.url()}/images/avi.png")
+    do_optional_url(user.avatar, default, options)
   end
 
   def banner_url(user, options \\ []) do
-    case user.banner do
-      %{"url" => [%{"href" => href} | _]} -> href
-      _ -> !options[:no_default] && "#{Endpoint.url()}/images/banner.png"
+    do_optional_url(user.banner, "#{Endpoint.url()}/images/banner.png", options)
+  end
+
+  defp do_optional_url(field, default, options) do
+    case field do
+      %{"url" => [%{"href" => href} | _]} when is_binary(href) ->
+        href
+
+      _ ->
+        unless options[:no_default], do: default
     end
   end
 
index 8e55df0d8cac2fc955b2394e197be133167ffe89..649bf909544d3f07f484c2bf966623bc29ec4e7b 100644 (file)
@@ -1502,13 +1502,22 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
 
   @spec upload(Upload.source(), keyword()) :: {:ok, Object.t()} | {:error, any()}
   def upload(file, opts \\ []) do
-    with {:ok, data} <- Upload.store(file, opts) do
+    with {:ok, data} <- Upload.store(sanitize_upload_file(file), opts) do
       obj_data = Maps.put_if_present(data, "actor", opts[:actor])
 
       Repo.insert(%Object{data: obj_data})
     end
   end
 
+  defp sanitize_upload_file(%Plug.Upload{filename: filename} = upload) when is_binary(filename) do
+    %Plug.Upload{
+      upload
+      | filename: Path.basename(filename)
+    }
+  end
+
+  defp sanitize_upload_file(upload), do: upload
+
   @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
index c07f91b2e9cb0e31dd85071d0608f1869a20683c..4e6842d85058b8d05cfab9a5d8d46aebc6899d16 100644 (file)
@@ -8,7 +8,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
   alias Pleroma.Activity
   alias Pleroma.Delivery
   alias Pleroma.Object
-  alias Pleroma.Object.Fetcher
   alias Pleroma.User
   alias Pleroma.Web.ActivityPub.ActivityPub
   alias Pleroma.Web.ActivityPub.InternalFetchActor
@@ -293,33 +292,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
     |> json("Invalid HTTP Signature")
   end
 
-  # POST /relay/inbox -or- POST /internal/fetch/inbox
-  def inbox(conn, %{"type" => "Create"} = params) do
-    if FederatingPlug.federating?() do
-      post_inbox_relayed_create(conn, params)
-    else
-      conn
-      |> put_status(:bad_request)
-      |> json("Not federating")
-    end
-  end
-
   def inbox(conn, _params) do
     conn
     |> put_status(:bad_request)
     |> json("error, missing HTTP Signature")
   end
 
-  defp post_inbox_relayed_create(conn, params) do
-    Logger.debug(
-      "Signature missing or not from author, relayed Create message, fetching object from source"
-    )
-
-    Fetcher.fetch_object_from_id(params["object"]["id"])
-
-    json(conn, "ok")
-  end
-
   defp represent_service_actor(%User{} = user, conn) do
     conn
     |> put_resp_content_type("application/activity+json")
index b187d3a48040cee9c5b1b182c4ea28051fcef093..3071c1b770593a8fe3702b2bad336285a169935f 100644 (file)
@@ -108,15 +108,28 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
       Config.get([:mrf_simple, :reject], [])
   end
 
+  defp allowed_instances do
+    Config.get([:mrf_simple, :accept])
+  end
+
   def should_federate?(url) do
     %{host: host} = URI.parse(url)
 
-    quarantined_instances =
-      blocked_instances()
+    with allowed <- allowed_instances(),
+         false <- Enum.empty?(allowed) do
+      allowed
       |> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples()
       |> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
+      |> Pleroma.Web.ActivityPub.MRF.subdomain_match?(host)
+    else
+      _ ->
+        quarantined_instances =
+          blocked_instances()
+          |> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples()
+          |> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
 
-    !Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
+        not Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
+    end
   end
 
   @spec recipients(User.t(), Activity.t()) :: list(User.t()) | []
index c13ff90968f6da314f3014fbb800725601c30412..307d3564341c129c26a6310fbb4260ad6fe9fbd6 100644 (file)
@@ -5,6 +5,16 @@ defmodule Pleroma.Web.AkkomaAPI.FrontendSettingsController do
   alias Pleroma.Akkoma.FrontendSettingsProfile
 
   @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
+
+  plug(
+    OAuthScopesPlug,
+    @unauthenticated_access
+    when action in [
+           :available_frontends,
+           :update_preferred_frontend
+         ]
+  )
+
   plug(
     OAuthScopesPlug,
     %{@unauthenticated_access | scopes: ["read:accounts"]}
@@ -93,4 +103,22 @@ defmodule Pleroma.Web.AkkomaAPI.FrontendSettingsController do
       |> json(profile.settings)
     end
   end
+
+  @doc "GET /api/v1/akkoma/preferred_frontend/available"
+  def available_frontends(conn, _params) do
+    available = Pleroma.Config.get([:frontends, :pickable])
+
+    conn
+    |> json(available)
+  end
+
+  @doc "PUT /api/v1/akkoma/preferred_frontend"
+  def update_preferred_frontend(
+        %{body_params: %{frontend_name: preferred_frontend}} = conn,
+        _params
+      ) do
+    conn
+    |> put_resp_cookie("preferred_frontend", preferred_frontend)
+    |> json(%{frontend_name: preferred_frontend})
+  end
 end
diff --git a/lib/pleroma/web/akkoma_api/controllers/frontend_switcher.ex b/lib/pleroma/web/akkoma_api/controllers/frontend_switcher.ex
new file mode 100644 (file)
index 0000000..2095db4
--- /dev/null
@@ -0,0 +1,20 @@
+defmodule Pleroma.Web.AkkomaAPI.FrontendSwitcherController do
+  use Pleroma.Web, :controller
+  alias Pleroma.Config
+
+  @doc "GET /akkoma/frontend"
+  def switch(conn, _params) do
+    pickable = Config.get([:frontends, :pickable], [])
+
+    conn
+    |> put_view(Pleroma.Web.AkkomaAPI.FrontendSwitcherView)
+    |> render("switch.html", choices: pickable)
+  end
+
+  @doc "POST /akkoma/frontend"
+  def do_switch(conn, params) do
+    conn
+    |> put_resp_cookie("preferred_frontend", params["frontend"])
+    |> html("<meta http-equiv=\"refresh\" content=\"0; url=/\">")
+  end
+end
diff --git a/lib/pleroma/web/akkoma_api/views/frontend_switcher.ex b/lib/pleroma/web/akkoma_api/views/frontend_switcher.ex
new file mode 100644 (file)
index 0000000..1564c9e
--- /dev/null
@@ -0,0 +1,3 @@
+defmodule Pleroma.Web.AkkomaAPI.FrontendSwitcherView do
+  use Pleroma.Web, :view
+end
index 40e81ad55b4568db9db08fe54854275c266dfb21..867a751b3362dc247b7d21ac94444d10d1c9a7de 100644 (file)
@@ -12,7 +12,7 @@ defmodule Pleroma.Web.ApiSpec.FrontendSettingsOperation do
   @spec list_profiles_operation() :: Operation.t()
   def list_profiles_operation() do
     %Operation{
-      tags: ["Retrieve frontend setting profiles"],
+      tags: ["Frontends"],
       summary: "Frontend Settings Profiles",
       description: "List frontend setting profiles",
       operationId: "AkkomaAPI.FrontendSettingsController.list_profiles",
@@ -37,7 +37,7 @@ defmodule Pleroma.Web.ApiSpec.FrontendSettingsOperation do
   @spec get_profile_operation() :: Operation.t()
   def get_profile_operation() do
     %Operation{
-      tags: ["Retrieve frontend setting profile"],
+      tags: ["Frontends"],
       summary: "Frontend Settings Profile",
       description: "Get frontend setting profile",
       operationId: "AkkomaAPI.FrontendSettingsController.get_profile",
@@ -60,7 +60,7 @@ defmodule Pleroma.Web.ApiSpec.FrontendSettingsOperation do
   @spec delete_profile_operation() :: Operation.t()
   def delete_profile_operation() do
     %Operation{
-      tags: ["Delete frontend setting profile"],
+      tags: ["Frontends"],
       summary: "Delete frontend Settings Profile",
       description: "Delete  frontend setting profile",
       operationId: "AkkomaAPI.FrontendSettingsController.delete_profile",
@@ -76,7 +76,7 @@ defmodule Pleroma.Web.ApiSpec.FrontendSettingsOperation do
   @spec update_profile_operation() :: Operation.t()
   def update_profile_operation() do
     %Operation{
-      tags: ["Update frontend setting profile"],
+      tags: ["Frontends"],
       summary: "Frontend Settings Profile",
       description: "Update frontend setting profile",
       operationId: "AkkomaAPI.FrontendSettingsController.update_profile_operation",
@@ -90,6 +90,57 @@ defmodule Pleroma.Web.ApiSpec.FrontendSettingsOperation do
     }
   end
 
+  def available_frontends_operation() do
+    %Operation{
+      tags: ["Frontends"],
+      summary: "Frontend Settings Profiles",
+      description: "List frontend setting profiles",
+      operationId: "AkkomaAPI.FrontendSettingsController.available_frontends",
+      responses: %{
+        200 =>
+          Operation.response("Frontends", "application/json", %Schema{
+            type: :array,
+            items: %Schema{
+              type: :string
+            }
+          })
+      }
+    }
+  end
+
+  def update_preferred_frontend_operation() do
+    %Operation{
+      tags: ["Frontends"],
+      summary: "Frontend Settings Profiles",
+      description: "List frontend setting profiles",
+      operationId: "AkkomaAPI.FrontendSettingsController.available_frontends",
+      requestBody:
+        request_body(
+          "Frontend",
+          %Schema{
+            type: :object,
+            required: [:frontend_name],
+            properties: %{
+              frontend_name: %Schema{
+                type: :string,
+                description: "Frontend name"
+              }
+            }
+          },
+          required: true
+        ),
+      responses: %{
+        200 =>
+          Operation.response("Frontends", "application/json", %Schema{
+            type: :array,
+            items: %Schema{
+              type: :string
+            }
+          })
+      }
+    }
+  end
+
   def frontend_name_param do
     Operation.parameter(:frontend_name, :path, :string, "Frontend name",
       example: "pleroma-fe",
index 3eb6f700b7f959986d9f58c5a6a93b502d4cfd62..45c97cab6d39d56ca744cc09a2dfbd9eaa21d1c1 100644 (file)
@@ -70,7 +70,8 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do
       operationId: "TimelineController.public",
       responses: %{
         200 => Operation.response("Array of Status", "application/json", array_of_statuses()),
-        401 => Operation.response("Error", "application/json", ApiError)
+        401 => Operation.response("Error", "application/json", ApiError),
+        404 => Operation.response("Error", "application/json", ApiError)
       }
     }
   end
index 49f659cf0138035d1b8da31d9296ff8889c3cecc..2e57fa42652874cb3de5c1bf20c5959acda59816 100644 (file)
@@ -20,7 +20,7 @@ defmodule Pleroma.Web.Fallback.RedirectController do
   def redirector(conn, _params, code \\ 200) do
     conn
     |> put_resp_content_type("text/html")
-    |> send_file(code, index_file_path())
+    |> send_file(code, index_file_path(conn))
   end
 
   def redirector_with_meta(conn, %{"maybe_nickname_or_id" => maybe_nickname_or_id} = params) do
@@ -33,7 +33,7 @@ defmodule Pleroma.Web.Fallback.RedirectController do
   end
 
   def redirector_with_meta(conn, params) do
-    {:ok, index_content} = File.read(index_file_path())
+    {:ok, index_content} = File.read(index_file_path(conn))
 
     tags = build_tags(conn, params)
     preloads = preload_data(conn, params)
@@ -53,7 +53,7 @@ defmodule Pleroma.Web.Fallback.RedirectController do
   end
 
   def redirector_with_preload(conn, params) do
-    {:ok, index_content} = File.read(index_file_path())
+    {:ok, index_content} = File.read(index_file_path(conn))
     preloads = preload_data(conn, params)
     tags = Metadata.build_static_tags(params)
     title = "<title>#{Pleroma.Config.get([:instance, :name])}</title>"
@@ -77,8 +77,9 @@ defmodule Pleroma.Web.Fallback.RedirectController do
     |> text("")
   end
 
-  defp index_file_path do
-    Pleroma.Web.Plugs.InstanceStatic.file_path("index.html")
+  defp index_file_path(conn) do
+    frontend_type = Pleroma.Web.Plugs.FrontendStatic.preferred_or_fallback(conn, :primary)
+    Pleroma.Web.Plugs.InstanceStatic.file_path("index.html", frontend_type)
   end
 
   defp build_tags(conn, params) do
index 2d0e36420f7a90c6007acd255e73fb467ddedda8..1d4e734a40d811f18ddd52bc542835173b490773 100644 (file)
@@ -16,7 +16,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
   alias Pleroma.Web.Plugs.RateLimiter
 
   plug(Pleroma.Web.ApiSpec.CastAndValidate)
-  plug(:skip_public_check when action in [:public, :hashtag])
+  plug(:skip_public_check when action in [:public, :hashtag, :bubble])
 
   # TODO: Replace with a macro when there is a Phoenix release with the following commit in it:
   # https://github.com/phoenixframework/phoenix/commit/2e8c63c01fec4dde5467dbbbf9705ff9e780735e
@@ -28,19 +28,25 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
   plug(RateLimiter, [name: :timeline, bucket_name: :list_timeline] when action == :list)
   plug(RateLimiter, [name: :timeline, bucket_name: :bubble_timeline] when action == :bubble)
 
-  plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:home, :direct, :bubble])
+  plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:home, :direct])
   plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :list)
 
   plug(
     OAuthScopesPlug,
     %{scopes: ["read:statuses"], fallback: :proceed_unauthenticated}
-    when action in [:public, :hashtag]
+    when action in [:public, :hashtag, :bubble]
   )
 
+  require Logger
+
   defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.TimelineOperation
 
   # GET /api/v1/timelines/home
   def home(%{assigns: %{user: user}} = conn, params) do
+    %{nickname: nickname} = user
+
+    Logger.debug("TimelineController.home: #{nickname}")
+
     followed_hashtags =
       user
       |> User.followed_hashtags()
@@ -58,11 +64,15 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
       |> Map.put(:followed_hashtags, followed_hashtags)
       |> Map.delete(:local)
 
+    Logger.debug("TimelineController.home: #{nickname} - fetching activities")
+
     activities =
       [user.ap_id | User.following(user)]
       |> ActivityPub.fetch_activities(params)
       |> Enum.reverse()
 
+    Logger.debug("TimelineController.home: #{nickname} - rendering")
+
     conn
     |> add_link_headers(activities)
     |> render("index.json",
@@ -75,6 +85,8 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
 
   # GET /api/v1/timelines/direct
   def direct(%{assigns: %{user: user}} = conn, params) do
+    Logger.debug("TimelineController.direct: #{user.nickname}")
+
     params =
       params
       |> Map.put(:type, "Create")
@@ -82,11 +94,15 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
       |> Map.put(:user, user)
       |> Map.put(:visibility, "direct")
 
+    Logger.debug("TimelineController.direct: #{user.nickname} - fetching activities")
+
     activities =
       [user.ap_id]
       |> ActivityPub.fetch_activities_query(params)
       |> Pagination.fetch_paginated(params)
 
+    Logger.debug("TimelineController.direct: #{user.nickname} - rendering")
+
     conn
     |> add_link_headers(activities)
     |> render("index.json",
@@ -96,21 +112,22 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
     )
   end
 
-  defp restrict_unauthenticated?(true = _local_only) do
-    Config.restrict_unauthenticated_access?(:timelines, :local)
-  end
-
-  defp restrict_unauthenticated?(_) do
-    Config.restrict_unauthenticated_access?(:timelines, :federated)
+  defp restrict_unauthenticated?(type) do
+    Config.restrict_unauthenticated_access?(:timelines, type)
   end
 
   # GET /api/v1/timelines/public
   def public(%{assigns: %{user: user}} = conn, params) do
+    Logger.debug("TimelineController.public")
     local_only = params[:local]
+    timeline_type = if local_only, do: :local, else: :federated
+
+    with {:enabled, true} <-
+           {:enabled, local_only || Config.get([:instance, :federated_timeline_available], true)},
+         {:authenticated, true} <-
+           {:authenticated, !(is_nil(user) and restrict_unauthenticated?(timeline_type))} do
+      Logger.debug("TimelineController.public: fetching activities")
 
-    if is_nil(user) and restrict_unauthenticated?(local_only) do
-      fail_on_bad_auth(conn)
-    else
       activities =
         params
         |> Map.put(:type, ["Create"])
@@ -123,6 +140,8 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
         |> Map.put(:includes_local_public, not is_nil(user))
         |> ActivityPub.fetch_public_activities()
 
+      Logger.debug("TimelineController.public: rendering")
+
       conn
       |> add_link_headers(activities, %{"local" => local_only})
       |> render("index.json",
@@ -131,20 +150,32 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
         as: :activity,
         with_muted: Map.get(params, :with_muted, false)
       )
+    else
+      {:enabled, false} ->
+        conn
+        |> put_status(404)
+        |> json(%{error: "Federated timeline is disabled"})
+
+      {:authenticated, false} ->
+        fail_on_bad_auth(conn)
     end
   end
 
   # GET /api/v1/timelines/bubble
   def bubble(%{assigns: %{user: user}} = conn, params) do
-    bubble_instances =
-      Enum.uniq(
-        Config.get([:instance, :local_bubble], []) ++
-          [Pleroma.Web.Endpoint.host()]
-      )
+    Logger.debug("TimelineController.bubble")
 
-    if is_nil(user) do
+    if is_nil(user) and restrict_unauthenticated?(:bubble) do
       fail_on_bad_auth(conn)
     else
+      bubble_instances =
+        Enum.uniq(
+          Config.get([:instance, :local_bubble], []) ++
+            [Pleroma.Web.Endpoint.host()]
+        )
+
+      Logger.debug("TimelineController.bubble: fetching activities")
+
       activities =
         params
         |> Map.put(:type, ["Create"])
@@ -154,6 +185,8 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
         |> Map.put(:instance, bubble_instances)
         |> ActivityPub.fetch_public_activities()
 
+      Logger.debug("TimelineController.bubble: rendering")
+
       conn
       |> add_link_headers(activities)
       |> render("index.json",
@@ -195,7 +228,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
   def hashtag(%{assigns: %{user: user}} = conn, params) do
     local_only = params[:local]
 
-    if is_nil(user) and restrict_unauthenticated?(local_only) do
+    if is_nil(user) and restrict_unauthenticated?(if local_only, do: :local, else: :federated) do
       fail_on_bad_auth(conn)
     else
       activities = hashtag_fetching(params, user, local_only)
index 2717da99d950b3c441ee3c7263c30b4ba61959ae..2b535487377dd2a591fb9d3c1c5eb6faeb411ffa 100644 (file)
@@ -67,6 +67,9 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
       "pleroma:api/v1/notifications:include_types_filter",
       "quote_posting",
       "editing",
+      if !Enum.empty?(Config.get([:instance, :local_bubble], [])) do
+        "bubble_timeline"
+      end,
       if Config.get([:media_proxy, :enabled]) do
         "media_proxy"
       end,
index 3868da8d9310c96ec33fcf65230e5eea2552e423..47d1616c47b36794be2e830f8a33e45ea3f3255b 100644 (file)
@@ -21,6 +21,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
   alias Pleroma.Web.MastodonAPI.StatusView
   alias Pleroma.Web.MediaProxy
   alias Pleroma.Web.PleromaAPI.EmojiReactionController
+  require Logger
 
   import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1, visible_for_user?: 2]
 
@@ -87,6 +88,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
   defp reblogged?(_activity, _user), do: false
 
   def render("index.json", opts) do
+    Logger.debug("Rendering index")
     reading_user = opts[:for]
     # To do: check AdminAPIControllerTest on the reasons behind nil activities in the list
     activities = Enum.filter(opts.activities, & &1)
@@ -136,8 +138,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
 
   def render(
         "show.json",
-        %{activity: %{data: %{"type" => "Announce", "object" => _object}} = activity} = opts
+        %{activity: %{id: id, data: %{"type" => "Announce", "object" => _object}} = activity} =
+          opts
       ) do
+    Logger.debug("Rendering reblog #{id}")
     user = CommonAPI.get_user(activity.data["actor"])
     created_at = Utils.to_masto_date(activity.data["published"])
     object = Object.normalize(activity, fetch: false)
@@ -209,7 +213,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
     }
   end
 
-  def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
+  def render("show.json", %{activity: %{id: id, data: %{"object" => _object}} = activity} = opts) do
+    Logger.debug("Rendering status #{id}")
+
     with %Object{} = object <- Object.normalize(activity, fetch: false) do
       user = CommonAPI.get_user(activity.data["actor"])
       user_follower_address = user.follower_address
@@ -428,6 +434,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
   end
 
   def render("history.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
+    Logger.debug("Rendering history for #{activity.id}")
     object = Object.normalize(activity, fetch: false)
 
     hashtags = Object.hashtags(object)
@@ -614,6 +621,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
   def render("attachment_meta.json", _), do: nil
 
   def render("context.json", %{activity: activity, activities: activities, user: user}) do
+    Logger.debug("Rendering context for #{activity.id}")
+
     %{ancestors: ancestors, descendants: descendants} =
       activities
       |> Enum.reverse()
index bf0d65f4522ae1b72aa6267b47263d2e15b08a6a..532ae53a721d3a4eaaa19a9e70ba213360b67462 100644 (file)
@@ -71,7 +71,15 @@ defmodule Pleroma.Web.Nodeinfo.Nodeinfo do
         restrictedNicknames: Config.get([Pleroma.User, :restricted_nicknames]),
         skipThreadContainment: Config.get([:instance, :skip_thread_containment], false),
         privilegedStaff: Config.get([:instance, :privileged_staff]),
-        localBubbleInstances: Config.get([:instance, :local_bubble], [])
+        localBubbleInstances: Config.get([:instance, :local_bubble], []),
+        publicTimelineVisibility: %{
+          federated:
+            !Config.restrict_unauthenticated_access?(:timelines, :federated) &&
+              Config.get([:instance, :federated_timeline_available], true),
+          local: !Config.restrict_unauthenticated_access?(:timelines, :local),
+          bubble: !Config.restrict_unauthenticated_access?(:timelines, :bubble)
+        },
+        federatedTimelineAvailable: Config.get([:instance, :federated_timeline_available], true)
       }
     }
   end
index a0dee7c6bc283d370e667a051bb61ad530adcdf4..ea2d86f92ec4e51b9b6bcaf14d19ce24cd450e3c 100644 (file)
@@ -5,12 +5,8 @@
 defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
   use Pleroma.Web, :controller
 
-  alias Pleroma.Config
-  alias Pleroma.Stats
-  alias Pleroma.User
-  alias Pleroma.Web.Federator.Publisher
-  alias Pleroma.Web.MastodonAPI.InstanceView
   alias Pleroma.Web.Endpoint
+  alias Pleroma.Web.Nodeinfo.Nodeinfo
 
   def schemas(conn, _params) do
     response = %{
@@ -29,101 +25,15 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
     json(conn, response)
   end
 
-  # returns a nodeinfo 2.0 map, since 2.1 just adds a repository field
-  # under software.
-  def raw_nodeinfo do
-    stats = Stats.get_stats()
-
-    staff_accounts =
-      User.all_superusers()
-      |> Enum.map(fn u -> u.ap_id end)
-      |> Enum.filter(fn u -> not Enum.member?(Config.get([:instance, :staff_transparency]), u) end)
-
-    features = InstanceView.features()
-    federation = InstanceView.federation()
-
-    %{
-      version: "2.0",
-      software: %{
-        name: Pleroma.Application.name() |> String.downcase(),
-        version: Pleroma.Application.version()
-      },
-      protocols: Publisher.gather_nodeinfo_protocol_names(),
-      services: %{
-        inbound: [],
-        outbound: []
-      },
-      openRegistrations: Config.get([:instance, :registrations_open]),
-      usage: %{
-        users: %{
-          total: Map.get(stats, :user_count, 0)
-        },
-        localPosts: Map.get(stats, :status_count, 0)
-      },
-      metadata: %{
-        nodeName: Config.get([:instance, :name]),
-        nodeDescription: Config.get([:instance, :description]),
-        private: !Config.get([:instance, :public], true),
-        suggestions: %{
-          enabled: false
-        },
-        staffAccounts: staff_accounts,
-        federation: federation,
-        pollLimits: Config.get([:instance, :poll_limits]),
-        postFormats: Config.get([:instance, :allowed_post_formats]),
-        uploadLimits: %{
-          general: Config.get([:instance, :upload_limit]),
-          avatar: Config.get([:instance, :avatar_upload_limit]),
-          banner: Config.get([:instance, :banner_upload_limit]),
-          background: Config.get([:instance, :background_upload_limit])
-        },
-        fieldsLimits: %{
-          maxFields: Config.get([:instance, :max_account_fields]),
-          maxRemoteFields: Config.get([:instance, :max_remote_account_fields]),
-          nameLength: Config.get([:instance, :account_field_name_length]),
-          valueLength: Config.get([:instance, :account_field_value_length])
-        },
-        accountActivationRequired: Config.get([:instance, :account_activation_required], false),
-        invitesEnabled: Config.get([:instance, :invites_enabled], false),
-        mailerEnabled: Config.get([Pleroma.Emails.Mailer, :enabled], false),
-        features: features,
-        restrictedNicknames: Config.get([Pleroma.User, :restricted_nicknames]),
-        skipThreadContainment: Config.get([:instance, :skip_thread_containment], false),
-        localBubbleInstances: Config.get([:instance, :local_bubble], [])
-      }
-    }
-  end
-
   # Schema definition: https://github.com/jhass/nodeinfo/blob/master/schemas/2.0/schema.json
   # and https://github.com/jhass/nodeinfo/blob/master/schemas/2.1/schema.json
-  def nodeinfo(conn, %{"version" => "2.0"}) do
+  def nodeinfo(conn, %{"version" => version}) when version in ["2.0", "2.1"] do
     conn
     |> put_resp_header(
       "content-type",
       "application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.0#; charset=utf-8"
     )
-    |> json(raw_nodeinfo())
-  end
-
-  def nodeinfo(conn, %{"version" => "2.1"}) do
-    raw_response = raw_nodeinfo()
-
-    updated_software =
-      raw_response
-      |> Map.get(:software)
-      |> Map.put(:repository, Pleroma.Application.repository())
-
-    response =
-      raw_response
-      |> Map.put(:software, updated_software)
-      |> Map.put(:version, "2.1")
-
-    conn
-    |> put_resp_header(
-      "content-type",
-      "application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.1#; charset=utf-8"
-    )
-    |> json(response)
+    |> json(Nodeinfo.get_nodeinfo(version))
   end
 
   def nodeinfo(conn, _) do
index 7731d847f6e0a3f9570266d574dd69e57debcfa9..95a22895e547a1aca24c39759c91955c249b4826 100644 (file)
@@ -36,7 +36,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do
   def object(conn, _params) do
     with id <- Endpoint.url() <> conn.request_path,
          {_, %Activity{} = activity} <-
-           {:activity, Activity.get_create_by_object_ap_id_with_object(id)},
+           {:activity, Activity.get_local_create_by_object_ap_id(id)},
          {_, true} <- {:public?, Visibility.is_public?(activity)},
          {_, false} <- {:local_public?, Visibility.is_local_public?(activity)} do
       redirect(conn, to: "/notice/#{activity.id}")
index 40f51e149a0da9d798ae81a1a84d977c47e5d4ab..41b8ba46b90d659a5ba17069f9a8da6eae87076b 100644 (file)
@@ -5,17 +5,23 @@
 defmodule Pleroma.Web.Plugs.FrontendStatic do
   require Pleroma.Constants
 
+  @frontend_cookie_name "preferred_frontend"
+
   @moduledoc """
   This is a shim to call `Plug.Static` but with runtime `from` configuration`. It dispatches to the different frontends.
   """
   @behaviour Plug
 
-  def file_path(path, frontend_type \\ :primary) do
-    if configuration = Pleroma.Config.get([:frontends, frontend_type]) do
-      instance_static_path = Pleroma.Config.get([:instance, :static_dir], "instance/static")
+  defp instance_static_path do
+    Pleroma.Config.get([:instance, :static_dir], "instance/static")
+  end
+
+  def file_path(path, frontend_type \\ :primary)
 
+  def file_path(path, frontend_type) when is_atom(frontend_type) do
+    if configuration = Pleroma.Config.get([:frontends, frontend_type]) do
       Path.join([
-        instance_static_path,
+        instance_static_path(),
         "frontends",
         configuration["name"],
         configuration["ref"],
@@ -26,6 +32,15 @@ defmodule Pleroma.Web.Plugs.FrontendStatic do
     end
   end
 
+  def file_path(path, frontend_type) when is_binary(frontend_type) do
+    Path.join([
+      instance_static_path(),
+      "frontends",
+      frontend_type,
+      path
+    ])
+  end
+
   def init(opts) do
     opts
     |> Keyword.put(:from, "__unconfigured_frontend_static_plug")
@@ -38,7 +53,8 @@ defmodule Pleroma.Web.Plugs.FrontendStatic do
     with false <- api_route?(conn.path_info),
          false <- invalid_path?(conn.path_info),
          true <- enabled?(opts[:if]),
-         frontend_type <- Map.get(opts, :frontend_type, :primary),
+         fallback_frontend_type <- Map.get(opts, :frontend_type, :primary),
+         frontend_type <- preferred_or_fallback(conn, fallback_frontend_type),
          path when not is_nil(path) <- file_path("", frontend_type) do
       call_static(conn, opts, path)
     else
@@ -47,6 +63,31 @@ defmodule Pleroma.Web.Plugs.FrontendStatic do
     end
   end
 
+  def preferred_frontend(conn) do
+    %{req_cookies: cookies} =
+      conn
+      |> Plug.Conn.fetch_cookies()
+
+    Map.get(cookies, @frontend_cookie_name)
+  end
+
+  # Only override primary frontend
+  def preferred_or_fallback(conn, :primary) do
+    case preferred_frontend(conn) do
+      nil ->
+        :primary
+
+      frontend ->
+        if Enum.member?(Pleroma.Config.get([:frontends, :pickable], []), frontend) do
+          frontend
+        else
+          :primary
+        end
+    end
+  end
+
+  def preferred_or_fallback(_conn, fallback), do: fallback
+
   defp enabled?(if_opt) when is_function(if_opt), do: if_opt.()
   defp enabled?(true), do: true
   defp enabled?(_), do: false
index b1f1ada94c98553a49393bd69e790638d5372c48..d7cff73436a83701ed02fbfe16b85174edea0265 100644 (file)
@@ -8,6 +8,8 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do
 
   require Logger
 
+  @mix_env Mix.env()
+
   def init(opts), do: opts
 
   def call(conn, _options) do
@@ -114,7 +116,14 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do
     style_src = "style-src 'self' '#{nonce_tag}'"
     font_src = "font-src 'self'"
 
-    script_src = "script-src 'self' '#{nonce_tag}'"
+    script_src = "script-src 'self' '#{nonce_tag}' "
+
+    script_src =
+      if @mix_env == :dev do
+        "script-src 'self' 'unsafe-eval' 'unsafe-inline'"
+      else
+        script_src
+      end
 
     report = if report_uri, do: ["report-uri ", report_uri, ";report-to csp-endpoint"]
     insecure = if scheme == "https", do: "upgrade-insecure-requests"
index 723b256793b28485e2f947003ec383c66c92db12..5f9a6ee83e68390a9d6e5ce4f1575a82c859ab76 100644 (file)
@@ -12,11 +12,11 @@ defmodule Pleroma.Web.Plugs.InstanceStatic do
   """
   @behaviour Plug
 
-  def file_path(path) do
+  def file_path(path, frontend_type \\ :primary) do
     instance_path =
       Path.join(Pleroma.Config.get([:instance, :static_dir], "instance/static/"), path)
 
-    frontend_path = Pleroma.Web.Plugs.FrontendStatic.file_path(path, :primary)
+    frontend_path = Pleroma.Web.Plugs.FrontendStatic.file_path(path, frontend_type)
 
     (File.exists?(instance_path) && instance_path) ||
       (frontend_path && File.exists?(frontend_path) && frontend_path) ||
index faaf3d67979b409c2224d85a99c6b62588bb6cbc..7550eefdf5663db00af287db1767312b0b4c6fec 100644 (file)
@@ -466,6 +466,29 @@ defmodule Pleroma.Web.Router do
     put("/statuses/:id/emoji_reactions/:emoji", EmojiReactionController, :create)
   end
 
+  scope "/akkoma/", Pleroma.Web.AkkomaAPI do
+    pipe_through(:browser)
+
+    get("/frontend", FrontendSwitcherController, :switch)
+    post("/frontend", FrontendSwitcherController, :do_switch)
+  end
+
+  scope "/api/v1/akkoma", Pleroma.Web.AkkomaAPI do
+    pipe_through(:api)
+
+    get(
+      "/api/v1/akkoma/preferred_frontend/available",
+      FrontendSettingsController,
+      :available_frontends
+    )
+
+    put(
+      "/api/v1/akkoma/preferred_frontend",
+      FrontendSettingsController,
+      :update_preferred_frontend
+    )
+  end
+
   scope "/api/v1/akkoma", Pleroma.Web.AkkomaAPI do
     pipe_through(:authenticated_api)
     get("/metrics", MetricsController, :show)
@@ -598,7 +621,6 @@ defmodule Pleroma.Web.Router do
     get("/timelines/home", TimelineController, :home)
     get("/timelines/direct", TimelineController, :direct)
     get("/timelines/list/:list_id", TimelineController, :list)
-    get("/timelines/bubble", TimelineController, :bubble)
 
     get("/announcements", AnnouncementController, :index)
     post("/announcements/:id/dismiss", AnnouncementController, :mark_read)
@@ -653,6 +675,7 @@ defmodule Pleroma.Web.Router do
 
     get("/timelines/public", TimelineController, :public)
     get("/timelines/tag/:tag", TimelineController, :hashtag)
+    get("/timelines/bubble", TimelineController, :bubble)
 
     get("/polls/:id", PollController, :show)
 
diff --git a/lib/pleroma/web/templates/akkoma_api/frontend_switcher/switch.html.eex b/lib/pleroma/web/templates/akkoma_api/frontend_switcher/switch.html.eex
new file mode 100644 (file)
index 0000000..a0b0a23
--- /dev/null
@@ -0,0 +1,10 @@
+<h2>Switch Frontend</h2>
+
+<h3>After you submit, you will need to refresh manually to get your new frontend!</h3>
+
+<%= form_for @conn, Routes.frontend_switcher_path(@conn, :do_switch), fn f -> %>
+  <%= select(f, :frontend, @choices) %>
+
+  <%= submit do: "submit" %>
+<% end %>
+
diff --git a/mix.exs b/mix.exs
index 098ea15b036150740780a1526661e424daf93a8d..6544529bd502e3607003b836615c1048e215989c 100644 (file)
--- a/mix.exs
+++ b/mix.exs
@@ -4,8 +4,8 @@ defmodule Pleroma.Mixfile do
   def project do
     [
       app: :pleroma,
-      version: version("3.7.1"),
-      elixir: "~> 1.12",
+      version: version("3.7.2"),
+      elixir: "~> 1.14",
       elixirc_paths: elixirc_paths(Mix.env()),
       compilers: [:phoenix] ++ Mix.compilers(),
       elixirc_options: [warnings_as_errors: warnings_as_errors()],
index 369d1ed252d16a1e42b6f1abfa54b32ba68be972..bee2c15857260796be5f54b0bef0e9d2ae0bf0ad 100644 (file)
--- a/mix.lock
+++ b/mix.lock
@@ -27,7 +27,7 @@
   "earmark": {:hex, :earmark, "1.4.37", "56ce845c543393aa3f9b294c818c3d783452a4a67e4ab18c4303a954a8b59363", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "d86d5e12868db86d5321b00e62a4bbcb4150346e4acc9a90a041fb188a5cb106"},
   "earmark_parser": {:hex, :earmark_parser, "1.4.31", "a93921cdc6b9b869f519213d5bc79d9e218ba768d7270d46fdcf1c01bacff9e2", [:mix], [], "hexpm", "317d367ee0335ef037a87e46c91a2269fef6306413f731e8ec11fc45a7efd059"},
   "eblurhash": {:hex, :eblurhash, "1.2.2", "7da4255aaea984b31bb71155f673257353b0e0554d0d30dcf859547e74602582", [:rebar3], [], "hexpm", "8c20ca00904de023a835a9dcb7b7762fed32264c85a80c3cafa85288e405044c"},
-  "ecto": {:hex, :ecto, "3.9.4", "3ee68e25dbe0c36f980f1ba5dd41ee0d3eb0873bccae8aeaf1a2647242bffa35", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "de5f988c142a3aa4ec18b85a4ec34a2390b65b24f02385c1144252ff6ff8ee75"},
+  "ecto": {:hex, :ecto, "3.9.5", "9f0aa7ae44a1577b651c98791c6988cd1b69b21bc724e3fd67090b97f7604263", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d4f3115d8cbacdc0bfa4b742865459fb1371d0715515842a1fb17fe31920b74c"},
   "ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"},
   "ecto_psql_extras": {:hex, :ecto_psql_extras, "0.7.10", "e14d400930f401ca9f541b3349212634e44027d7f919bbb71224d7ac0d0e8acd", [:mix], [{:ecto_sql, "~> 3.4", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.15.7 or ~> 0.16.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "505e8cd81e4f17c090be0f99e92b1b3f0fd915f98e76965130b8ccfb891e7088"},
   "ecto_sql": {:hex, :ecto_sql, "3.9.2", "34227501abe92dba10d9c3495ab6770e75e79b836d114c41108a4bf2ce200ad5", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9.2", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1eb5eeb4358fdbcd42eac11c1fbd87e3affd7904e639d77903c1358b2abd3f70"},
@@ -38,7 +38,7 @@
   "ex_aws": {:hex, :ex_aws, "2.1.9", "dc4865ecc20a05190a34a0ac5213e3e5e2b0a75a0c2835e923ae7bfeac5e3c31", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "3e6c776703c9076001fbe1f7c049535f042cb2afa0d2cbd3b47cbc4e92ac0d10"},
   "ex_aws_s3": {:hex, :ex_aws_s3, "2.4.0", "ce8decb6b523381812798396bc0e3aaa62282e1b40520125d1f4eff4abdff0f4", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "85dda6e27754d94582869d39cba3241d9ea60b6aa4167f9c88e309dc687e56bb"},
   "ex_const": {:hex, :ex_const, "0.2.4", "d06e540c9d834865b012a17407761455efa71d0ce91e5831e86881b9c9d82448", [:mix], [], "hexpm", "96fd346610cc992b8f896ed26a98be82ac4efb065a0578f334a32d60a3ba9767"},
-  "ex_doc": {:hex, :ex_doc, "0.29.2", "dfa97532ba66910b2a3016a4bbd796f41a86fc71dd5227e96f4c8581fdf0fdf0", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "6b5d7139eda18a753e3250e27e4a929f8d2c880dd0d460cb9986305dea3e03af"},
+  "ex_doc": {:hex, :ex_doc, "0.29.3", "f07444bcafb302db86e4f02d8bbcd82f2e881a0dcf4f3e4740e4b8128b9353f7", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "3dc6787d7b08801ec3b51e9bd26be5e8826fbf1a17e92d1ebc252e1a1c75bfe1"},
   "ex_machina": {:hex, :ex_machina, "2.7.0", "b792cc3127fd0680fecdb6299235b4727a4944a09ff0fa904cc639272cd92dc7", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "419aa7a39bde11894c87a615c4ecaa52d8f107bbdd81d810465186f783245bf8"},
   "ex_syslogger": {:hex, :ex_syslogger, "1.5.2", "72b6aa2d47a236e999171f2e1ec18698740f40af0bd02c8c650bf5f1fd1bac79", [:mix], [{:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:syslog, "~> 1.1.0", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "ab9fab4136dbc62651ec6f16fa4842f10cf02ab4433fa3d0976c01be99398399"},
   "excoveralls": {:hex, :excoveralls, "0.15.1", "83c8cf7973dd9d1d853dce37a2fb98aaf29b564bf7d01866e409abf59dac2c0e", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f8416bd90c0082d56a2178cf46c837595a06575f70a5624f164a1ffe37de07e7"},
   "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"},
   "phoenix_html": {:hex, :phoenix_html, "3.3.1", "4788757e804a30baac6b3fc9695bf5562465dd3f1da8eb8460ad5b404d9a2178", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bed1906edd4906a15fd7b412b85b05e521e1f67c9a85418c55999277e553d0d3"},
   "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.7.2", "97cc4ff2dba1ebe504db72cb45098cb8e91f11160528b980bd282cc45c73b29c", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0e5fdf063c7a3b620c566a30fcf68b7ee02e5e46fe48ee46a6ec3ba382dc05b7"},
-  "phoenix_live_view": {:hex, :phoenix_live_view, "0.18.17", "74938b02f3c531bed3f87fe1ea39af6b5b2d26ab1405e77e76b8ef5df9ffa8a1", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f4b5710e19a29b8dc93b7af4bab4739c067a3cb759af01ffc3057165453dce38"},
+  "phoenix_live_view": {:hex, :phoenix_live_view, "0.18.18", "1f38fbd7c363723f19aad1a04b5490ff3a178e37daaf6999594d5f34796c47fc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a5810d0472f3189ede6d2a95bda7f31c6113156b91784a3426cb0ab6a6d85214"},
   "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
   "phoenix_swoosh": {:hex, :phoenix_swoosh, "0.3.4", "615f8f393135de7e0cbb4bd00ba238b1e0cd324b0d90efbaee613c2f02ca5e5c", [:mix], [{:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.0", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "3971221846232021ab5e3c7489fd62ec5bfd6a2e01cae10a317ccf6fb350571c"},
   "phoenix_template": {:hex, :phoenix_template, "1.0.1", "85f79e3ad1b0180abb43f9725973e3b8c2c3354a87245f91431eec60553ed3ef", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "157dc078f6226334c91cb32c1865bf3911686f8bcd6bcff86736f6253e6993ee"},
   "phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"},
-  "plug": {:hex, :plug, "1.14.0", "ba4f558468f69cbd9f6b356d25443d0b796fbdc887e03fa89001384a9cac638f", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bf020432c7d4feb7b3af16a0c2701455cbbbb95e5b6866132cb09eb0c29adc14"},
-  "plug_cowboy": {:hex, :plug_cowboy, "2.6.0", "d1cf12ff96a1ca4f52207c5271a6c351a4733f413803488d75b70ccf44aebec2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "073cf20b753ce6682ed72905cd62a2d4bd9bad1bf9f7feb02a1b8e525bd94fa6"},
-  "plug_crypto": {:hex, :plug_crypto, "1.2.4", "34c380ef387cc7e8d537854ddd4b7096c79a4397d53587cb80419c782b03fdbc", [:mix], [], "hexpm", "4de415f03faec94d9da9be8c12cb51e9c98cbf66d732b6df669d4562d8e91acc"},
+  "plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"},
+  "plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"},
+  "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
   "plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "79fd4fcf34d110605c26560cbae8f23c603ec4158c08298bd4360fdea90bb5cf"},
   "poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"},
   "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
   "telemetry_poller": {:hex, :telemetry_poller, "0.5.1", "21071cc2e536810bac5628b935521ff3e28f0303e770951158c73eaaa01e962a", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4cab72069210bc6e7a080cec9afffad1b33370149ed5d379b81c7c5f0c663fd4"},
   "temple": {:git, "https://akkoma.dev/AkkomaGang/temple.git", "066a699ade472d8fa42a9d730b29a61af9bc8b59", [ref: "066a699ade472d8fa42a9d730b29a61af9bc8b59"]},
   "tesla": {:hex, :tesla, "1.4.4", "bb89aa0c9745190930366f6a2ac612cdf2d0e4d7fff449861baa7875afd797b2", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.3", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "d5503a49f9dec1b287567ea8712d085947e247cb11b06bc54adb05bfde466457"},
-  "timex": {:hex, :timex, "3.7.9", "790cdfc4acfce434e442f98c02ea6d84d0239073bfd668968f82ac63e9a6788d", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "64691582e5bb87130f721fc709acfb70f24405833998fabf35be968984860ce1"},
+  "timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"},
   "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"},
   "tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"},
   "ueberauth": {:hex, :ueberauth, "0.10.5", "806adb703df87e55b5615cf365e809f84c20c68aa8c08ff8a416a5a6644c4b02", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3efd1f31d490a125c7ed453b926f7c31d78b97b8a854c755f5c40064bf3ac9e1"},
diff --git a/priv/static/emoji/hehe.png b/priv/static/emoji/hehe.png
new file mode 100644 (file)
index 0000000..c02592d
Binary files /dev/null and b/priv/static/emoji/hehe.png differ
diff --git a/priv/static/emoji/nothehe.png b/priv/static/emoji/nothehe.png
new file mode 100644 (file)
index 0000000..427bb6d
Binary files /dev/null and b/priv/static/emoji/nothehe.png differ
index 8f242630ffbaecfb2d12afaea429cb7048b0a27f..ad6065b43ee28e552225c9314761c7f1621363d5 100644 (file)
@@ -54,7 +54,7 @@ defmodule Pleroma.UploadTest do
       assert result ==
                %{
                  "id" => result["id"],
-                 "name" => "image.jpg",
+                 "name" => "",
                  "type" => "Document",
                  "mediaType" => "image/jpeg",
                  "url" => [
@@ -154,19 +154,6 @@ defmodule Pleroma.UploadTest do
                  "e30397b58d226d6583ab5b8b3c5defb0c682bda5c31ef07a9f57c1c4986e3781.jpg"
     end
 
-    test "copies the file to the configured folder without deduping" do
-      File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
-
-      file = %Plug.Upload{
-        content_type: "image/jpeg",
-        path: Path.absname("test/fixtures/image_tmp.jpg"),
-        filename: "an [image.jpg"
-      }
-
-      {:ok, data} = Upload.store(file)
-      assert data["name"] == "an [image.jpg"
-    end
-
     test "fixes incorrect content type when base64 is given" do
       params = %{
         img: "data:image/png;base64,#{Base.encode64(File.read!("test/fixtures/image.jpg"))}"
@@ -184,7 +171,7 @@ defmodule Pleroma.UploadTest do
       }
 
       {:ok, data} = Upload.store(params)
-      assert String.ends_with?(data["name"], ".jpg")
+      assert String.ends_with?(List.first(data["url"])["href"], ".jpg")
     end
 
     test "copies the file to the configured folder with anonymizing filename" do
index a590946c215ea2d3781b75b76ec1028a50945923..12ccc6bf4aa3a2c207f1fa1703ceaa97f695551c 100644 (file)
@@ -2509,6 +2509,16 @@ defmodule Pleroma.UserTest do
     assert User.avatar_url(user, no_default: true) == nil
   end
 
+  test "avatar object with nil in href" do
+    user = insert(:user, avatar: %{"url" => [%{"href" => nil}]})
+    assert User.avatar_url(user) != nil
+  end
+
+  test "banner object with nil in href" do
+    user = insert(:user, banner: %{"url" => [%{"href" => nil}]})
+    assert User.banner_url(user) != nil
+  end
+
   test "get_host/1" do
     user = insert(:user, ap_id: "https://lain.com/users/lain", nickname: "lain")
     assert User.get_host(user) == "lain.com"
index 2008ebf04bc17c106205c0c05f3e196611ed122e..0d4a7ec2eb1f5fffb2ac5e61dd71d9275301e8d1 100644 (file)
@@ -662,35 +662,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
       assert_receive {:mix_shell, :info, ["https://relay.mastodon.host/actor"]}
     end
 
-    @tag capture_log: true
-    test "without valid signature, " <>
-           "it only accepts Create activities and requires enabled federation",
-         %{conn: conn} do
-      data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
-      non_create_data = File.read!("test/fixtures/mastodon-announce.json") |> Jason.decode!()
-
-      conn = put_req_header(conn, "content-type", "application/activity+json")
-
-      clear_config([:instance, :federating], false)
-
-      conn
-      |> post("/inbox", data)
-      |> json_response(403)
-
-      conn
-      |> post("/inbox", non_create_data)
-      |> json_response(403)
-
-      clear_config([:instance, :federating], true)
-
-      ret_conn = post(conn, "/inbox", data)
-      assert "ok" == json_response(ret_conn, 200)
-
-      conn
-      |> post("/inbox", non_create_data)
-      |> json_response(400)
-    end
-
     test "accepts Add/Remove activities", %{conn: conn} do
       object_id = "c61d6733-e256-4fe1-ab13-1e369789423f"
 
index 17c52fc912354a460665445a8860533c05cf8acc..b65575f01421eb684c707c2c9ce562b5df6d1bd0 100644 (file)
@@ -1303,31 +1303,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
       %{test_file: test_file}
     end
 
-    test "sets a description if given", %{test_file: file} do
-      {:ok, %Object{} = object} = ActivityPub.upload(file, description: "a cool file")
-      assert object.data["name"] == "a cool file"
-    end
-
-    test "it sets the default description depending on the configuration", %{test_file: file} do
-      clear_config([Pleroma.Upload, :default_description])
-
-      clear_config([Pleroma.Upload, :default_description], nil)
-      {:ok, %Object{} = object} = ActivityPub.upload(file)
-      assert object.data["name"] == ""
-
-      clear_config([Pleroma.Upload, :default_description], :filename)
-      {:ok, %Object{} = object} = ActivityPub.upload(file)
-      assert object.data["name"] == "an_image.jpg"
-
-      clear_config([Pleroma.Upload, :default_description], "unnamed attachment")
+    test "strips / from filename", %{test_file: file} do
+      file = %Plug.Upload{file | filename: "../../../../../nested/bad.jpg"}
       {:ok, %Object{} = object} = ActivityPub.upload(file)
-      assert object.data["name"] == "unnamed attachment"
+      [%{"href" => href}] = object.data["url"]
+      assert Regex.match?(~r"/bad.jpg$", href)
+      refute Regex.match?(~r"/nested/", href)
     end
 
-    test "copies the file to the configured folder", %{test_file: file} do
-      clear_config([Pleroma.Upload, :default_description], :filename)
-      {:ok, %Object{} = object} = ActivityPub.upload(file)
-      assert object.data["name"] == "an_image.jpg"
+    test "sets a description if given", %{test_file: file} do
+      {:ok, %Object{} = object} = ActivityPub.upload(file, description: "a cool file")
+      assert object.data["name"] == "a cool file"
     end
 
     test "works with base64 encoded images" do
index 50b9febead639757f117d1c925f6169beb8667c4..7ff8cff6bd0b9e67400986c5a3cc1fa5449745f7 100644 (file)
@@ -124,6 +124,23 @@ defmodule Pleroma.Web.MastodonAPI.MediaControllerTest do
 
       assert :ok == File.rm(Path.absname("test/tmp/large_binary.data"))
     end
+
+    test "Do not allow nested filename", %{conn: conn, image: image} do
+      image = %Plug.Upload{
+        image
+        | filename: "../../../../../nested/file.jpg"
+      }
+
+      desc = "Description of the image"
+
+      media =
+        conn
+        |> put_req_header("content-type", "multipart/form-data")
+        |> post("/api/v1/media", %{"file" => image, "description" => desc})
+        |> json_response_and_validate_schema(:ok)
+
+      refute Regex.match?(~r"/nested/", media["url"])
+    end
   end
 
   describe "Update media description" do
index aa9006681823441f93c963028078910c9a8701cd..eed12234f0afa1da216a1a1e4d67732873d6bccb 100644 (file)
@@ -408,6 +408,25 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
 
       assert [] = result
     end
+
+    test "should return 404 if disabled" do
+      clear_config([:instance, :federated_timeline_available], false)
+
+      result =
+        build_conn()
+        |> get("/api/v1/timelines/public")
+        |> json_response_and_validate_schema(404)
+
+      assert %{"error" => "Federated timeline is disabled"} = result
+    end
+
+    test "should not return 404 if local is specified" do
+      clear_config([:instance, :federated_timeline_available], false)
+
+      build_conn()
+      |> get("/api/v1/timelines/public?local=true")
+      |> json_response_and_validate_schema(200)
+    end
   end
 
   defp local_and_remote_activities do
@@ -1036,9 +1055,8 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
   end
 
   describe "bubble" do
-    setup do: oauth_access(["read:statuses"])
-
-    test "filtering", %{conn: conn, user: user} do
+    test "filtering" do
+      %{conn: conn, user: user} = oauth_access(["read:statuses"])
       clear_config([:instance, :local_bubble], [])
       # our endpoint host has a port in it so let's set the AP ID
       local_user = insert(:user, %{ap_id: "https://localhost/users/user"})
@@ -1060,7 +1078,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
 
       assert local_activity.id in one_instance
 
-      # If we have others, also include theirs 
+      # If we have others, also include theirs
       clear_config([:instance, :local_bubble], ["example.com"])
 
       two_instances =
@@ -1072,6 +1090,20 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
       assert local_activity.id in two_instances
       assert remote_activity.id in two_instances
     end
+
+    test "restrict_unauthenticated with bubble timeline", %{conn: conn} do
+      clear_config([:restrict_unauthenticated, :timelines, :bubble], true)
+
+      conn
+      |> get("/api/v1/timelines/bubble")
+      |> json_response_and_validate_schema(:unauthorized)
+
+      clear_config([:restrict_unauthenticated, :timelines, :bubble], false)
+
+      conn
+      |> get("/api/v1/timelines/bubble")
+      |> json_response_and_validate_schema(200)
+    end
   end
 
   defp create_remote_activity(user) do
index e9b8825bfb7801f7c5507175ecb2fb9b82dbbf31..4aec31eacd07088edde803686b2ea9c86c7c3a8a 100644 (file)
@@ -396,6 +396,34 @@ defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do
       assert :ok == File.rm(Path.absname("test/tmp/large_binary.data"))
     end
 
+    test "Strip / from upload files", %{user: user, conn: conn} do
+      new_image = %Plug.Upload{
+        content_type: "image/jpeg",
+        path: Path.absname("test/fixtures/image.jpg"),
+        filename: "../../../../nested/an_image.jpg"
+      }
+
+      assert user.avatar == %{}
+
+      res =
+        patch(conn, "/api/v1/accounts/update_credentials", %{
+          "avatar" => new_image,
+          "header" => new_image,
+          "pleroma_background_image" => new_image
+        })
+
+      assert user_response = json_response_and_validate_schema(res, 200)
+      assert user_response["avatar"]
+      assert user_response["header"]
+      assert user_response["pleroma"]["background_image"]
+      refute Regex.match?(~r"/nested/", user_response["avatar"])
+      refute Regex.match?(~r"/nested/", user_response["header"])
+      refute Regex.match?(~r"/nested/", user_response["pleroma"]["background_image"])
+
+      user = User.get_by_id(user.id)
+      refute user.avatar == %{}
+    end
+
     test "requires 'write:accounts' permission" do
       token1 = insert(:oauth_token, scopes: ["read"])
       token2 = insert(:oauth_token, scopes: ["write", "follow"])
index c9036d67d225165ddde7bc08572412c3be162b0d..6ef89f7998136e137b436f1a9e76ed6ef360b3d6 100644 (file)
@@ -269,8 +269,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
     }
 
     with_mock(
-      Pleroma.Web.Nodeinfo.NodeinfoController,
-      raw_nodeinfo: fn -> %{version: "2.0"} end
+      Pleroma.Web.Nodeinfo.Nodeinfo,
+      get_nodeinfo: fn _ -> %{version: "2.0"} end
     ) do
       assert expected ==
                AccountView.render("show.json", %{user: user, skip_visibility_check: true})
index 05a078266c1d41d950f09653a4e37a0fa10de9e1..ff14db4607f50bce7603024afbfd6a406d6f615a 100644 (file)
@@ -292,4 +292,38 @@ defmodule Pleroma.Web.NodeInfoTest do
       assert response["metadata"]["federation"]["mrf_simple_info"] == expected_config
     end
   end
+
+  describe "public timeline visibility" do
+    test "shows public timeline visibility", %{conn: conn} do
+      clear_config([:restrict_unauthenticated, :timelines], %{local: false, federated: false})
+
+      response =
+        conn
+        |> get("/nodeinfo/2.1.json")
+        |> json_response(:ok)
+
+      assert response["metadata"]["publicTimelineVisibility"]["local"] == true
+      assert response["metadata"]["publicTimelineVisibility"]["federated"] == true
+
+      clear_config([:restrict_unauthenticated, :timelines], %{local: true, federated: false})
+
+      response =
+        conn
+        |> get("/nodeinfo/2.1.json")
+        |> json_response(:ok)
+
+      assert response["metadata"]["publicTimelineVisibility"]["local"] == false
+      assert response["metadata"]["publicTimelineVisibility"]["federated"] == true
+
+      clear_config([:restrict_unauthenticated, :timelines], %{local: false, federated: true})
+
+      response =
+        conn
+        |> get("/nodeinfo/2.1.json")
+        |> json_response(:ok)
+
+      assert response["metadata"]["publicTimelineVisibility"]["local"] == true
+      assert response["metadata"]["publicTimelineVisibility"]["federated"] == false
+    end
+  end
 end
index 66e6ba4ca68203c19355d70f3314b11c69ad1343..815e888ee995445b4a1097817d65bfd7cb1dd390 100644 (file)
@@ -83,6 +83,7 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do
       "main",
       "ostatus_subscribe",
       "oauth",
+      "akkoma",
       "objects",
       "activities",
       "notice",
index 21c574ba3349e173f27417ca6484119446ea4315..c42b82810613fd65293c8666adbbad0dbda7cbad 100644 (file)
@@ -69,6 +69,47 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlugTest do
     assert %{valid_signature: false} == conn.assigns
   end
 
+  test "allowlist federation: it considers a mapped identity to be valid when the associated instance is allowed" do
+    clear_config([:activitypub, :authorized_fetch_mode], true)
+
+    clear_config([:mrf_simple, :accept], [
+      {"mastodon.example.org", "anime is allowed"}
+    ])
+
+    on_exit(fn ->
+      Pleroma.Config.put([:activitypub, :authorized_fetch_mode], false)
+      Pleroma.Config.put([:mrf_simple, :accept], [])
+    end)
+
+    conn =
+      build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"})
+      |> set_signature("http://mastodon.example.org/users/admin")
+      |> MappedSignatureToIdentityPlug.call(%{})
+
+    assert conn.assigns[:valid_signature]
+    refute is_nil(conn.assigns.user)
+  end
+
+  test "allowlist federation: it considers a mapped identity to be invalid when the associated instance is not allowed" do
+    clear_config([:activitypub, :authorized_fetch_mode], true)
+
+    clear_config([:mrf_simple, :accept], [
+      {"misskey.example.org", "anime is allowed"}
+    ])
+
+    on_exit(fn ->
+      Pleroma.Config.put([:activitypub, :authorized_fetch_mode], false)
+      Pleroma.Config.put([:mrf_simple, :accept], [])
+    end)
+
+    conn =
+      build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"})
+      |> set_signature("http://mastodon.example.org/users/admin")
+      |> MappedSignatureToIdentityPlug.call(%{})
+
+    assert %{valid_signature: false} == conn.assigns
+  end
+
   @tag skip: "known breakage; the testsuite presently depends on it"
   test "it considers a mapped identity to be invalid when the identity cannot be found" do
     conn =