From c44cf7d74f513397a9b25e7f7e0fe6092d679840 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Wed, 29 May 2024 06:24:24 +0200 Subject: [PATCH 001/391] StatusOperation: Remove obsolete card_operation --- .../api_spec/operations/status_operation.ex | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex index 1717c68c8..f7914406a 100644 --- a/lib/pleroma/web/api_spec/operations/status_operation.ex +++ b/lib/pleroma/web/api_spec/operations/status_operation.ex @@ -339,34 +339,6 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do } end - def card_operation do - %Operation{ - tags: ["Retrieve status information"], - deprecated: true, - summary: "Preview card", - description: "Deprecated in favor of card property inlined on Status entity", - operationId: "StatusController.card", - parameters: [id_param()], - security: [%{"oAuth" => ["read:statuses"]}], - responses: %{ - 200 => - Operation.response("Card", "application/json", %Schema{ - type: :object, - nullable: true, - properties: %{ - type: %Schema{type: :string, enum: ["link", "photo", "video", "rich"]}, - provider_name: %Schema{type: :string, nullable: true}, - provider_url: %Schema{type: :string, format: :uri}, - url: %Schema{type: :string, format: :uri}, - image: %Schema{type: :string, nullable: true, format: :uri}, - title: %Schema{type: :string}, - description: %Schema{type: :string} - } - }) - } - } - end - def favourited_by_operation do %Operation{ tags: ["Retrieve status information"], From d860d5059703bfbcd3550a8ff8361d83ec06d00d Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Wed, 29 May 2024 06:27:11 +0200 Subject: [PATCH 002/391] StatusController: Restore preview cards tests This is a partial revert of 19002fd6c11760898daf0b5ed648d6ba58d84b97 Subsequent commit updating it to cards embedded in statuses required to pass tests. --- .../controllers/status_controller_test.exs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs index f34911e5b..0386a5b7a 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -1653,6 +1653,63 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do end end + describe "cards" do + setup do + Pleroma.StaticStubbedConfigMock + |> stub(:get, fn + [:rich_media, :enabled] -> true + path -> Pleroma.Test.StaticConfig.get(path) + end) + + oauth_access(["read:statuses"]) + end + + test "returns rich-media card", %{conn: conn, user: user} do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + + {:ok, activity} = CommonAPI.post(user, %{status: "https://example.com/ogp"}) + + card_data = %{ + "image" => "http://ia.media-imdb.com/images/rock.jpg", + "provider_name" => "example.com", + "provider_url" => "https://example.com", + "title" => "The Rock", + "type" => "link", + "url" => "https://example.com/ogp", + "description" => + "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.", + "pleroma" => %{ + "opengraph" => %{ + "image" => "http://ia.media-imdb.com/images/rock.jpg", + "title" => "The Rock", + "type" => "video.movie", + "url" => "https://example.com/ogp", + "description" => + "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer." + } + } + } + + response = + conn + |> get("/api/v1/statuses/#{activity.id}/card") + |> json_response_and_validate_schema(200) + + assert response == card_data + + # works with private posts + {:ok, activity} = + CommonAPI.post(user, %{status: "https://example.com/ogp", visibility: "direct"}) + + response_two = + conn + |> get("/api/v1/statuses/#{activity.id}/card") + |> json_response_and_validate_schema(200) + + assert response_two == card_data + end + end + test "bookmarks" do bookmarks_uri = "/api/v1/bookmarks" From c7119d17d874ad15291decc29ce1147bbdf78488 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Wed, 29 May 2024 06:24:50 +0200 Subject: [PATCH 003/391] StatusControllerTest: Update preview cards tests to current API --- .../mastodon_api/controllers/status_controller_test.exs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs index 0386a5b7a..08589f6ae 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -1692,10 +1692,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do response = conn - |> get("/api/v1/statuses/#{activity.id}/card") + |> get("/api/v1/statuses/#{activity.id}") |> json_response_and_validate_schema(200) - assert response == card_data + assert response["card"] == card_data # works with private posts {:ok, activity} = @@ -1703,10 +1703,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do response_two = conn - |> get("/api/v1/statuses/#{activity.id}/card") + |> get("/api/v1/statuses/#{activity.id}") |> json_response_and_validate_schema(200) - assert response_two == card_data + assert response_two["card"] == card_data end end From 656dce96dc3d4ceb2ac2fed1badc0f2baa290c7f Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Thu, 15 Feb 2024 00:57:34 +0100 Subject: [PATCH 004/391] Status: Fill all MastoAPI required values in preview cards --- changelog.d/previewcard-void-fill.fix | 1 + lib/pleroma/web/api_spec/schemas/status.ex | 49 +++++++++++++++++-- .../web/mastodon_api/views/status_view.ex | 7 +++ .../controllers/status_controller_test.exs | 44 ++++++++++++++++- 4 files changed, 95 insertions(+), 6 deletions(-) create mode 100644 changelog.d/previewcard-void-fill.fix diff --git a/changelog.d/previewcard-void-fill.fix b/changelog.d/previewcard-void-fill.fix new file mode 100644 index 000000000..04e571347 --- /dev/null +++ b/changelog.d/previewcard-void-fill.fix @@ -0,0 +1 @@ +Status: Fill all MastoAPI required values in preview cards diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex index 6e537b5da..1432be264 100644 --- a/lib/pleroma/web/api_spec/schemas/status.ex +++ b/lib/pleroma/web/api_spec/schemas/status.ex @@ -34,16 +34,40 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do type: :object, nullable: true, description: "Preview card for links included within status content", - required: [:url, :title, :description, :type], + # Yeah, it's effectivelly all required in MastoAPI so far + required: [ + :url, + :title, + :description, + :type, + :author_name, + :author_url, + :provider_name, + :provider_url, + :html, + :width, + :height, + :image, + :embed_url, + :blurhash + ], properties: %{ + url: %Schema{type: :string, format: :uri, description: "Location of linked resource"}, + title: %Schema{type: :string, description: "Title of linked resource"}, + description: %Schema{type: :string, description: "Description of preview"}, type: %Schema{ type: :string, enum: ["link", "photo", "video", "rich"], description: "The type of the preview card" }, + author_name: %Schema{type: :string, description: "author of the original resource"}, + author_url: %Schema{ + type: :string, + format: :uri, + description: "link to the author of the original resource" + }, provider_name: %Schema{ type: :string, - nullable: true, description: "The provider of the original resource" }, provider_url: %Schema{ @@ -51,7 +75,13 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do format: :uri, description: "A link to the provider of the original resource" }, - url: %Schema{type: :string, format: :uri, description: "Location of linked resource"}, + html: %Schema{ + type: :string, + format: :html, + description: "HTML to be used for generating the preview card" + }, + width: %Schema{type: :integer, description: "Width of preview, in pixels"}, + height: %Schema{type: :integer, description: "Height of preview, in pixels"}, image: %Schema{ type: :string, nullable: true, @@ -62,8 +92,17 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do type: :string, description: "Alternate text that describes what is in the thumbnail" }, - title: %Schema{type: :string, description: "Title of linked resource"}, - description: %Schema{type: :string, description: "Description of preview"} + embed_url: %Schema{ + type: :string, + format: :uri, + description: "Used for photo embeds, instead of custom `html`" + }, + blurhash: %Schema{ + type: :string, + nullable: true, + description: + "A hash computed by the (BlurHash algorithm)[https://github.com/woltapp/blurhash], for generating colorful preview thumbnails when media has not been downloaded yet." + } } }, content: %Schema{type: :string, format: :html, description: "HTML-encoded status content"}, diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 0c16749a4..7a654381d 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -586,6 +586,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do image_description: rich_media["image:alt"] || "", title: rich_media["title"] || "", description: rich_media["description"] || "", + author_name: "", + author_url: "", + html: "", + width: 0, + height: 0, + embed_url: "", + blurhash: nil, pleroma: %{ opengraph: rich_media diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs index 08589f6ae..725356edc 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -1687,7 +1687,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do "description" => "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer." } - } + }, + "author_name" => "", + "author_url" => "", + "blurhash" => nil, + "embed_url" => "", + "height" => 0, + "html" => "", + "width" => 0 } response = @@ -1708,6 +1715,41 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do assert response_two["card"] == card_data end + + test "replaces missing description with an empty string", %{conn: conn, user: user} do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + + {:ok, activity} = CommonAPI.post(user, %{status: "https://example.com/ogp-missing-data"}) + + response = + conn + |> get("/api/v1/statuses/#{activity.id}") + |> json_response_and_validate_schema(:ok) + + assert response["card"] == %{ + "type" => "link", + "title" => "Pleroma", + "description" => "", + "image" => nil, + "provider_name" => "example.com", + "provider_url" => "https://example.com", + "url" => "https://example.com/ogp-missing-data", + "pleroma" => %{ + "opengraph" => %{ + "title" => "Pleroma", + "type" => "website", + "url" => "https://example.com/ogp-missing-data" + } + }, + "author_name" => "", + "author_url" => "", + "blurhash" => nil, + "embed_url" => "", + "height" => 0, + "html" => "", + "width" => 0 + } + end end test "bookmarks" do From 9ce615d3d58eebe59dd7279ae5c7ccf9dcc73f82 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Thu, 19 Dec 2024 21:48:48 +0100 Subject: [PATCH 005/391] =?UTF-8?q?rel/=E2=80=A6/init.d/pleroma:=20tab=20i?= =?UTF-8?q?ndent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rel/files/installation/init.d/pleroma | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rel/files/installation/init.d/pleroma b/rel/files/installation/init.d/pleroma index ca5b842e1..ed8a27830 100755 --- a/rel/files/installation/init.d/pleroma +++ b/rel/files/installation/init.d/pleroma @@ -17,6 +17,6 @@ retry="SIGTERM/30/SIGKILL/5" pidfile="/var/run/pleroma.pid" depend() { - want nginx - need postgresql + want nginx + need postgresql } From a1247220f35b5d42d2c33dfeacb3c0c0ebc8c279 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Thu, 19 Dec 2024 21:49:12 +0100 Subject: [PATCH 006/391] =?UTF-8?q?rel/=E2=80=A6/init.d/pleroma:=20set=20r?= =?UTF-8?q?espawn=5Fmax=20&=20respawn=5Fperiod?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rel/files/installation/init.d/pleroma | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rel/files/installation/init.d/pleroma b/rel/files/installation/init.d/pleroma index ed8a27830..4e3c20575 100755 --- a/rel/files/installation/init.d/pleroma +++ b/rel/files/installation/init.d/pleroma @@ -16,6 +16,10 @@ retry="SIGTERM/30/SIGKILL/5" pidfile="/var/run/pleroma.pid" +# Needs OpenRC >= 0.42 +respawn_max=200 +respawn_period=86400 # 1*day + depend() { want nginx need postgresql From 9bbeeeb428272873063d0eacd33112be09f4d6bd Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Thu, 19 Dec 2024 21:49:32 +0100 Subject: [PATCH 007/391] */init.d/pleroma: run migrations before starting --- changelog.d/opt-init.d-migrations.change | 1 + installation/init.d/pleroma | 6 ++++++ rel/files/installation/init.d/pleroma | 6 ++++++ 3 files changed, 13 insertions(+) create mode 100644 changelog.d/opt-init.d-migrations.change diff --git a/changelog.d/opt-init.d-migrations.change b/changelog.d/opt-init.d-migrations.change new file mode 100644 index 000000000..c53f2baa0 --- /dev/null +++ b/changelog.d/opt-init.d-migrations.change @@ -0,0 +1 @@ +- OpenRC: Automatically run migrations before starting pleroma diff --git a/installation/init.d/pleroma b/installation/init.d/pleroma index cb6635a0b..2cd366140 100755 --- a/installation/init.d/pleroma +++ b/installation/init.d/pleroma @@ -37,6 +37,12 @@ depend() { need nginx postgresql } +start_pre() { + ebegin "running migrations if any" + su -s /bin/sh -c '/usr/bin/mix ecto.migrate' -l "${command_user}" + eend "$?" +} + healthcheck() { # put pleroma_health=YES in /etc/conf.d/pleroma if you want healthchecking # and make sure you have curl installed diff --git a/rel/files/installation/init.d/pleroma b/rel/files/installation/init.d/pleroma index 4e3c20575..0e117e9b8 100755 --- a/rel/files/installation/init.d/pleroma +++ b/rel/files/installation/init.d/pleroma @@ -24,3 +24,9 @@ depend() { want nginx need postgresql } + +start_pre() { + ebegin "running migrations if any" + su -s /bin/sh -c '/opt/pleroma/bin/pleroma_ctl migrate' -l "${command_user}" + eend "$?" +} From c3c14f5860051d0311ae34900ef0181ada5c630d Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Tue, 21 Jan 2025 14:36:50 +0400 Subject: [PATCH 008/391] OAuth App: Fix test, optimize code. --- .../optimize-orphaned-apps-cleanup.change | 1 + lib/pleroma/web/o_auth/app.ex | 14 ++++--- test/pleroma/web/o_auth/app_test.exs | 42 ++++++++++++++++--- 3 files changed, 46 insertions(+), 11 deletions(-) create mode 100644 changelog.d/optimize-orphaned-apps-cleanup.change diff --git a/changelog.d/optimize-orphaned-apps-cleanup.change b/changelog.d/optimize-orphaned-apps-cleanup.change new file mode 100644 index 000000000..e86b75524 --- /dev/null +++ b/changelog.d/optimize-orphaned-apps-cleanup.change @@ -0,0 +1 @@ +- Optimize orphaned OAuth apps cleanup by using a single delete query instead of deleting records one by one. diff --git a/lib/pleroma/web/o_auth/app.ex b/lib/pleroma/web/o_auth/app.ex index 7661c2566..30a9d9134 100644 --- a/lib/pleroma/web/o_auth/app.ex +++ b/lib/pleroma/web/o_auth/app.ex @@ -168,16 +168,20 @@ defmodule Pleroma.Web.OAuth.App do @spec remove_orphans(pos_integer()) :: :ok def remove_orphans(limit \\ 100) do - fifteen_mins_ago = DateTime.add(DateTime.utc_now(), -900, :second) + fifteen_mins_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -900) - Repo.transaction(fn -> + # First get the IDs of apps to delete + app_ids = from(a in __MODULE__, where: is_nil(a.user_id) and a.inserted_at < ^fifteen_mins_ago, - limit: ^limit + limit: ^limit, + select: a.id ) |> Repo.all() - |> Enum.each(&Repo.delete(&1)) - end) + + # Then delete those specific apps + from(a in __MODULE__, where: a.id in ^app_ids) + |> Repo.delete_all() :ok end diff --git a/test/pleroma/web/o_auth/app_test.exs b/test/pleroma/web/o_auth/app_test.exs index 44219cf90..871b10ac3 100644 --- a/test/pleroma/web/o_auth/app_test.exs +++ b/test/pleroma/web/o_auth/app_test.exs @@ -55,19 +55,49 @@ defmodule Pleroma.Web.OAuth.AppTest do end test "removes orphaned apps" do + # Create an orphaned app (no user_id) attrs = %{client_name: "Mastodon-Local", redirect_uris: "."} {:ok, %App{} = old_app} = App.get_or_make(attrs, ["write"]) - attrs = %{client_name: "PleromaFE", redirect_uris: "."} - {:ok, %App{} = app} = App.get_or_make(attrs, ["write"]) + # Create a non-orphaned app with a user + user = insert(:user) + attrs = %{client_name: "PleromaFE", redirect_uris: ".", user_id: user.id} + {:ok, %App{} = kept_app} = App.get_or_make(attrs, ["write"]) + + # Create an old but non-orphaned app + attrs = %{client_name: "OldButValid", redirect_uris: ".", user_id: user.id} + {:ok, %App{} = old_kept_app} = App.get_or_make(attrs, ["write"]) + + # backdate both old apps so they're within the threshold for being cleaned up + # 1 hour ago + past_time = NaiveDateTime.add(NaiveDateTime.utc_now(), -3600) - # backdate the old app so it's within the threshold for being cleaned up {:ok, _} = - "UPDATE apps SET inserted_at = now() - interval '1 hour' WHERE id = #{old_app.id}" - |> Pleroma.Repo.query() + Repo.query( + "UPDATE apps SET inserted_at = $1 WHERE id IN ($2, $3)", + [past_time, old_app.id, old_kept_app.id] + ) + + # Ensure the updates were applied + updated_app = Repo.get(App, old_app.id) + updated_kept_app = Repo.get(App, old_kept_app.id) + + assert NaiveDateTime.compare( + updated_app.inserted_at, + NaiveDateTime.add(NaiveDateTime.utc_now(), -900) + ) == :lt + + assert NaiveDateTime.compare( + updated_kept_app.inserted_at, + NaiveDateTime.add(NaiveDateTime.utc_now(), -900) + ) == :lt App.remove_orphans() - assert [app] == Pleroma.Repo.all(App) + # Verify the orphaned app was removed + assert is_nil(Repo.get(App, old_app.id)) + # Verify both non-orphaned apps still exist, regardless of age + assert Repo.get(App, kept_app.id) + assert Repo.get(App, old_kept_app.id) end end From 9d7bdab7a2d9808e8c6dc77531f99d653364d61a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 19 Sep 2025 17:07:09 +0200 Subject: [PATCH 009/391] Mastodon-compatible webhooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/webhooks.add | 1 + config/config.exs | 3 +- config/description.exs | 20 ++ docs/development/API/admin_api.md | 89 ++++++++ lib/pleroma/application.ex | 3 +- lib/pleroma/user.ex | 2 + lib/pleroma/web/activity_pub/activity_pub.ex | 5 +- .../controllers/webhook_controller.ex | 88 ++++++++ .../web/admin_api/views/webhook_view.ex | 33 +++ lib/pleroma/web/api_spec.ex | 3 +- .../operations/admin/webhook_operation.ex | 193 ++++++++++++++++++ .../operations/notification_operation.ex | 1 - .../mastodon_api/admin/views/account_view.ex | 65 ++++++ .../mastodon_api/admin/views/report_view.ex | 54 +++++ lib/pleroma/web/router.ex | 9 + lib/pleroma/webhook.ex | 100 +++++++++ lib/pleroma/webhook/notify.ex | 83 ++++++++ lib/pleroma/workers/webhook_worker.ex | 41 ++++ .../20220624104914_create_webhooks.exs | 20 ++ test/mix/tasks/pleroma/database_test.exs | 3 +- test/pleroma/user_test.exs | 10 + .../web/activity_pub/activity_pub_test.exs | 24 +++ .../controllers/webhook_controller_test.exs | 84 ++++++++ test/pleroma/webhook/notify_test.exs | 75 +++++++ test/pleroma/webhook_test.exs | 56 +++++ 25 files changed, 1059 insertions(+), 6 deletions(-) create mode 100644 changelog.d/webhooks.add create mode 100644 lib/pleroma/web/admin_api/controllers/webhook_controller.ex create mode 100644 lib/pleroma/web/admin_api/views/webhook_view.ex create mode 100644 lib/pleroma/web/api_spec/operations/admin/webhook_operation.ex create mode 100644 lib/pleroma/web/mastodon_api/admin/views/account_view.ex create mode 100644 lib/pleroma/web/mastodon_api/admin/views/report_view.ex create mode 100644 lib/pleroma/webhook.ex create mode 100644 lib/pleroma/webhook/notify.ex create mode 100644 lib/pleroma/workers/webhook_worker.ex create mode 100644 priv/repo/migrations/20220624104914_create_webhooks.exs create mode 100644 test/pleroma/web/admin_api/controllers/webhook_controller_test.exs create mode 100644 test/pleroma/webhook/notify_test.exs create mode 100644 test/pleroma/webhook_test.exs diff --git a/changelog.d/webhooks.add b/changelog.d/webhooks.add new file mode 100644 index 000000000..323428f9f --- /dev/null +++ b/changelog.d/webhooks.add @@ -0,0 +1 @@ +Add support for Mastodon-compatible webhooks \ No newline at end of file diff --git a/config/config.exs b/config/config.exs index 683805fe3..b01f1ffcc 100644 --- a/config/config.exs +++ b/config/config.exs @@ -926,7 +926,8 @@ config :pleroma, Pleroma.User.Backup, timeout: :timer.minutes(30) config :pleroma, ConcurrentLimiter, [ - {Pleroma.Search, [max_running: 30, max_waiting: 50]} + {Pleroma.Search, [max_running: 30, max_waiting: 50]}, + {Pleroma.Webhook.Notify, [max_running: 5, max_waiting: 200]} ] config :pleroma, Pleroma.Web.WebFinger, domain: nil, update_nickname_on_user_fetch: true diff --git a/config/description.exs b/config/description.exs index c61a344e8..764137674 100644 --- a/config/description.exs +++ b/config/description.exs @@ -3479,6 +3479,26 @@ config :pleroma, :config_description, [ suggestion: [5] } ] + }, + %{ + key: Pleroma.Webhook.Notify, + type: :keyword, + description: "Concurrent limits configuration for webhooks.", + suggestions: [max_running: 5, max_waiting: 5], + children: [ + %{ + key: :max_running, + type: :integer, + description: "Max running concurrently jobs.", + suggestion: [5] + }, + %{ + key: :max_waiting, + type: :integer, + description: "Max waiting jobs.", + suggestion: [5] + } + ] } ] }, diff --git a/docs/development/API/admin_api.md b/docs/development/API/admin_api.md index 64c06ca2b..ee82c077d 100644 --- a/docs/development/API/admin_api.md +++ b/docs/development/API/admin_api.md @@ -1806,3 +1806,92 @@ Note that this differs from the Mastodon API variant: Mastodon API only returns ```json {} ``` + +## `GET /api/v1/pleroma/admin/webhooks` + +### List webhooks + +- Method: `GET` +- Response: + +```json +[ + { + "enabled": true, + "id": "2", + "events": ["account.created"], + "url": "https://webhook.example/", + "secret": "eb85d4ccd8510e78f912743949dc354e8146987d", + "updated_at": "2022-10-29T17:44:16.000Z", + "created_at": "2022-10-29T17:44:13.000Z" + } +] +``` + +## `GET /api/v1/pleroma/admin/webhooks/:id` + +### Get an individual webhook + +- Method: `GET` +- Params: + - `id`: **string** Webhook ID +- Response: A webhook + +## `POST /api/v1/pleroma/admin/webhooks` + +### Create a webhook + +- Method: `POST` +- Params: + - `url`: **string** Webhook URL + - *optional* `events`: **[string]** Types of events to trigger on (`account.created`, `report.created`) + - *optional* `enabled`: **boolean** Whether webhook is enabled +- Response: A webhook + +## `PATCH /api/v1/pleroma/admin/webhooks/:id` + +### Update a webhook + +- Method: `PATCH` +- Params: + - `id`: **string** Webhook ID + - *optional* `url`: **string** Webhook URL + - *optional* `events`: **[string]** Types of events to trigger on (`account.created`, `report.created`) + - *optional* `enabled`: **boolean** Whether webhook is enabled +- Response: A webhook + +## `DELETE /api/v1/pleroma/admin/webhooks/:id` + +### Delete a webhook + +- Method: `DELETE` +- Params: + - `id`: **string** Webhook ID +- Response: A webhook + +## `POST /api/v1/pleroma/admin/webhooks/:id/enable` + +### Activate a webhook + +- Method: `POST` +- Params: + - `id`: **string** Webhook ID +- Response: A webhook + +## `POST /api/v1/pleroma/admin/webhooks/:id/disable` + +### Deactivate a webhook + +- Method: `POST` +- Params: + - `id`: **string** Webhook ID +- Response: A webhook + +## `POST /api/v1/pleroma/admin/webhooks/:id/rotate_secret` + +### Rotate webhook signing secret + +- Method: `POST` +- Params: + - `id`: **string** Webhook ID +- Response: A webhook diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 8e1c5de0d..171884639 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -278,7 +278,8 @@ defmodule Pleroma.Application do config = Config.get(ConcurrentLimiter, []) [ - Pleroma.Search + Pleroma.Search, + Pleroma.Webhook.Notify ] |> Enum.each(fn module -> mod_config = Keyword.get(config, module, []) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 468e124b5..f9f5c15f7 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -39,6 +39,7 @@ defmodule Pleroma.User do alias Pleroma.Web.Endpoint alias Pleroma.Web.OAuth alias Pleroma.Web.RelMe + alias Pleroma.Webhook.Notify alias Pleroma.Workers.BackgroundWorker alias Pleroma.Workers.DeleteWorker alias Pleroma.Workers.UserRefreshWorker @@ -980,6 +981,7 @@ defmodule Pleroma.User do @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)" def register(%Ecto.Changeset{} = changeset) do with {:ok, user} <- Repo.insert(changeset) do + Notify.trigger_webhooks(user, :"account.created") post_register_action(user) end end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 3e5239a28..deaf249eb 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -30,6 +30,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do import Ecto.Query import Pleroma.Web.ActivityPub.Utils import Pleroma.Web.ActivityPub.Visibility + import Pleroma.Webhook.Notify, only: [trigger_webhooks: 2] require Logger require Pleroma.Constants @@ -415,7 +416,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do with flag_data <- make_flag_data(params, additional), {:ok, activity} <- insert(flag_data, local), _ <- notify_and_stream(activity), - :ok <- maybe_federate(activity) do + _ <- trigger_webhooks(activity, :"report.created"), + :ok <- + maybe_federate(activity) do User.all_users_with_privilege(:reports_manage_reports) |> Enum.filter(fn user -> user.ap_id != actor end) |> Enum.filter(fn user -> not is_nil(user.email) end) diff --git a/lib/pleroma/web/admin_api/controllers/webhook_controller.ex b/lib/pleroma/web/admin_api/controllers/webhook_controller.ex new file mode 100644 index 000000000..8a6b0de7a --- /dev/null +++ b/lib/pleroma/web/admin_api/controllers/webhook_controller.ex @@ -0,0 +1,88 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.WebhookController do + use Pleroma.Web, :controller + + alias Pleroma.Repo + alias Pleroma.Web.Plugs.OAuthScopesPlug + alias Pleroma.Webhook + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + + plug( + OAuthScopesPlug, + %{scopes: ["admin:write"]} + when action in [:update, :create, :delete, :enable, :disable, :rotate_secret] + ) + + plug(OAuthScopesPlug, %{scopes: ["admin:read"]} when action in [:index, :show]) + + action_fallback(Pleroma.Web.AdminAPI.FallbackController) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.WebhookOperation + + def index(conn, _) do + webhooks = + Webhook + |> Repo.all() + + render(conn, "index.json", webhooks: webhooks) + end + + def show(conn, %{id: id}) do + with %Webhook{} = webhook <- Webhook.get(id) do + render(conn, "show.json", webhook: webhook) + else + nil -> {:error, :not_found} + end + end + + def create(%{body_params: params} = conn, _) do + with webhook <- Webhook.create(params) do + render(conn, "show.json", webhook: webhook) + end + end + + def update(%{body_params: params} = conn, %{id: id}) do + with %Webhook{} = webhook <- Webhook.get(id), + webhook <- Webhook.update(webhook, params) do + render(conn, "show.json", webhook: webhook) + end + end + + def delete(conn, %{id: id}) do + with %Webhook{} = webhook <- Webhook.get(id), + {:ok, webhook} <- Webhook.delete(webhook) do + render(conn, "show.json", webhook: webhook) + end + end + + def enable(conn, %{id: id}) do + with %Webhook{} = webhook <- Webhook.get(id), + {:ok, webhook} <- Webhook.set_enabled(webhook, true) do + render(conn, "show.json", webhook: webhook) + else + nil -> {:error, :not_found} + end + end + + def disable(conn, %{id: id}) do + with %Webhook{} = webhook <- Webhook.get(id), + {:ok, webhook} <- Webhook.set_enabled(webhook, false) do + render(conn, "show.json", webhook: webhook) + else + nil -> {:error, :not_found} + end + end + + def rotate_secret(conn, %{id: id}) do + with %Webhook{} = webhook <- Webhook.get(id), + {:ok, webhook} <- Webhook.rotate_secret(webhook) do + render(conn, "show.json", webhook: webhook) + else + nil -> {:error, :not_found} + end + end +end diff --git a/lib/pleroma/web/admin_api/views/webhook_view.ex b/lib/pleroma/web/admin_api/views/webhook_view.ex new file mode 100644 index 000000000..725183029 --- /dev/null +++ b/lib/pleroma/web/admin_api/views/webhook_view.ex @@ -0,0 +1,33 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.WebhookView do + use Pleroma.Web, :view + + alias Pleroma.Web.CommonAPI.Utils + + def render("index.json", %{webhooks: webhooks}) do + render_many(webhooks, __MODULE__, "show.json") + end + + def render("show.json", %{webhook: webhook}) do + %{ + id: webhook.id |> to_string(), + url: webhook.url, + events: webhook.events, + secret: webhook.secret, + enabled: webhook.enabled, + created_at: Utils.to_masto_date(webhook.inserted_at), + updated_at: Utils.to_masto_date(webhook.updated_at) + } + end + + def render("event.json", %{type: type, object: object}) do + %{ + type: type, + created_at: Utils.to_masto_date(NaiveDateTime.utc_now()), + object: object + } + end +end diff --git a/lib/pleroma/web/api_spec.ex b/lib/pleroma/web/api_spec.ex index 3e0ac3704..71d9f08f7 100644 --- a/lib/pleroma/web/api_spec.ex +++ b/lib/pleroma/web/api_spec.ex @@ -105,7 +105,8 @@ defmodule Pleroma.Web.ApiSpec do "Report management", "Status administration", "User administration", - "Announcement management" + "Announcement management", + "Webhooks" ] }, %{"name" => "Applications", "tags" => ["Applications", "Push subscriptions"]}, diff --git a/lib/pleroma/web/api_spec/operations/admin/webhook_operation.ex b/lib/pleroma/web/api_spec/operations/admin/webhook_operation.ex new file mode 100644 index 000000000..c83caa555 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/admin/webhook_operation.ex @@ -0,0 +1,193 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Admin.WebhookOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def index_operation do + %Operation{ + tags: ["Webhooks"], + summary: "Retrieve a list of webhooks", + operationId: "AdminAPI.WebhookController.index", + security: [%{"oAuth" => ["admin:show"]}], + responses: %{ + 200 => + Operation.response("Array of webhooks", "application/json", %Schema{ + type: :array, + items: webhook() + }) + } + } + end + + def show_operation do + %Operation{ + tags: ["Webhooks"], + summary: "Retrieve a webhook", + operationId: "AdminAPI.WebhookController.show", + security: [%{"oAuth" => ["admin:show"]}], + parameters: [id_param()], + responses: %{ + 200 => Operation.response("Webhook", "application/json", webhook()) + } + } + end + + def create_operation do + %Operation{ + tags: ["Webhooks"], + summary: "Create a webhook", + operationId: "AdminAPI.WebhookController.create", + security: [%{"oAuth" => ["admin:write"]}], + requestBody: + request_body( + "Parameters", + %Schema{ + description: "POST body for creating a webhook", + type: :object, + properties: %{ + url: %Schema{type: :string, format: :uri, required: true}, + events: event_type(true), + enabled: %Schema{type: :boolean} + } + } + ), + responses: %{ + 200 => Operation.response("Webhook", "application/json", webhook()) + } + } + end + + def update_operation do + %Operation{ + tags: ["Webhooks"], + summary: "Update a webhook", + operationId: "AdminAPI.WebhookController.update", + security: [%{"oAuth" => ["admin:write"]}], + parameters: [id_param()], + requestBody: + request_body( + "Parameters", + %Schema{ + description: "POST body for updating a webhook", + type: :object, + properties: %{ + url: %Schema{type: :string, format: :uri}, + events: event_type(), + enabled: %Schema{type: :boolean} + } + } + ), + responses: %{ + 200 => Operation.response("Webhook", "application/json", webhook()) + } + } + end + + def delete_operation do + %Operation{ + tags: ["Webhooks"], + summary: "Delete a webhook", + operationId: "AdminAPI.WebhookController.delete", + security: [%{"oAuth" => ["admin:write"]}], + parameters: [id_param()], + responses: %{ + 200 => Operation.response("Webhook", "application/json", webhook()) + } + } + end + + def enable_operation do + %Operation{ + tags: ["Webhooks"], + summary: "Enable a webhook", + operationId: "AdminAPI.WebhookController.enable", + security: [%{"oAuth" => ["admin:write"]}], + parameters: [id_param()], + responses: %{ + 200 => Operation.response("Webhook", "application/json", webhook()) + } + } + end + + def disable_operation do + %Operation{ + tags: ["Webhooks"], + summary: "Disable a webhook", + operationId: "AdminAPI.WebhookController.disable", + security: [%{"oAuth" => ["admin:write"]}], + parameters: [id_param()], + responses: %{ + 200 => Operation.response("Webhook", "application/json", webhook()) + } + } + end + + def rotate_secret_operation do + %Operation{ + tags: ["Webhooks"], + summary: "Rotate webhook signing secret", + operationId: "AdminAPI.WebhookController.rotate_secret", + security: [%{"oAuth" => ["admin:write"]}], + parameters: [id_param()], + responses: %{ + 200 => Operation.response("Webhook", "application/json", webhook()) + } + } + end + + defp webhook do + %Schema{ + title: "Webhook", + description: "Schema for a webhook", + type: :object, + properties: %{ + id: %Schema{type: :string}, + url: %Schema{type: :string, format: :uri}, + events: event_type(), + secret: %Schema{type: :string}, + enabled: %Schema{type: :boolean}, + created_at: %Schema{type: :string, format: :"date-time"}, + updated_at: %Schema{type: :string, format: :"date-time"} + }, + example: %{ + "id" => "1", + "url" => "https://example.com/webhook", + "events" => ["report.created"], + "secret" => "d3d8cf4bc11fd9c41fd34dcc38d282e451c8bd34", + "enabled" => true, + "created_at" => "2022-06-24T16:19:38.523Z", + "updated_at" => "2022-06-24T16:19:38.523Z" + } + } + end + + defp event_type(required \\ nil) do + %Schema{ + type: :array, + items: %Schema{ + title: "Event", + description: "Event type", + type: :string, + enum: ["account.created", "report.created"], + required: required + } + } + end + + defp id_param do + Operation.parameter(:id, :path, :string, "Webhook ID", + example: "123", + required: true + ) + end +end diff --git a/lib/pleroma/web/api_spec/operations/notification_operation.ex b/lib/pleroma/web/api_spec/operations/notification_operation.ex index 94d1f6b82..ded5e725d 100644 --- a/lib/pleroma/web/api_spec/operations/notification_operation.ex +++ b/lib/pleroma/web/api_spec/operations/notification_operation.ex @@ -3,7 +3,6 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ApiSpec.NotificationOperation do - alias OpenApiSpex.Operation alias OpenApiSpex.Operation alias OpenApiSpex.Schema alias Pleroma.Web.ApiSpec.Schemas.Account diff --git a/lib/pleroma/web/mastodon_api/admin/views/account_view.ex b/lib/pleroma/web/mastodon_api/admin/views/account_view.ex new file mode 100644 index 000000000..40fac94ca --- /dev/null +++ b/lib/pleroma/web/mastodon_api/admin/views/account_view.ex @@ -0,0 +1,65 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2024 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.Admin.AccountView do + use Pleroma.Web, :view + + alias Pleroma.User + alias Pleroma.Web.CommonAPI.Utils + alias Pleroma.Web.MastodonAPI + + def render("index.json", %{users: users}) do + render_many(users, __MODULE__, "show.json", as: :user) + end + + def render("show.json", %{user: user}) do + account = + MastodonAPI.AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + + %{ + id: user.id, + username: username_from_nickname(user.nickname), + domain: domain_from_nickname(user.nickname), + created_at: Utils.to_masto_date(user.inserted_at), + email: user.email, + ip: nil, + role: role(user), + confirmed: user.is_confirmed, + sensitized: nil, + suspened: nil, + silenced: nil, + disabled: !user.is_active, + approved: user.is_approved, + locale: nil, + invite_request: user.registration_reason, + ips: nil, + account: account + } + end + + defp username_from_nickname(string) when is_binary(string) do + hd(String.split(string, "@")) + end + + defp username_from_nickname(_), do: nil + + defp domain_from_nickname(string) when is_binary(string) do + String.split(string, "@") + |> Enum.at(1, nil) + end + + defp domain_from_nickname(_), do: nil + + defp role(%User{is_admin: true}) do + "admin" + end + + defp role(%User{is_moderator: true}) do + "moderator" + end + + defp role(_user) do + nil + end +end diff --git a/lib/pleroma/web/mastodon_api/admin/views/report_view.ex b/lib/pleroma/web/mastodon_api/admin/views/report_view.ex new file mode 100644 index 000000000..dd8dec8e3 --- /dev/null +++ b/lib/pleroma/web/mastodon_api/admin/views/report_view.ex @@ -0,0 +1,54 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2024 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.Admin.ReportView do + use Pleroma.Web, :view + + alias Pleroma.HTML + alias Pleroma.Web.AdminAPI.Report + alias Pleroma.Web.CommonAPI.Utils + alias Pleroma.Web.MastodonAPI.Admin.AccountView + alias Pleroma.Web.MastodonAPI.StatusView + + def render("index.json", %{reports: reports}) do + reports + |> Enum.map(&Report.extract_report_info/1) + |> Enum.map(&render(__MODULE__, "show.json", &1)) + end + + def render("show.json", %{ + report: report, + user: account, + account: target_account, + statuses: statuses + }) do + created_at = Utils.to_masto_date(report.data["published"]) + + content = + unless is_nil(report.data["content"]) do + HTML.filter_tags(report.data["content"]) + else + nil + end + + %{ + id: report.id, + action_taken: report.data["state"] != "open", + category: "other", + comment: content, + created_at: created_at, + updated_at: created_at, + account: AccountView.render("show.json", %{user: account}), + target_account: AccountView.render("show.json", %{user: target_account}), + assigned_account: nil, + action_taken_by_account: nil, + statuses: + StatusView.render("index.json", %{ + activities: statuses, + as: :activity + }), + rules: [] + } + end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index cd9cfd3ed..e3c2f3ff7 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -320,6 +320,15 @@ defmodule Pleroma.Web.Router do get("/announcements/:id", AnnouncementController, :show) patch("/announcements/:id", AnnouncementController, :change) delete("/announcements/:id", AnnouncementController, :delete) + + get("/webhooks", WebhookController, :index) + get("/webhooks/:id", WebhookController, :show) + post("/webhooks", WebhookController, :create) + patch("/webhooks/:id", WebhookController, :update) + delete("/webhooks/:id", WebhookController, :delete) + post("/webhooks/:id/enable", WebhookController, :enable) + post("/webhooks/:id/disable", WebhookController, :disable) + post("/webhooks/:id/rotate_secret", WebhookController, :rotate_secret) end # AdminAPI: admins and mods (staff) can perform these actions (if privileged by role) diff --git a/lib/pleroma/webhook.ex b/lib/pleroma/webhook.ex new file mode 100644 index 000000000..6cf47fd68 --- /dev/null +++ b/lib/pleroma/webhook.ex @@ -0,0 +1,100 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Webhook do + use Ecto.Schema + + import Ecto.Changeset + import Ecto.Query + + alias Pleroma.EctoType.ActivityPub.ObjectValidators + alias Pleroma.Repo + + @event_types [:"account.created", :"report.created"] + + schema "webhooks" do + field(:url, ObjectValidators.Uri) + field(:events, {:array, Ecto.Enum}, values: @event_types, default: []) + field(:secret, :string, default: "") + field(:enabled, :boolean, default: true) + + timestamps() + end + + def get(id), do: Repo.get(__MODULE__, id) + + def get_by_type(type) do + __MODULE__ + |> where([w], ^type in w.events) + |> Repo.all() + end + + def changeset(%__MODULE__{} = webhook, params) do + webhook + |> cast(params, [:url, :events, :enabled]) + |> validate_required([:url, :events]) + |> unique_constraint(:url) + |> strip_events() + |> put_secret() + end + + def update_changeset(%__MODULE__{} = webhook, params \\ %{}) do + webhook + |> cast(params, [:url, :events, :enabled]) + |> unique_constraint(:url) + |> strip_events() + end + + def create(params) do + {:ok, webhook} = + %__MODULE__{} + |> changeset(params) + |> Repo.insert() + + webhook + end + + def update(%__MODULE__{} = webhook, params) do + {:ok, webhook} = + webhook + |> update_changeset(params) + |> Repo.update() + + webhook + end + + def delete(webhook), do: webhook |> Repo.delete() + + def rotate_secret(%__MODULE__{} = webhook) do + webhook + |> cast(%{}, []) + |> put_secret() + |> Repo.update() + end + + def set_enabled(%__MODULE__{} = webhook, enabled) do + webhook + |> cast(%{enabled: enabled}, [:enabled]) + |> Repo.update() + end + + defp strip_events(params) do + if Map.has_key?(params, :events) do + params + |> Map.put(:events, Enum.filter(params[:events], &Enum.member?(@event_types, &1))) + else + params + end + end + + defp put_secret(changeset) do + changeset + |> put_change(:secret, generate_secret()) + end + + defp generate_secret do + Base.encode16(:crypto.strong_rand_bytes(20)) + |> String.downcase() + end +end diff --git a/lib/pleroma/webhook/notify.ex b/lib/pleroma/webhook/notify.ex new file mode 100644 index 000000000..7dbfc52a0 --- /dev/null +++ b/lib/pleroma/webhook/notify.ex @@ -0,0 +1,83 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Webhook.Notify do + alias Phoenix.View + alias Pleroma.Activity + alias Pleroma.User + alias Pleroma.Web.AdminAPI.Report + alias Pleroma.Webhook + alias Pleroma.Workers.WebhookWorker + + def trigger_webhooks(%Activity{} = activity, :"report.created" = type) do + webhooks = Webhook.get_by_type(type) + + webhooks + |> Enum.map(fn webhook -> + WebhookWorker.new(%{ + "op" => "notify", + "type" => type, + "webhook_id" => webhook.id, + "activity_id" => activity.id + }) + end) + |> Oban.insert_all() + end + + def trigger_webhooks(%User{} = user, :"account.created" = type) do + webhooks = Webhook.get_by_type(type) + + webhooks + |> Enum.map(fn webhook -> + WebhookWorker.new(%{ + "op" => "notify", + "type" => type, + "webhook_id" => webhook.id, + "user_id" => user.id + }) + end) + |> Oban.insert_all() + end + + def report_created(%Webhook{} = webhook, %Activity{} = report) do + object = + View.render( + Pleroma.Web.MastodonAPI.Admin.ReportView, + "show.json", + Report.extract_report_info(report) + ) + + deliver(webhook, object, :"report.created") + end + + def account_created(%Webhook{} = webhook, %User{} = user) do + object = + View.render( + Pleroma.Web.MastodonAPI.Admin.AccountView, + "show.json", + user: user + ) + + deliver(webhook, object, :"account.created") + end + + defp deliver(%Webhook{url: url, secret: secret}, object, type) do + body = + View.render_to_string(Pleroma.Web.AdminAPI.WebhookView, "event.json", + type: type, + object: object + ) + + headers = [ + {"Content-Type", "application/json"}, + {"X-Hub-Signature", "sha256=#{signature(body, secret)}"} + ] + + Pleroma.HTTP.post(url, body, headers) + end + + defp signature(body, secret) do + :crypto.mac(:hmac, :sha256, secret, body) |> Base.encode16() + end +end diff --git a/lib/pleroma/workers/webhook_worker.ex b/lib/pleroma/workers/webhook_worker.ex new file mode 100644 index 000000000..b4329b775 --- /dev/null +++ b/lib/pleroma/workers/webhook_worker.ex @@ -0,0 +1,41 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.WebhookWorker do + alias Pleroma.Activity + + use Oban.Worker, queue: :background + + @impl true + def perform(%Job{ + args: %{ + "op" => "notify", + "type" => "report.created", + "webhook_id" => webhook_id, + "activity_id" => report_id + } + }) do + webhook = Pleroma.Webhook.get(webhook_id) + report = Activity.get_by_id(report_id) + + Pleroma.Webhook.Notify.report_created(webhook, report) + end + + def perform(%Job{ + args: %{ + "op" => "notify", + "type" => "account.created", + "webhook_id" => webhook_id, + "user_id" => user_id + } + }) do + webhook = Pleroma.Webhook.get(webhook_id) + user = Pleroma.User.get_by_id(user_id) + + Pleroma.Webhook.Notify.account_created(webhook, user) + end + + @impl true + def timeout(_job), do: :timer.seconds(10) +end diff --git a/priv/repo/migrations/20220624104914_create_webhooks.exs b/priv/repo/migrations/20220624104914_create_webhooks.exs new file mode 100644 index 000000000..c7836fc0c --- /dev/null +++ b/priv/repo/migrations/20220624104914_create_webhooks.exs @@ -0,0 +1,20 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Repo.Migrations.CreateWebhooks do + use Ecto.Migration + + def change do + create_if_not_exists table(:webhooks) do + add(:url, :string, null: false) + add(:events, {:array, :string}, null: false, default: []) + add(:secret, :string, null: false, default: "") + add(:enabled, :boolean, null: false, default: true) + + timestamps() + end + + create_if_not_exists(unique_index(:webhooks, [:url])) + end +end diff --git a/test/mix/tasks/pleroma/database_test.exs b/test/mix/tasks/pleroma/database_test.exs index 38ed096ae..c7dba7771 100644 --- a/test/mix/tasks/pleroma/database_test.exs +++ b/test/mix/tasks/pleroma/database_test.exs @@ -416,7 +416,8 @@ defmodule Mix.Tasks.Pleroma.DatabaseTest do ["user_invite_tokens"], ["user_notes"], ["user_relationships"], - ["users"] + ["users"], + ["webhooks"] ] end diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs index 0b4dc9197..04ec3a32e 100644 --- a/test/pleroma/user_test.exs +++ b/test/pleroma/user_test.exs @@ -11,12 +11,14 @@ defmodule Pleroma.UserTest do alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.CommonAPI + alias Pleroma.Webhook.Notify use Pleroma.DataCase, async: false use Oban.Testing, repo: Pleroma.Repo import Pleroma.Factory import ExUnit.CaptureLog + import Mock import Swoosh.TestAssertions setup do @@ -719,6 +721,14 @@ defmodule Pleroma.UserTest do assert user.is_confirmed end + + test_with_mock "triggers webhooks", Notify, trigger_webhooks: fn _, _ -> nil end do + cng = User.register_changeset(%User{}, @full_user_data) + + {:ok, registered_user} = User.register(cng) + + assert_called(Notify.trigger_webhooks(registered_user, :"account.created")) + end end describe "user registration, with :account_activation_required" do diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs index 7fc4f47cd..771af7639 100644 --- a/test/pleroma/web/activity_pub/activity_pub_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_test.exs @@ -17,6 +17,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.CommonAPI + alias Pleroma.Webhook.Notify import ExUnit.CaptureLog import Mock @@ -1715,6 +1716,29 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do assert Repo.aggregate(Object, :count, :id) == 1 assert Repo.aggregate(Notification, :count, :id) == 0 end + + test_with_mock "triggers webhooks", + %{ + reporter: reporter, + context: context, + target_account: target_account, + reported_activity: reported_activity, + content: content + }, + Notify, + [:passthrough], + trigger_webhooks: fn _, _ -> nil end do + {:ok, activity} = + ActivityPub.flag(%{ + actor: reporter, + context: context, + account: target_account, + statuses: [reported_activity], + content: content + }) + + assert_called(Notify.trigger_webhooks(activity, :"report.created")) + end end test "fetch_activities/2 returns activities addressed to a list " do diff --git a/test/pleroma/web/admin_api/controllers/webhook_controller_test.exs b/test/pleroma/web/admin_api/controllers/webhook_controller_test.exs new file mode 100644 index 000000000..6a1586ff1 --- /dev/null +++ b/test/pleroma/web/admin_api/controllers/webhook_controller_test.exs @@ -0,0 +1,84 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.WebhookControllerTest do + use Pleroma.Web.ConnCase, async: true + + import Pleroma.Factory + + alias Pleroma.Webhook + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + {:ok, %{admin: admin, token: token, conn: conn}} + end + + describe "GET /api/pleroma/admin/webhook" do + test "lists existing webhooks", %{conn: conn} do + Webhook.create(%{url: "https://example.com/webhook1", events: [:"report.created"]}) + Webhook.create(%{url: "https://example.com/webhook2", events: [:"account.created"]}) + + response = + conn + |> get("/api/pleroma/admin/webhooks") + |> json_response_and_validate_schema(:ok) + + assert length(response) == 2 + end + end + + describe "POST /api/pleroma/admin/webhooks" do + test "creates a webhook", %{conn: conn} do + %{"id" => id} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/webhooks", %{ + url: "http://example.com/webhook", + events: ["account.created"] + }) + |> json_response_and_validate_schema(:ok) + + assert %{url: "http://example.com/webhook", events: [:"account.created"]} = Webhook.get(id) + end + end + + describe "PATCH /api/pleroma/admin/webhooks" do + test "edits a webhook", %{conn: conn} do + %{id: id} = + Webhook.create(%{url: "https://example.com/webhook1", events: [:"report.created"]}) + + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/pleroma/admin/webhooks/#{id}", %{ + events: ["report.created", "account.created"] + }) + |> json_response_and_validate_schema(:ok) + + assert %{events: [:"report.created", :"account.created"]} = Webhook.get(id) + end + end + + describe "DELETE /api/pleroma/admin/webhooks" do + test "deletes a webhook", %{conn: conn} do + %{id: id} = + Webhook.create(%{url: "https://example.com/webhook1", events: [:"report.created"]}) + + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/pleroma/admin/webhooks/#{id}") + |> json_response_and_validate_schema(:ok) + + assert [] = + Webhook + |> Pleroma.Repo.all() + end + end +end diff --git a/test/pleroma/webhook/notify_test.exs b/test/pleroma/webhook/notify_test.exs new file mode 100644 index 000000000..e03890aab --- /dev/null +++ b/test/pleroma/webhook/notify_test.exs @@ -0,0 +1,75 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Webhook.NotifyTest do + use Pleroma.DataCase, async: true + + alias Pleroma.Webhook + alias Pleroma.Webhook.Notify + + import Pleroma.Factory + + test "triggers a webhook for a report" do + %{id: activity_id} = activity = insert(:report_activity) + + url = "https://example.com/webhook" + + Webhook.create(%{url: url, events: [:"report.created"]}) + + Tesla.Mock.mock(fn %{url: ^url, body: body} = _ -> + report = + body + |> Jason.decode!() + |> Map.get("object") + + assert %{"id" => ^activity_id} = report + + %Tesla.Env{status: 200, body: ""} + end) + + [job] = Notify.trigger_webhooks(activity, :"report.created") + + Pleroma.Workers.WebhookWorker.perform(job) + end + + test "triggers a webhook for an account" do + %{id: account_id} = user = insert(:user) + + url = "https://example.com/webhook" + + Webhook.create(%{url: url, events: [:"account.created"]}) + + Tesla.Mock.mock(fn %{url: ^url, body: body} = _ -> + report = + body + |> Jason.decode!() + |> Map.get("object") + + assert %{"id" => ^account_id} = report + + %Tesla.Env{status: 200, body: ""} + end) + + [job] = Notify.trigger_webhooks(user, :"account.created") + + Pleroma.Workers.WebhookWorker.perform(job) + end + + test "notifies have a valid signature" do + activity = insert(:report_activity) + + %{secret: secret} = + webhook = Webhook.create(%{url: "https://example.com/webhook", events: [:"report.created"]}) + + Tesla.Mock.mock(fn %{url: "https://example.com/webhook", body: body, headers: headers} = _ -> + {"X-Hub-Signature", "sha256=" <> signature} = + Enum.find(headers, fn {key, _} -> key == "X-Hub-Signature" end) + + assert signature == :crypto.mac(:hmac, :sha256, secret, body) |> Base.encode16() + %Tesla.Env{status: 200, body: ""} + end) + + Notify.report_created(webhook, activity) + end +end diff --git a/test/pleroma/webhook_test.exs b/test/pleroma/webhook_test.exs new file mode 100644 index 000000000..ea1378730 --- /dev/null +++ b/test/pleroma/webhook_test.exs @@ -0,0 +1,56 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.WebhookTest do + use Pleroma.DataCase, async: true + + alias Pleroma.Webhook + + test "creating a webhook" do + %{id: id} = Webhook.create(%{url: "https://example.com/webhook", events: [:"report.created"]}) + + assert %{url: "https://example.com/webhook"} = Webhook.get(id) + end + + test "editing a webhook" do + %{id: id} = + webhook = Webhook.create(%{url: "https://example.com/webhook", events: [:"report.created"]}) + + Webhook.update(webhook, %{events: [:"account.created"]}) + + assert %{events: [:"account.created"]} = Webhook.get(id) + end + + test "filter webhooks by type" do + %{id: id1} = + Webhook.create(%{url: "https://example.com/webhook1", events: [:"report.created"]}) + + %{id: id2} = + Webhook.create(%{ + url: "https://example.com/webhook2", + events: [:"account.created", :"report.created"] + }) + + Webhook.create(%{url: "https://example.com/webhook3", events: [:"account.created"]}) + + assert [%{id: ^id1}, %{id: ^id2}] = Webhook.get_by_type(:"report.created") + end + + test "change webhook state" do + %{id: id, enabled: true} = + webhook = Webhook.create(%{url: "https://example.com/webhook", events: [:"report.created"]}) + + Webhook.set_enabled(webhook, false) + assert %{enabled: false} = Webhook.get(id) + end + + test "rotate webhook secrets" do + %{id: id, secret: secret} = + webhook = Webhook.create(%{url: "https://example.com/webhook", events: [:"report.created"]}) + + Webhook.rotate_secret(webhook) + %{secret: new_secret} = Webhook.get(id) + assert secret != new_secret + end +end From 6d46aefb7f780ce0410a459d756f846ec05fef56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sat, 29 Oct 2022 19:07:54 +0200 Subject: [PATCH 010/391] Add internal (not managable from API) webhooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../controllers/fallback_controller.ex | 6 ++++++ .../controllers/webhook_controller.ex | 17 ++++++++++++----- .../web/admin_api/views/webhook_view.ex | 1 + .../operations/admin/webhook_operation.ex | 18 +++++++++++++----- lib/pleroma/webhook.ex | 5 +++-- ...20221029171353_add_internal_to_webhooks.exs | 13 +++++++++++++ .../controllers/webhook_controller_test.exs | 14 ++++++++++++++ 7 files changed, 62 insertions(+), 12 deletions(-) create mode 100644 priv/repo/migrations/20221029171353_add_internal_to_webhooks.exs diff --git a/lib/pleroma/web/admin_api/controllers/fallback_controller.ex b/lib/pleroma/web/admin_api/controllers/fallback_controller.ex index e72f45c21..d8fb54cad 100644 --- a/lib/pleroma/web/admin_api/controllers/fallback_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/fallback_controller.ex @@ -11,6 +11,12 @@ defmodule Pleroma.Web.AdminAPI.FallbackController do |> json(%{error: dgettext("errors", "Not found")}) end + def call(conn, {:error, :forbidden}) do + conn + |> put_status(:forbidden) + |> json(%{error: dgettext("errors", "Forbidden")}) + end + def call(conn, {:error, reason}) do conn |> put_status(:bad_request) diff --git a/lib/pleroma/web/admin_api/controllers/webhook_controller.ex b/lib/pleroma/web/admin_api/controllers/webhook_controller.ex index 8a6b0de7a..dd9712a31 100644 --- a/lib/pleroma/web/admin_api/controllers/webhook_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/webhook_controller.ex @@ -46,42 +46,49 @@ defmodule Pleroma.Web.AdminAPI.WebhookController do end def update(%{body_params: params} = conn, %{id: id}) do - with %Webhook{} = webhook <- Webhook.get(id), + with %Webhook{internal: false} = webhook <- Webhook.get(id), webhook <- Webhook.update(webhook, params) do render(conn, "show.json", webhook: webhook) + else + %Webhook{internal: true} -> {:error, :forbidden} end end def delete(conn, %{id: id}) do - with %Webhook{} = webhook <- Webhook.get(id), + with %Webhook{internal: false} = webhook <- Webhook.get(id), {:ok, webhook} <- Webhook.delete(webhook) do render(conn, "show.json", webhook: webhook) + else + %Webhook{internal: true} -> {:error, :forbidden} end end def enable(conn, %{id: id}) do - with %Webhook{} = webhook <- Webhook.get(id), + with %Webhook{internal: false} = webhook <- Webhook.get(id), {:ok, webhook} <- Webhook.set_enabled(webhook, true) do render(conn, "show.json", webhook: webhook) else + %Webhook{internal: true} -> {:error, :forbidden} nil -> {:error, :not_found} end end def disable(conn, %{id: id}) do - with %Webhook{} = webhook <- Webhook.get(id), + with %Webhook{internal: false} = webhook <- Webhook.get(id), {:ok, webhook} <- Webhook.set_enabled(webhook, false) do render(conn, "show.json", webhook: webhook) else + %Webhook{internal: true} -> {:error, :forbidden} nil -> {:error, :not_found} end end def rotate_secret(conn, %{id: id}) do - with %Webhook{} = webhook <- Webhook.get(id), + with %Webhook{internal: false} = webhook <- Webhook.get(id), {:ok, webhook} <- Webhook.rotate_secret(webhook) do render(conn, "show.json", webhook: webhook) else + %Webhook{internal: true} -> {:error, :forbidden} nil -> {:error, :not_found} end end diff --git a/lib/pleroma/web/admin_api/views/webhook_view.ex b/lib/pleroma/web/admin_api/views/webhook_view.ex index 725183029..552aba4f1 100644 --- a/lib/pleroma/web/admin_api/views/webhook_view.ex +++ b/lib/pleroma/web/admin_api/views/webhook_view.ex @@ -18,6 +18,7 @@ defmodule Pleroma.Web.AdminAPI.WebhookView do events: webhook.events, secret: webhook.secret, enabled: webhook.enabled, + internal: webhook.internal, created_at: Utils.to_masto_date(webhook.inserted_at), updated_at: Utils.to_masto_date(webhook.updated_at) } diff --git a/lib/pleroma/web/api_spec/operations/admin/webhook_operation.ex b/lib/pleroma/web/api_spec/operations/admin/webhook_operation.ex index c83caa555..fd5f3a561 100644 --- a/lib/pleroma/web/api_spec/operations/admin/webhook_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/webhook_operation.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.WebhookOperation do alias OpenApiSpex.Operation alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.ApiError import Pleroma.Web.ApiSpec.Helpers @@ -88,7 +89,8 @@ defmodule Pleroma.Web.ApiSpec.Admin.WebhookOperation do } ), responses: %{ - 200 => Operation.response("Webhook", "application/json", webhook()) + 200 => Operation.response("Webhook", "application/json", webhook()), + 403 => Operation.response("Forbidden", "application/json", ApiError) } } end @@ -101,7 +103,8 @@ defmodule Pleroma.Web.ApiSpec.Admin.WebhookOperation do security: [%{"oAuth" => ["admin:write"]}], parameters: [id_param()], responses: %{ - 200 => Operation.response("Webhook", "application/json", webhook()) + 200 => Operation.response("Webhook", "application/json", webhook()), + 403 => Operation.response("Forbidden", "application/json", ApiError) } } end @@ -114,7 +117,8 @@ defmodule Pleroma.Web.ApiSpec.Admin.WebhookOperation do security: [%{"oAuth" => ["admin:write"]}], parameters: [id_param()], responses: %{ - 200 => Operation.response("Webhook", "application/json", webhook()) + 200 => Operation.response("Webhook", "application/json", webhook()), + 403 => Operation.response("Forbidden", "application/json", ApiError) } } end @@ -127,7 +131,8 @@ defmodule Pleroma.Web.ApiSpec.Admin.WebhookOperation do security: [%{"oAuth" => ["admin:write"]}], parameters: [id_param()], responses: %{ - 200 => Operation.response("Webhook", "application/json", webhook()) + 200 => Operation.response("Webhook", "application/json", webhook()), + 403 => Operation.response("Forbidden", "application/json", ApiError) } } end @@ -140,7 +145,8 @@ defmodule Pleroma.Web.ApiSpec.Admin.WebhookOperation do security: [%{"oAuth" => ["admin:write"]}], parameters: [id_param()], responses: %{ - 200 => Operation.response("Webhook", "application/json", webhook()) + 200 => Operation.response("Webhook", "application/json", webhook()), + 403 => Operation.response("Forbidden", "application/json", ApiError) } } end @@ -156,6 +162,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.WebhookOperation do events: event_type(), secret: %Schema{type: :string}, enabled: %Schema{type: :boolean}, + internal: %Schema{type: :boolean}, created_at: %Schema{type: :string, format: :"date-time"}, updated_at: %Schema{type: :string, format: :"date-time"} }, @@ -165,6 +172,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.WebhookOperation do "events" => ["report.created"], "secret" => "d3d8cf4bc11fd9c41fd34dcc38d282e451c8bd34", "enabled" => true, + "internal" => false, "created_at" => "2022-06-24T16:19:38.523Z", "updated_at" => "2022-06-24T16:19:38.523Z" } diff --git a/lib/pleroma/webhook.ex b/lib/pleroma/webhook.ex index 6cf47fd68..f1e1b7678 100644 --- a/lib/pleroma/webhook.ex +++ b/lib/pleroma/webhook.ex @@ -18,6 +18,7 @@ defmodule Pleroma.Webhook do field(:events, {:array, Ecto.Enum}, values: @event_types, default: []) field(:secret, :string, default: "") field(:enabled, :boolean, default: true) + field(:internal, :boolean, default: false) timestamps() end @@ -32,7 +33,7 @@ defmodule Pleroma.Webhook do def changeset(%__MODULE__{} = webhook, params) do webhook - |> cast(params, [:url, :events, :enabled]) + |> cast(params, [:url, :events, :enabled, :internal]) |> validate_required([:url, :events]) |> unique_constraint(:url) |> strip_events() @@ -41,7 +42,7 @@ defmodule Pleroma.Webhook do def update_changeset(%__MODULE__{} = webhook, params \\ %{}) do webhook - |> cast(params, [:url, :events, :enabled]) + |> cast(params, [:url, :events, :enabled, :internal]) |> unique_constraint(:url) |> strip_events() end diff --git a/priv/repo/migrations/20221029171353_add_internal_to_webhooks.exs b/priv/repo/migrations/20221029171353_add_internal_to_webhooks.exs new file mode 100644 index 000000000..8c8ce7fc7 --- /dev/null +++ b/priv/repo/migrations/20221029171353_add_internal_to_webhooks.exs @@ -0,0 +1,13 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Repo.Migrations.AddInternalToWebhooks do + use Ecto.Migration + + def change do + alter table(:webhooks) do + add(:internal, :boolean, default: false, null: false) + end + end +end diff --git a/test/pleroma/web/admin_api/controllers/webhook_controller_test.exs b/test/pleroma/web/admin_api/controllers/webhook_controller_test.exs index 6a1586ff1..0cd00e753 100644 --- a/test/pleroma/web/admin_api/controllers/webhook_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/webhook_controller_test.exs @@ -64,6 +64,20 @@ defmodule Pleroma.Web.AdminAPI.WebhookControllerTest do assert %{events: [:"report.created", :"account.created"]} = Webhook.get(id) end + + test "can't edit an internal webhook", %{conn: conn} do + %{id: id} = + Webhook.create(%{url: "https://example.com/webhook1", events: [], internal: true}) + + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/pleroma/admin/webhooks/#{id}", %{ + events: ["report.created", "account.created"] + }) + |> json_response_and_validate_schema(:forbidden) + + assert %{events: []} = Webhook.get(id) + end end describe "DELETE /api/pleroma/admin/webhooks" do From 71f5a493f3be15308959a77ad368a61de48721e2 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 1 Jan 2026 10:02:37 +0400 Subject: [PATCH 011/391] Search: Better sorting for user searches. --- lib/pleroma/user/search.ex | 48 +++++++- test/pleroma/user/search_test.exs | 195 ++++++++++++++++++++++++++++++ 2 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 test/pleroma/user/search_test.exs diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex index 851745714..abc45f62a 100644 --- a/lib/pleroma/user/search.ex +++ b/lib/pleroma/user/search.ex @@ -4,6 +4,7 @@ defmodule Pleroma.User.Search do alias Pleroma.EctoType.ActivityPub.ObjectValidators.Uri, as: UriType + alias Pleroma.Instances.Instance alias Pleroma.Pagination alias Pleroma.User @@ -88,12 +89,13 @@ defmodule Pleroma.User.Search do |> filter_invisible_users() |> filter_internal_users() |> filter_blocked_domains(for_user) + |> filter_unreachable_users() |> fts_search(query_string) |> select_top_users(top_user_ids) |> trigram_rank(query_string) |> boost_search_rank(for_user, top_user_ids) |> subquery() - |> order_by(desc: :search_rank) + |> order_by_search_rank(for_user) |> maybe_restrict_local(for_user) |> maybe_restrict_accepting_chat_messages(capabilities) |> filter_deactivated_users() @@ -196,6 +198,14 @@ defmodule Pleroma.User.Search do defp filter_blocked_domains(query, _), do: query + defp filter_unreachable_users(query) do + from(u in query, + left_join: i in Instance, + on: i.host == fragment("substring(? from '.*://([^/]*)')", u.ap_id), + where: is_nil(i.unreachable_since) + ) + end + defp maybe_resolve(true, user, query) do case {limit(), user} do {:all, _} -> :noop @@ -236,6 +246,16 @@ defmodule Pleroma.User.Search do from(u in subquery(query), select_merge: %{ + search_type: + fragment( + """ + CASE WHEN (?) THEN 2 + WHEN (?) THEN 1 + ELSE 0 END + """, + u.id in ^top_user_ids, + u.id in ^friends_ids or u.id in ^followers_ids + ), search_rank: fragment( """ @@ -261,6 +281,14 @@ defmodule Pleroma.User.Search do defp boost_search_rank(query, _for_user, top_user_ids) do from(u in subquery(query), select_merge: %{ + search_type: + fragment( + """ + CASE WHEN (?) THEN 2 + ELSE 0 END + """, + u.id in ^top_user_ids + ), search_rank: fragment( """ @@ -273,4 +301,22 @@ defmodule Pleroma.User.Search do } ) end + + defp order_by_search_rank(query, %User{}) do + order_by( + query, + [u], + desc: u.search_type, + desc_nulls_last: + fragment( + "CASE WHEN ? = 1 THEN COALESCE(?, ?) ELSE NULL END", + u.search_type, + u.last_status_at, + u.last_active_at + ), + desc: u.search_rank + ) + end + + defp order_by_search_rank(query, _), do: order_by(query, desc: :search_rank) end diff --git a/test/pleroma/user/search_test.exs b/test/pleroma/user/search_test.exs new file mode 100644 index 000000000..c1aca90bc --- /dev/null +++ b/test/pleroma/user/search_test.exs @@ -0,0 +1,195 @@ +# Pleroma: A lightweight social networking server +# Copyright © Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.User.SearchTest do + use Pleroma.DataCase, async: false + + import Pleroma.Factory + + alias Pleroma.Instances + alias Pleroma.Repo + alias Pleroma.User + + describe "search/2 mention suggestions" do + test "prioritizes followed/follower users before others" do + user = insert(:user) + + related = + insert(:user, + local: false, + nickname: "hj@real.example", + ap_id: "https://real.example/users/hj", + last_status_at: ~N[2020-01-01 00:00:00] + ) + + other = insert(:user, nickname: "hj", last_status_at: ~N[2020-01-02 00:00:00]) + + {:ok, _related, _user} = User.follow(related, user) + + results = User.search("hj", for_user: user) |> Enum.map(& &1.id) + + assert results == [related.id, other.id] + end + + test "orders followed/follower users by most recent activity" do + user = insert(:user) + + older = + insert(:user, + local: false, + nickname: "ali@remote.example", + ap_id: "https://remote.example/users/ali", + last_status_at: ~N[2020-01-01 00:00:00] + ) + + newer = + insert(:user, + local: false, + nickname: "alia@remote.example", + ap_id: "https://remote.example/users/alia", + last_status_at: ~N[2020-01-02 00:00:00] + ) + + {:ok, _user, _older} = User.follow(user, older) + {:ok, _user, _newer} = User.follow(user, newer) + + assert [newer.id, older.id] == + User.search("ali", for_user: user) + |> Enum.map(& &1.id) + end + + test "groups followed/follower users first and sorts them by recency" do + user = insert(:user) + + following_newest = + insert(:user, + local: false, + nickname: "mentiontesta@related.example", + ap_id: "https://related.example/users/mentiontesta", + last_status_at: ~N[2020-01-03 00:00:00] + ) + + follower_middle = + insert(:user, + local: false, + nickname: "mentiontestb@related.example", + ap_id: "https://related.example/users/mentiontestb", + last_status_at: ~N[2020-01-02 00:00:00] + ) + + mutual_oldest = + insert(:user, + local: false, + nickname: "mentiontestc@related.example", + ap_id: "https://related.example/users/mentiontestc", + last_status_at: ~N[2020-01-01 00:00:00] + ) + + unrelated_newer = + insert(:user, + local: false, + nickname: "mentiontestd@unrelated.example", + ap_id: "https://unrelated.example/users/mentiontestd", + last_status_at: ~N[2020-01-04 00:00:00] + ) + + {:ok, _user, _following_newest} = User.follow(user, following_newest) + {:ok, _follower_middle, _user} = User.follow(follower_middle, user) + + {:ok, _user, _mutual_oldest} = User.follow(user, mutual_oldest) + {:ok, _mutual_oldest, _user} = User.follow(mutual_oldest, user) + + results = User.search("mentiontest", for_user: user) + + assert Enum.map(results, & &1.id) == + [following_newest.id, follower_middle.id, mutual_oldest.id, unrelated_newer.id] + end + + test "uses last_active_at when last_status_at is missing" do + user = insert(:user) + + older = + insert(:user, + local: false, + nickname: "activefallbacka@remote.example", + ap_id: "https://remote.example/users/activefallbacka", + last_status_at: nil, + last_active_at: ~N[2020-01-01 00:00:00] + ) + + newer = + insert(:user, + local: false, + nickname: "activefallbackb@remote.example", + ap_id: "https://remote.example/users/activefallbackb", + last_status_at: nil, + last_active_at: ~N[2020-01-02 00:00:00] + ) + + {:ok, _user, _older} = User.follow(user, older) + {:ok, _user, _newer} = User.follow(user, newer) + + assert [newer.id, older.id] == + User.search("activefallback", for_user: user) + |> Enum.map(& &1.id) + end + + test "does not return deactivated users even if related" do + user = insert(:user) + + active = + insert(:user, + local: false, + nickname: "deactivatedtesta@remote.example", + ap_id: "https://remote.example/users/deactivatedtesta", + last_status_at: ~N[2020-01-02 00:00:00] + ) + + deactivated = + insert(:user, + local: false, + nickname: "deactivatedtestb@remote.example", + ap_id: "https://remote.example/users/deactivatedtestb", + last_status_at: ~N[2020-01-03 00:00:00] + ) + + {:ok, _user, _active} = User.follow(user, active) + {:ok, _user, _deactivated} = User.follow(user, deactivated) + Repo.update!(Ecto.Changeset.change(deactivated, is_active: false)) + + results = User.search("deactivatedtest", for_user: user) |> Enum.map(& &1.id) + + assert results == [active.id] + end + + test "does not return users from unreachable instances" do + user = insert(:user) + + {:ok, _instance} = Instances.set_unreachable("dead.example") + + dead = + insert(:user, + local: false, + nickname: "ali@dead.example", + ap_id: "https://dead.example/users/ali", + last_status_at: ~N[2020-01-02 00:00:00] + ) + + alive = + insert(:user, + local: false, + nickname: "ali@alive.example", + ap_id: "https://alive.example/users/ali", + last_status_at: ~N[2020-01-02 00:00:00] + ) + + {:ok, _user, _alive} = User.follow(user, alive) + {:ok, _user, _dead} = User.follow(user, dead) + + results = User.search("ali", for_user: user) |> Enum.map(& &1.id) + + assert results == [alive.id] + end + end +end From 3903f12c78f9192ad27defe9738ca9669473c4c9 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 1 Jan 2026 10:04:55 +0400 Subject: [PATCH 012/391] Add changelog --- changelog.d/user-search-sorting.change | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/user-search-sorting.change diff --git a/changelog.d/user-search-sorting.change b/changelog.d/user-search-sorting.change new file mode 100644 index 000000000..def27e955 --- /dev/null +++ b/changelog.d/user-search-sorting.change @@ -0,0 +1 @@ +Improve user search / autocompletion ordering. From 0feb23a3082b65915e0abbe09c2df83f6014d6c3 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Fri, 16 Jan 2026 23:16:51 +0400 Subject: [PATCH 013/391] AP ingest: accept object type as list --- .../web/activity_pub/transmogrifier.ex | 6 +++ .../fedidev.fun/object_type_list.json | 28 ++++++++++ .../fedidev_fun_object_type_list_test.exs | 53 +++++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 test/fixtures/fedidev.fun/object_type_list.json create mode 100644 test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_object_type_list_test.exs diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index ba6f4030d..6b3194709 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -433,9 +433,15 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def handle_incoming(data, options \\ []) do data |> fix_recursive(&strip_internal_fields/1) + |> fix_recursive(&fix_type_as_string/1) |> handle_incoming_normalized(options) end + defp fix_type_as_string(%{"type" => [type | _]} = data) when is_binary(type), + do: Map.put(data, "type", type) + + defp fix_type_as_string(data), do: data + # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them # with nil ID. defp handle_incoming_normalized( diff --git a/test/fixtures/fedidev.fun/object_type_list.json b/test/fixtures/fedidev.fun/object_type_list.json new file mode 100644 index 000000000..f4ab00578 --- /dev/null +++ b/test/fixtures/fedidev.fun/object_type_list.json @@ -0,0 +1,28 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams" + ], + "type": "Create", + "id": "https://fedidev.fun/activity/object-type-list", + "actor": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:43:32Z", + "object": { + "type": [ + "Note" + ], + "id": "https://fedidev.fun/objects/note-object-type-list", + "attributedTo": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:43:32Z", + "name": "name", + "summary": "summary", + "content": "content" + } +} diff --git a/test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_object_type_list_test.exs b/test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_object_type_list_test.exs new file mode 100644 index 000000000..619e6f477 --- /dev/null +++ b/test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_object_type_list_test.exs @@ -0,0 +1,53 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.FedidevFunObjectTypeListTest do + use Oban.Testing, repo: Pleroma.Repo + use Pleroma.DataCase + + alias Pleroma.Object + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Transmogrifier + + import Pleroma.Factory + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + defp ensure_remote_actor!(ap_id) when is_binary(ap_id) do + case User.get_cached_by_ap_id(ap_id) do + %User{} -> + :ok + + _ -> + insert(:user, + local: false, + ap_id: ap_id, + follower_address: ap_id <> "/followers", + following_address: ap_id <> "/following", + featured_address: ap_id <> "/collections/featured", + inbox: ap_id <> "/inbox" + ) + + :ok + end + end + + defp ingest_fixture!(rel_path) do + data = rel_path |> File.read!() |> Jason.decode!() + ensure_remote_actor!(data["actor"]) + + assert {:ok, activity} = Transmogrifier.handle_incoming(data) + object = Object.normalize(activity.data["object"], fetch: false) + + {activity, object} + end + + test "accepts object type as a list and stores the first type" do + {_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/object_type_list.json") + assert object.data["type"] == "Note" + end +end From 39817fa54727204dc997e9211bae57d5fd625583 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Fri, 16 Jan 2026 23:25:07 +0400 Subject: [PATCH 014/391] AP ingest: normalize object url list --- .../article_note_page_validator.ex | 27 +++++++++- test/fixtures/fedidev.fun/url_list.json | 27 ++++++++++ .../fedidev_fun_url_list_test.exs | 53 +++++++++++++++++++ 3 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/fedidev.fun/url_list.json create mode 100644 test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_url_list_test.exs diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex index c0626ce4d..b0cdee552 100644 --- a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex @@ -46,7 +46,32 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do end defp fix_url(%{"url" => url} = data) when is_bitstring(url), do: data - defp fix_url(%{"url" => url} = data) when is_map(url), do: Map.put(data, "url", url["href"]) + + defp fix_url(%{"url" => url} = data) when is_map(url) do + if is_binary(url["href"]) do + Map.put(data, "url", url["href"]) + else + Map.delete(data, "url") + end + end + + defp fix_url(%{"url" => url} = data) when is_list(url) do + first_element = Enum.at(url, 0) + + url_string = + cond do + is_bitstring(first_element) -> first_element + is_map(first_element) -> first_element["href"] + true -> nil + end + + if is_binary(url_string) do + Map.put(data, "url", url_string) + else + Map.delete(data, "url") + end + end + defp fix_url(data), do: data defp fix_tag(%{"tag" => tag} = data) when is_list(tag) do diff --git a/test/fixtures/fedidev.fun/url_list.json b/test/fixtures/fedidev.fun/url_list.json new file mode 100644 index 000000000..b4cfd4cd6 --- /dev/null +++ b/test/fixtures/fedidev.fun/url_list.json @@ -0,0 +1,27 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams" + ], + "type": "Create", + "id": "https://fedidev.fun/activity/url-list", + "actor": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:39:06Z", + "object": { + "type": "Note", + "id": "https://fedidev.fun/objects/note-url-list", + "attributedTo": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:39:06Z", + "content": "text", + "url": [ + "https://fedidev.fun/objects/123" + ] + } +} diff --git a/test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_url_list_test.exs b/test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_url_list_test.exs new file mode 100644 index 000000000..bc084a622 --- /dev/null +++ b/test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_url_list_test.exs @@ -0,0 +1,53 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.FedidevFunUrlListTest do + use Oban.Testing, repo: Pleroma.Repo + use Pleroma.DataCase + + alias Pleroma.Object + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Transmogrifier + + import Pleroma.Factory + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + defp ensure_remote_actor!(ap_id) when is_binary(ap_id) do + case User.get_cached_by_ap_id(ap_id) do + %User{} -> + :ok + + _ -> + insert(:user, + local: false, + ap_id: ap_id, + follower_address: ap_id <> "/followers", + following_address: ap_id <> "/following", + featured_address: ap_id <> "/collections/featured", + inbox: ap_id <> "/inbox" + ) + + :ok + end + end + + defp ingest_fixture!(rel_path) do + data = rel_path |> File.read!() |> Jason.decode!() + ensure_remote_actor!(data["actor"]) + + assert {:ok, activity} = Transmogrifier.handle_incoming(data) + object = Object.normalize(activity.data["object"], fetch: false) + + {activity, object} + end + + test "normalizes object url list to a string" do + {_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/url_list.json") + assert object.data["url"] == "https://fedidev.fun/objects/123" + end +end From 6701f6199006b52a70288d2c90cd93aa97ce34ff Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Fri, 16 Jan 2026 23:27:06 +0400 Subject: [PATCH 015/391] AP ingest: normalize quoteUrl fields --- .../object_validators/common_fixes.ex | 27 ++++++-- .../fedidev.fun/misskey_quote_list.json | 28 +++++++++ test/fixtures/fedidev.fun/quote_uri_list.json | 28 +++++++++ test/fixtures/fedidev.fun/quote_url_list.json | 28 +++++++++ .../fedidev_fun_quote_url_list_test.exs | 63 +++++++++++++++++++ 5 files changed, 170 insertions(+), 4 deletions(-) create mode 100644 test/fixtures/fedidev.fun/misskey_quote_list.json create mode 100644 test/fixtures/fedidev.fun/quote_uri_list.json create mode 100644 test/fixtures/fedidev.fun/quote_url_list.json create mode 100644 test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_quote_url_list_test.exs diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex index 2e0f7a8a0..f34a4327f 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex @@ -96,23 +96,37 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do Map.put(data, "to", to) end - def fix_quote_url(%{"quoteUrl" => _quote_url} = data), do: data + def fix_quote_url(%{"quoteUrl" => quote_url} = data) do + case normalize_object_id(quote_url) do + quote_url when is_binary(quote_url) -> Map.put(data, "quoteUrl", quote_url) + _ -> Map.delete(data, "quoteUrl") + end + end # Fedibird # https://github.com/fedibird/mastodon/commit/dbd7ae6cf58a92ec67c512296b4daaea0d01e6ac def fix_quote_url(%{"quoteUri" => quote_url} = data) do - Map.put(data, "quoteUrl", quote_url) + case normalize_object_id(quote_url) do + quote_url when is_binary(quote_url) -> Map.put(data, "quoteUrl", quote_url) + _ -> data + end end # Old Fedibird (bug) # https://github.com/fedibird/mastodon/issues/9 def fix_quote_url(%{"quoteURL" => quote_url} = data) do - Map.put(data, "quoteUrl", quote_url) + case normalize_object_id(quote_url) do + quote_url when is_binary(quote_url) -> Map.put(data, "quoteUrl", quote_url) + _ -> data + end end # Misskey fallback def fix_quote_url(%{"_misskey_quote" => quote_url} = data) do - Map.put(data, "quoteUrl", quote_url) + case normalize_object_id(quote_url) do + quote_url when is_binary(quote_url) -> Map.put(data, "quoteUrl", quote_url) + _ -> data + end end def fix_quote_url(%{"tag" => [_ | _] = tags} = data) do @@ -128,6 +142,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do def fix_quote_url(data), do: data + defp normalize_object_id(object_id) when is_binary(object_id), do: object_id + defp normalize_object_id(%{"id" => object_id}) when is_binary(object_id), do: object_id + defp normalize_object_id([object_id | _]) when is_binary(object_id), do: object_id + defp normalize_object_id(_), do: nil + # On Mastodon, `"likes"` attribute includes an inlined `Collection` with `totalItems`, # not a list of users. # https://github.com/mastodon/mastodon/pull/32007 diff --git a/test/fixtures/fedidev.fun/misskey_quote_list.json b/test/fixtures/fedidev.fun/misskey_quote_list.json new file mode 100644 index 000000000..b9ea08691 --- /dev/null +++ b/test/fixtures/fedidev.fun/misskey_quote_list.json @@ -0,0 +1,28 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams" + ], + "type": "Create", + "id": "https://fedidev.fun/activity/misskey-quote-list", + "actor": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:43:53Z", + "object": { + "type": "Note", + "id": "https://fedidev.fun/objects/note-misskey-quote-list", + "attributedTo": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:43:53Z", + "content": "_misskey_quote attribute as list", + "_misskey_quote": [ + "https://fedidev.fun/assets/note1.jsonap", + "https://fedidev.fun/assets/note2.jsonap" + ] + } +} diff --git a/test/fixtures/fedidev.fun/quote_uri_list.json b/test/fixtures/fedidev.fun/quote_uri_list.json new file mode 100644 index 000000000..9245077e6 --- /dev/null +++ b/test/fixtures/fedidev.fun/quote_uri_list.json @@ -0,0 +1,28 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams" + ], + "type": "Create", + "id": "https://fedidev.fun/activity/quote-uri-list", + "actor": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:43:52Z", + "object": { + "type": "Note", + "id": "https://fedidev.fun/objects/note-quote-uri-list", + "attributedTo": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:43:52Z", + "content": "quoteUri attribute as list", + "quoteUri": [ + "https://fedidev.fun/assets/note1.jsonap", + "https://fedidev.fun/assets/note2.jsonap" + ] + } +} diff --git a/test/fixtures/fedidev.fun/quote_url_list.json b/test/fixtures/fedidev.fun/quote_url_list.json new file mode 100644 index 000000000..eecd0dd35 --- /dev/null +++ b/test/fixtures/fedidev.fun/quote_url_list.json @@ -0,0 +1,28 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams" + ], + "type": "Create", + "id": "https://fedidev.fun/activity/quote-url-list", + "actor": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:43:52Z", + "object": { + "type": "Note", + "id": "https://fedidev.fun/objects/note-quote-url-list", + "attributedTo": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:43:52Z", + "content": "quoteUrl attribute as list", + "quoteUrl": [ + "https://fedidev.fun/assets/note1.jsonap", + "https://fedidev.fun/assets/note2.jsonap" + ] + } +} diff --git a/test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_quote_url_list_test.exs b/test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_quote_url_list_test.exs new file mode 100644 index 000000000..9607fabdd --- /dev/null +++ b/test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_quote_url_list_test.exs @@ -0,0 +1,63 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.FedidevFunQuoteUrlListTest do + use Oban.Testing, repo: Pleroma.Repo + use Pleroma.DataCase + + alias Pleroma.Object + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Transmogrifier + + import Pleroma.Factory + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + defp ensure_remote_actor!(ap_id) when is_binary(ap_id) do + case User.get_cached_by_ap_id(ap_id) do + %User{} -> + :ok + + _ -> + insert(:user, + local: false, + ap_id: ap_id, + follower_address: ap_id <> "/followers", + following_address: ap_id <> "/following", + featured_address: ap_id <> "/collections/featured", + inbox: ap_id <> "/inbox" + ) + + :ok + end + end + + defp ingest_fixture!(rel_path) do + data = rel_path |> File.read!() |> Jason.decode!() + ensure_remote_actor!(data["actor"]) + + assert {:ok, activity} = Transmogrifier.handle_incoming(data) + object = Object.normalize(activity.data["object"], fetch: false) + + {activity, object} + end + + test "normalizes quoteUri list to the first object ID" do + {_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/quote_uri_list.json") + assert object.data["quoteUrl"] == "https://fedidev.fun/assets/note1.jsonap" + end + + test "normalizes quoteUrl list to the first object ID" do + {_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/quote_url_list.json") + assert object.data["quoteUrl"] == "https://fedidev.fun/assets/note1.jsonap" + end + + test "normalizes _misskey_quote list to the first object ID" do + {_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/misskey_quote_list.json") + assert object.data["quoteUrl"] == "https://fedidev.fun/assets/note1.jsonap" + end +end From bff67650e109b8f2064c4b6570164da06fe90ac4 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Fri, 16 Jan 2026 23:57:01 +0400 Subject: [PATCH 016/391] AP ingest: normalize inReplyTo shapes --- .../object_validators/common_fixes.ex | 30 +++++--- .../web/activity_pub/transmogrifier.ex | 30 +++++++- .../fedidev.fun/in_reply_to_collection.json | 30 ++++++++ .../in_reply_to_embedded_note.json | 35 +++++++++ .../fedidev.fun/in_reply_to_link.json | 28 ++++++++ .../fedidev.fun/in_reply_to_list.json | 28 ++++++++ .../fedidev_fun_in_reply_to_test.exs | 72 +++++++++++++++++++ .../transmogrifier/note_handling_test.exs | 4 +- 8 files changed, 245 insertions(+), 12 deletions(-) create mode 100644 test/fixtures/fedidev.fun/in_reply_to_collection.json create mode 100644 test/fixtures/fedidev.fun/in_reply_to_embedded_note.json create mode 100644 test/fixtures/fedidev.fun/in_reply_to_link.json create mode 100644 test/fixtures/fedidev.fun/in_reply_to_list.json create mode 100644 test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_in_reply_to_test.exs diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex index f34a4327f..631f301af 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex @@ -41,22 +41,34 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do def fix_object_defaults(data) do data = Maps.filter_empty_values(data) + in_reply_to_id = in_reply_to_id(data["inReplyTo"]) + context = Utils.maybe_create_context( - data["context"] || data["conversation"] || data["inReplyTo"] || data["id"] + data["context"] || data["conversation"] || in_reply_to_id || data["id"] ) - %User{follower_address: follower_collection} = User.get_cached_by_ap_id(data["attributedTo"]) + data = Map.put(data, "context", context) - data - |> Map.put("context", context) - |> cast_and_filter_recipients("to", follower_collection) - |> cast_and_filter_recipients("cc", follower_collection) - |> cast_and_filter_recipients("bto", follower_collection) - |> cast_and_filter_recipients("bcc", follower_collection) - |> Transmogrifier.fix_implicit_addressing(follower_collection) + with attributed_to when is_binary(attributed_to) <- data["attributedTo"], + %User{follower_address: follower_collection} <- User.get_cached_by_ap_id(attributed_to) do + data + |> cast_and_filter_recipients("to", follower_collection) + |> cast_and_filter_recipients("cc", follower_collection) + |> cast_and_filter_recipients("bto", follower_collection) + |> cast_and_filter_recipients("bcc", follower_collection) + |> Transmogrifier.fix_implicit_addressing(follower_collection) + else + _ -> data + end end + defp in_reply_to_id(in_reply_to) when is_binary(in_reply_to), do: in_reply_to + defp in_reply_to_id(%{"id" => in_reply_to}) when is_binary(in_reply_to), do: in_reply_to + defp in_reply_to_id(%{"href" => in_reply_to}) when is_binary(in_reply_to), do: in_reply_to + defp in_reply_to_id([in_reply_to | _]), do: in_reply_to_id(in_reply_to) + defp in_reply_to_id(_), do: nil + def fix_activity_addressing(activity) do %User{follower_address: follower_collection} = User.get_cached_by_ap_id(activity["actor"]) diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 6b3194709..43d991a22 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -201,9 +201,29 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options) when not is_nil(in_reply_to) do in_reply_to_id = prepare_in_reply_to(in_reply_to) + + object = + cond do + is_list(in_reply_to) -> + if not_empty_string(in_reply_to_id) do + Map.put(object, "inReplyTo", in_reply_to_id) + else + Map.delete(object, "inReplyTo") + end + + is_map(in_reply_to) and is_nil(in_reply_to["id"]) and is_binary(in_reply_to["href"]) -> + Map.put(object, "inReplyTo", in_reply_to["href"]) + + is_map(in_reply_to) and is_nil(in_reply_to["id"]) and is_nil(in_reply_to["href"]) -> + Map.delete(object, "inReplyTo") + + true -> + object + end + depth = (options[:depth] || 0) + 1 - if Federator.allowed_thread_distance?(depth) do + if not_empty_string(in_reply_to_id) and Federator.allowed_thread_distance?(depth) do with {:ok, replied_object} <- get_obj_helper(in_reply_to_id, options), %Activity{} <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do object @@ -249,9 +269,17 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) -> in_reply_to["id"] + is_map(in_reply_to) && is_bitstring(in_reply_to["href"]) -> + in_reply_to["href"] + is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) -> Enum.at(in_reply_to, 0) + is_list(in_reply_to) && is_map(Enum.at(in_reply_to, 0)) -> + in_reply_to + |> Enum.at(0) + |> prepare_in_reply_to() + true -> "" end diff --git a/test/fixtures/fedidev.fun/in_reply_to_collection.json b/test/fixtures/fedidev.fun/in_reply_to_collection.json new file mode 100644 index 000000000..4deb7ed9a --- /dev/null +++ b/test/fixtures/fedidev.fun/in_reply_to_collection.json @@ -0,0 +1,30 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams" + ], + "type": "Create", + "id": "https://fedidev.fun/activity/in-reply-to-collection", + "actor": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:40:08Z", + "object": { + "type": "Note", + "id": "https://fedidev.fun/objects/note-in-reply-to-collection", + "attributedTo": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:40:08Z", + "content": "collection", + "inReplyTo": { + "type": "Collection", + "items": [ + "https://fedidev.fun/users/alice" + ] + } + } +} diff --git a/test/fixtures/fedidev.fun/in_reply_to_embedded_note.json b/test/fixtures/fedidev.fun/in_reply_to_embedded_note.json new file mode 100644 index 000000000..de66f144f --- /dev/null +++ b/test/fixtures/fedidev.fun/in_reply_to_embedded_note.json @@ -0,0 +1,35 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams" + ], + "type": "Create", + "id": "https://fedidev.fun/activity/in-reply-to-embedded-note", + "actor": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:40:21Z", + "object": { + "type": "Note", + "id": "https://fedidev.fun/objects/note-in-reply-to-embedded-note", + "attributedTo": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:40:21Z", + "content": "embedded note", + "inReplyTo": { + "@context": "https://www.w3.org/ns/activitystreams", + "type": "Note", + "id": "https://fedidev.fun/assets/note1.jsonap", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "attributedTo": "https://fedidev.fun/users/alice", + "content": "One", + "published": "2024-01-06T13:11:45Z" + } + } +} diff --git a/test/fixtures/fedidev.fun/in_reply_to_link.json b/test/fixtures/fedidev.fun/in_reply_to_link.json new file mode 100644 index 000000000..f000ddcac --- /dev/null +++ b/test/fixtures/fedidev.fun/in_reply_to_link.json @@ -0,0 +1,28 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams" + ], + "type": "Create", + "id": "https://fedidev.fun/activity/in-reply-to-link", + "actor": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:39:56Z", + "object": { + "type": "Note", + "id": "https://fedidev.fun/objects/note-in-reply-to-link", + "attributedTo": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:39:56Z", + "content": "Link in inReplyTo", + "inReplyTo": { + "type": "Link", + "href": "https://fedidev.fun/assets/note1.jsonap" + } + } +} diff --git a/test/fixtures/fedidev.fun/in_reply_to_list.json b/test/fixtures/fedidev.fun/in_reply_to_list.json new file mode 100644 index 000000000..7242558b3 --- /dev/null +++ b/test/fixtures/fedidev.fun/in_reply_to_list.json @@ -0,0 +1,28 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams" + ], + "type": "Create", + "id": "https://fedidev.fun/activity/in-reply-to-list", + "actor": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:40:08Z", + "object": { + "type": "Note", + "id": "https://fedidev.fun/objects/note-in-reply-to-list", + "attributedTo": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:40:08Z", + "content": "two elements in inReplyTo", + "inReplyTo": [ + "https://fedidev.fun/assets/note1.jsonap", + "https://fedidev.fun/assets/note2.jsonap" + ] + } +} diff --git a/test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_in_reply_to_test.exs b/test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_in_reply_to_test.exs new file mode 100644 index 000000000..b720a5441 --- /dev/null +++ b/test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_in_reply_to_test.exs @@ -0,0 +1,72 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.FedidevFunInReplyToTest do + use Oban.Testing, repo: Pleroma.Repo + use Pleroma.DataCase + + alias Pleroma.Object + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Transmogrifier + + import Pleroma.Factory + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + defp ensure_remote_actor!(ap_id) when is_binary(ap_id) do + case User.get_cached_by_ap_id(ap_id) do + %User{} -> + :ok + + _ -> + insert(:user, + local: false, + ap_id: ap_id, + follower_address: ap_id <> "/followers", + following_address: ap_id <> "/following", + featured_address: ap_id <> "/collections/featured", + inbox: ap_id <> "/inbox" + ) + + :ok + end + end + + defp ingest_fixture!(rel_path) do + data = rel_path |> File.read!() |> Jason.decode!() + ensure_remote_actor!(data["actor"]) + + assert {:ok, activity} = Transmogrifier.handle_incoming(data) + object = Object.normalize(activity.data["object"], fetch: false) + + {activity, object} + end + + test "normalizes Link inReplyTo to a string" do + {_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/in_reply_to_link.json") + assert object.data["inReplyTo"] == "https://fedidev.fun/assets/note1.jsonap" + end + + test "normalizes list inReplyTo to the first object ID" do + {_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/in_reply_to_list.json") + assert object.data["inReplyTo"] == "https://fedidev.fun/assets/note1.jsonap" + end + + test "drops invalid map shapes in inReplyTo" do + {_activity, object} = + ingest_fixture!("test/fixtures/fedidev.fun/in_reply_to_collection.json") + + assert object.data["inReplyTo"] == nil + end + + test "accepts embedded objects in inReplyTo" do + {_activity, object} = + ingest_fixture!("test/fixtures/fedidev.fun/in_reply_to_embedded_note.json") + + assert object.data["inReplyTo"] == "https://fedidev.fun/assets/note1.jsonap" + end +end diff --git a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs index 403c98a2d..f28d4f771 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs @@ -572,11 +572,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do Map.put(data["object"], "inReplyTo", ["https://shitposter.club/notice/2827873"]) modified_object = Transmogrifier.fix_in_reply_to(object_with_reply) - assert modified_object["inReplyTo"] == ["https://shitposter.club/notice/2827873"] + assert modified_object["inReplyTo"] == "https://shitposter.club/notice/2827873" object_with_reply = Map.put(data["object"], "inReplyTo", []) modified_object = Transmogrifier.fix_in_reply_to(object_with_reply) - assert modified_object["inReplyTo"] == [] + assert modified_object["inReplyTo"] == nil end test "returns modified object when allowed incoming reply", %{data: data} do From e781a7ef9b231a62c9c3d93cde20841dca13abea Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 17 Jan 2026 00:08:19 +0400 Subject: [PATCH 017/391] AP ingest: harden attachment parsing --- .../article_note_page_validator.ex | 5 +- .../web/activity_pub/transmogrifier.ex | 11 ++- .../fedidev.fun/attachment_embedded_note.json | 32 +++++++++ .../fedidev.fun/attachment_payto_link.json | 31 +++++++++ .../fedidev.fun/attachment_url_list.json | 40 +++++++++++ .../fedidev_fun_attachments_test.exs | 69 +++++++++++++++++++ 6 files changed, 182 insertions(+), 6 deletions(-) create mode 100644 test/fixtures/fedidev.fun/attachment_embedded_note.json create mode 100644 test/fixtures/fedidev.fun/attachment_payto_link.json create mode 100644 test/fixtures/fedidev.fun/attachment_url_list.json create mode 100644 test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_attachments_test.exs diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex index b0cdee552..f2df2250f 100644 --- a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex @@ -100,10 +100,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do defp fix_replies(data), do: Map.delete(data, "replies") - def fix_attachments(%{"attachment" => attachment} = data) when is_map(attachment), - do: Map.put(data, "attachment", [attachment]) - - def fix_attachments(data), do: data + def fix_attachments(data), do: Transmogrifier.fix_attachments(data) defp fix(data) do data diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 43d991a22..cc84cf8af 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -293,6 +293,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> Map.drop(["conversation"]) end + defp valid_http_url?(href) when is_binary(href) do + match?({:ok, _}, ObjectValidators.Uri.cast(href)) + end + + defp valid_http_url?(_), do: false + def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do attachments = Enum.map(attachment, fn data -> @@ -305,7 +311,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do media_type = cond do - is_map(url) && url =~ Pleroma.Constants.mime_regex() -> + is_map(url) && is_bitstring(url["mediaType"]) && + url["mediaType"] =~ Pleroma.Constants.mime_regex() -> url["mediaType"] is_bitstring(data["mediaType"]) && data["mediaType"] =~ Pleroma.Constants.mime_regex() -> @@ -326,7 +333,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do true -> nil end - if href do + if href && valid_http_url?(href) do attachment_url = %{ "href" => href, diff --git a/test/fixtures/fedidev.fun/attachment_embedded_note.json b/test/fixtures/fedidev.fun/attachment_embedded_note.json new file mode 100644 index 000000000..8e83762dc --- /dev/null +++ b/test/fixtures/fedidev.fun/attachment_embedded_note.json @@ -0,0 +1,32 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams" + ], + "type": "Create", + "id": "https://fedidev.fun/activity/attachment-embedded-note", + "actor": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:43:01Z", + "object": { + "type": "Note", + "id": "https://fedidev.fun/objects/note-attachment-embedded-note", + "attributedTo": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:43:01Z", + "content": "Note with embedded Note attachment", + "attachment": { + "type": "Note", + "attributedTo": "https://fedidev.fun/users/alice", + "name": "attached note", + "content": "This is just a note", + "published": "2024-03-06T07:23:56Z" + } + } +} + diff --git a/test/fixtures/fedidev.fun/attachment_payto_link.json b/test/fixtures/fedidev.fun/attachment_payto_link.json new file mode 100644 index 000000000..1da324cb3 --- /dev/null +++ b/test/fixtures/fedidev.fun/attachment_payto_link.json @@ -0,0 +1,31 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams" + ], + "type": "Create", + "id": "https://fedidev.fun/activity/attachment-payto-link", + "actor": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:43:00Z", + "object": { + "type": "Note", + "id": "https://fedidev.fun/objects/note-attachment-payto-link", + "attributedTo": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:43:00Z", + "content": "Payment Link", + "attachment": { + "type": "Link", + "name": "Donate", + "href": "payto://iban/DE75512108001245126199", + "rel": "payment" + } + } +} + diff --git a/test/fixtures/fedidev.fun/attachment_url_list.json b/test/fixtures/fedidev.fun/attachment_url_list.json new file mode 100644 index 000000000..0efaeafdb --- /dev/null +++ b/test/fixtures/fedidev.fun/attachment_url_list.json @@ -0,0 +1,40 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams" + ], + "type": "Create", + "id": "https://fedidev.fun/activity/attachment-url-list", + "actor": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:43:02Z", + "object": { + "type": "Note", + "id": "https://fedidev.fun/objects/note-attachment-url-list", + "attributedTo": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:43:02Z", + "content": "url is array of two Link objects", + "attachment": { + "type": "Image", + "url": [ + { + "type": "Link", + "href": "https://fedidev.fun/images/007.png", + "mediaType": "image/png" + }, + { + "type": "Link", + "href": "https://fedidev.fun/images/008.jpg", + "mediaType": "image/jpeg" + } + ] + } + } +} + diff --git a/test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_attachments_test.exs b/test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_attachments_test.exs new file mode 100644 index 000000000..a13912b08 --- /dev/null +++ b/test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_attachments_test.exs @@ -0,0 +1,69 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.FedidevFunAttachmentsTest do + use Oban.Testing, repo: Pleroma.Repo + use Pleroma.DataCase + + alias Pleroma.Object + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Transmogrifier + + import Pleroma.Factory + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + defp ensure_remote_actor!(ap_id) when is_binary(ap_id) do + case User.get_cached_by_ap_id(ap_id) do + %User{} -> + :ok + + _ -> + insert(:user, + local: false, + ap_id: ap_id, + follower_address: ap_id <> "/followers", + following_address: ap_id <> "/following", + featured_address: ap_id <> "/collections/featured", + inbox: ap_id <> "/inbox" + ) + + :ok + end + end + + defp ingest_fixture!(rel_path) do + data = rel_path |> File.read!() |> Jason.decode!() + ensure_remote_actor!(data["actor"]) + + assert {:ok, activity} = Transmogrifier.handle_incoming(data) + object = Object.normalize(activity.data["object"], fetch: false) + + {activity, object} + end + + test "drops non-http(s) attachment href" do + {_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/attachment_payto_link.json") + assert object.data["attachment"] == [] + end + + test "drops embedded non-attachment objects" do + {_activity, object} = + ingest_fixture!("test/fixtures/fedidev.fun/attachment_embedded_note.json") + + assert object.data["attachment"] == [] + end + + test "accepts attachment url as list and keeps the first link" do + {_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/attachment_url_list.json") + + [attachment] = object.data["attachment"] + [%{"href" => href} | _] = attachment["url"] + + assert href == "https://fedidev.fun/images/007.png" + end +end From e9e3d255e5349f913ed88a36e6ff38ec93c8544b Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 17 Jan 2026 00:12:48 +0400 Subject: [PATCH 018/391] AP ingest: infer tag type when missing --- .../object_validators/tag_validator.ex | 21 ++++++ .../fedidev.fun/tag_hashtag_missing_type.json | 30 +++++++++ .../fedidev.fun/tag_mention_missing_type.json | 31 +++++++++ .../fedidev_fun_tag_inference_test.exs | 65 +++++++++++++++++++ 4 files changed, 147 insertions(+) create mode 100644 test/fixtures/fedidev.fun/tag_hashtag_missing_type.json create mode 100644 test/fixtures/fedidev.fun/tag_mention_missing_type.json create mode 100644 test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_tag_inference_test.exs diff --git a/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex b/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex index dc2770189..a412c8dea 100644 --- a/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex @@ -80,6 +80,27 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.TagValidator do |> Map.put(:action, :ignore) end + def changeset(struct, data) when is_map(data) do + data = infer_type(data) + + if Map.has_key?(data, "type") do + changeset(struct, data) + else + struct + |> cast(data, []) + |> Map.put(:action, :ignore) + end + end + + defp infer_type(%{"type" => _} = data), do: data + defp infer_type(%{"name" => "#" <> _} = data), do: Map.put(data, "type", "Hashtag") + defp infer_type(%{"name" => "@" <> _} = data), do: Map.put(data, "type", "Mention") + + defp infer_type(%{"mediaType" => _media_type, "href" => _href} = data), + do: Map.put(data, "type", "Link") + + defp infer_type(data), do: data + def icon_changeset(struct, data) do struct |> cast(data, [:type, :url]) diff --git a/test/fixtures/fedidev.fun/tag_hashtag_missing_type.json b/test/fixtures/fedidev.fun/tag_hashtag_missing_type.json new file mode 100644 index 000000000..74f9d91b8 --- /dev/null +++ b/test/fixtures/fedidev.fun/tag_hashtag_missing_type.json @@ -0,0 +1,30 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams" + ], + "type": "Create", + "id": "https://fedidev.fun/activity/tag-hashtag-missing-type", + "actor": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:41:00Z", + "object": { + "type": "Note", + "id": "https://fedidev.fun/objects/note-tag-hashtag-missing-type", + "attributedTo": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:41:00Z", + "content": "hashtag without type", + "tag": [ + { + "name": "#Test" + } + ] + } +} + diff --git a/test/fixtures/fedidev.fun/tag_mention_missing_type.json b/test/fixtures/fedidev.fun/tag_mention_missing_type.json new file mode 100644 index 000000000..d290be6c2 --- /dev/null +++ b/test/fixtures/fedidev.fun/tag_mention_missing_type.json @@ -0,0 +1,31 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams" + ], + "type": "Create", + "id": "https://fedidev.fun/activity/tag-mention-missing-type", + "actor": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:41:01Z", + "object": { + "type": "Note", + "id": "https://fedidev.fun/objects/note-tag-mention-missing-type", + "attributedTo": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:41:01Z", + "content": "mention without type", + "tag": [ + { + "name": "@bob", + "href": "https://remote.example/users/bob" + } + ] + } +} + diff --git a/test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_tag_inference_test.exs b/test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_tag_inference_test.exs new file mode 100644 index 000000000..ef7feb8ad --- /dev/null +++ b/test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_tag_inference_test.exs @@ -0,0 +1,65 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.FedidevFunTagInferenceTest do + use Oban.Testing, repo: Pleroma.Repo + use Pleroma.DataCase + + alias Pleroma.Object + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Transmogrifier + + import Pleroma.Factory + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + defp ensure_remote_actor!(ap_id) when is_binary(ap_id) do + case User.get_cached_by_ap_id(ap_id) do + %User{} -> + :ok + + _ -> + insert(:user, + local: false, + ap_id: ap_id, + follower_address: ap_id <> "/followers", + following_address: ap_id <> "/following", + featured_address: ap_id <> "/collections/featured", + inbox: ap_id <> "/inbox" + ) + + :ok + end + end + + defp ingest_fixture!(rel_path) do + data = rel_path |> File.read!() |> Jason.decode!() + ensure_remote_actor!(data["actor"]) + + assert {:ok, activity} = Transmogrifier.handle_incoming(data) + object = Object.normalize(activity.data["object"], fetch: false) + + {activity, object} + end + + test "infers Hashtag type when missing" do + {_activity, object} = + ingest_fixture!("test/fixtures/fedidev.fun/tag_hashtag_missing_type.json") + + assert "test" in Object.hashtags(object) + end + + test "infers Mention type when missing" do + {_activity, object} = + ingest_fixture!("test/fixtures/fedidev.fun/tag_mention_missing_type.json") + + assert Enum.any?(object.data["tag"], fn + %{"type" => "Mention", "href" => "https://remote.example/users/bob"} -> true + _ -> false + end) + end +end From 9912a9f603b1d0816f95491b67a9ba12553123ce Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 17 Jan 2026 01:33:21 +0400 Subject: [PATCH 019/391] AP ingest: accept Emoji icon variants --- .../object_validators/tag_validator.ex | 62 ++++++++++++++++-- .../web/activity_pub/transmogrifier.ex | 30 ++++++++- .../fedidev.fun/emoji_icon_string.json | 32 +++++++++ .../emoji_icon_url_missing_type.json | 34 ++++++++++ .../fedidev.fun/emoji_missing_icon.json | 31 +++++++++ .../transmogrifier/fedidev_fun_emoji_test.exs | 65 +++++++++++++++++++ 6 files changed, 246 insertions(+), 8 deletions(-) create mode 100644 test/fixtures/fedidev.fun/emoji_icon_string.json create mode 100644 test/fixtures/fedidev.fun/emoji_icon_url_missing_type.json create mode 100644 test/fixtures/fedidev.fun/emoji_missing_icon.json create mode 100644 test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_emoji_test.exs diff --git a/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex b/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex index a412c8dea..cdab8d78d 100644 --- a/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex @@ -59,12 +59,29 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.TagValidator do end def changeset(struct, %{"type" => "Emoji"} = data) do - data = Map.put(data, "name", String.trim(data["name"], ":")) + data = + data + |> Map.put("name", String.trim(data["name"], ":")) + |> normalize_emoji_icon() - struct - |> cast(data, [:type, :name, :updated, :id]) - |> cast_embed(:icon, with: &icon_changeset/2) - |> validate_required([:type, :name, :icon]) + case data["icon"] do + %{"url" => url} when is_binary(url) -> + if valid_http_url?(url) do + struct + |> cast(data, [:type, :name, :updated, :id]) + |> cast_embed(:icon, with: &icon_changeset/2) + |> validate_required([:type, :name, :icon]) + else + struct + |> cast(data, []) + |> Map.put(:action, :ignore) + end + + _ -> + struct + |> cast(data, []) + |> Map.put(:action, :ignore) + end end def changeset(struct, %{"type" => "Link"} = data) do @@ -101,6 +118,41 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.TagValidator do defp infer_type(data), do: data + defp normalize_emoji_icon(%{"icon" => icon} = data) when is_binary(icon) do + Map.put(data, "icon", %{"type" => "Image", "url" => icon}) + end + + defp normalize_emoji_icon(%{"icon" => icon} = data) when is_map(icon) do + case extract_icon_url(icon) do + url when is_binary(url) -> Map.put(data, "icon", %{"type" => "Image", "url" => url}) + _ -> Map.delete(data, "icon") + end + end + + defp normalize_emoji_icon(data), do: data + + defp extract_icon_url(%{"url" => url}) when is_binary(url), do: url + + defp extract_icon_url(%{"url" => %{"href" => href}}) when is_binary(href), do: href + + defp extract_icon_url(%{"url" => [first | _]}) do + cond do + is_binary(first) -> first + is_map(first) -> first["href"] + true -> nil + end + end + + defp extract_icon_url(%{"href" => href}) when is_binary(href), do: href + + defp extract_icon_url(_), do: nil + + defp valid_http_url?(url) when is_binary(url) do + match?({:ok, _}, ObjectValidators.Uri.cast(url)) + end + + defp valid_http_url?(_), do: false + def icon_changeset(struct, data) do struct |> cast(data, [:type, :url]) diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index cc84cf8af..55b70b881 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -386,6 +386,24 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def fix_url(object), do: object + defp emoji_icon_url(icon) when is_binary(icon), do: icon + + defp emoji_icon_url(%{"url" => url}) when is_binary(url), do: url + + defp emoji_icon_url(%{"url" => %{"href" => href}}) when is_binary(href), do: href + + defp emoji_icon_url(%{"url" => [first | _]}) do + cond do + is_binary(first) -> first + is_map(first) -> first["href"] || first["url"] || first["id"] + true -> nil + end + end + + defp emoji_icon_url(%{"href" => href}) when is_binary(href), do: href + + defp emoji_icon_url(_), do: nil + def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do emoji = tags @@ -393,7 +411,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> Enum.reduce(%{}, fn data, mapping -> name = String.trim(data["name"], ":") - Map.put(mapping, name, data["icon"]["url"]) + case emoji_icon_url(data["icon"]) do + url when is_binary(url) -> Map.put(mapping, name, url) + _ -> mapping + end end) Map.put(object, "emoji", emoji) @@ -401,9 +422,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do name = String.trim(tag["name"], ":") - emoji = %{name => tag["icon"]["url"]} - Map.put(object, "emoji", emoji) + with url when is_binary(url) <- emoji_icon_url(tag["icon"]) do + Map.put(object, "emoji", %{name => url}) + else + _ -> object + end end def fix_emoji(object), do: object diff --git a/test/fixtures/fedidev.fun/emoji_icon_string.json b/test/fixtures/fedidev.fun/emoji_icon_string.json new file mode 100644 index 000000000..0be3deb8c --- /dev/null +++ b/test/fixtures/fedidev.fun/emoji_icon_string.json @@ -0,0 +1,32 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams" + ], + "type": "Create", + "id": "https://fedidev.fun/activity/emoji-icon-string", + "actor": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:40:00Z", + "object": { + "type": "Note", + "id": "https://fedidev.fun/objects/note-emoji-icon-string", + "attributedTo": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:40:00Z", + "content": "emoji icon as string :cow:", + "tag": [ + { + "type": "Emoji", + "name": ":cow:", + "icon": "https://fedidev.fun/static/cow.png" + } + ] + } +} + diff --git a/test/fixtures/fedidev.fun/emoji_icon_url_missing_type.json b/test/fixtures/fedidev.fun/emoji_icon_url_missing_type.json new file mode 100644 index 000000000..2837872eb --- /dev/null +++ b/test/fixtures/fedidev.fun/emoji_icon_url_missing_type.json @@ -0,0 +1,34 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams" + ], + "type": "Create", + "id": "https://fedidev.fun/activity/emoji-icon-url-missing-type", + "actor": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:40:01Z", + "object": { + "type": "Note", + "id": "https://fedidev.fun/objects/note-emoji-icon-url-missing-type", + "attributedTo": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:40:01Z", + "content": "emoji icon url missing type :cow2:", + "tag": [ + { + "type": "Emoji", + "name": ":cow2:", + "icon": { + "url": "https://fedidev.fun/static/cow.png" + } + } + ] + } +} + diff --git a/test/fixtures/fedidev.fun/emoji_missing_icon.json b/test/fixtures/fedidev.fun/emoji_missing_icon.json new file mode 100644 index 000000000..69af01a73 --- /dev/null +++ b/test/fixtures/fedidev.fun/emoji_missing_icon.json @@ -0,0 +1,31 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams" + ], + "type": "Create", + "id": "https://fedidev.fun/activity/emoji-missing-icon", + "actor": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:40:02Z", + "object": { + "type": "Note", + "id": "https://fedidev.fun/objects/note-emoji-missing-icon", + "attributedTo": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:40:02Z", + "content": "emoji missing icon :cow3:", + "tag": [ + { + "type": "Emoji", + "name": ":cow3:" + } + ] + } +} + diff --git a/test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_emoji_test.exs b/test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_emoji_test.exs new file mode 100644 index 000000000..56c305352 --- /dev/null +++ b/test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_emoji_test.exs @@ -0,0 +1,65 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.FedidevFunEmojiTest do + use Oban.Testing, repo: Pleroma.Repo + use Pleroma.DataCase + + alias Pleroma.Object + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Transmogrifier + + import Pleroma.Factory + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + defp ensure_remote_actor!(ap_id) when is_binary(ap_id) do + case User.get_cached_by_ap_id(ap_id) do + %User{} -> + :ok + + _ -> + insert(:user, + local: false, + ap_id: ap_id, + follower_address: ap_id <> "/followers", + following_address: ap_id <> "/following", + featured_address: ap_id <> "/collections/featured", + inbox: ap_id <> "/inbox" + ) + + :ok + end + end + + defp ingest_fixture!(rel_path) do + data = rel_path |> File.read!() |> Jason.decode!() + ensure_remote_actor!(data["actor"]) + + assert {:ok, activity} = Transmogrifier.handle_incoming(data) + object = Object.normalize(activity.data["object"], fetch: false) + + {activity, object} + end + + test "ingests Emoji tag with string icon" do + {_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/emoji_icon_string.json") + assert object.data["emoji"]["cow"] == "https://fedidev.fun/static/cow.png" + end + + test "ingests Emoji tag with icon missing type" do + {_activity, object} = + ingest_fixture!("test/fixtures/fedidev.fun/emoji_icon_url_missing_type.json") + + assert object.data["emoji"]["cow2"] == "https://fedidev.fun/static/cow.png" + end + + test "ignores Emoji tag missing icon" do + {_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/emoji_missing_icon.json") + assert object.data["emoji"] == %{} + end +end From 3b3e369f83940db06da4030242c9ccc2ba8897cf Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 17 Jan 2026 02:46:59 +0400 Subject: [PATCH 020/391] AP ingest: fill missing attributedTo from activity --- lib/pleroma/object/containment.ex | 30 ++++++++---- .../web/activity_pub/object_validator.ex | 2 + .../object_validators/common_fixes.ex | 11 +++++ .../fedidev.fun/missing_attributed_to.json | 25 ++++++++++ ...fedidev_fun_missing_attributed_to_test.exs | 49 +++++++++++++++++++ 5 files changed, 108 insertions(+), 9 deletions(-) create mode 100644 test/fixtures/fedidev.fun/missing_attributed_to.json create mode 100644 test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_missing_attributed_to_test.exs diff --git a/lib/pleroma/object/containment.ex b/lib/pleroma/object/containment.ex index 77fac12c0..b90d68636 100644 --- a/lib/pleroma/object/containment.ex +++ b/lib/pleroma/object/containment.ex @@ -15,12 +15,28 @@ defmodule Pleroma.Object.Containment do actor end + def get_actor(%{"actor" => nil, "attributedTo" => actor}) when not is_nil(actor) do + get_actor(%{"actor" => actor}) + end + + def get_actor(%{"actor" => nil}), do: nil + + def get_actor(%{"actor" => []}), do: nil + def get_actor(%{"actor" => actor}) when is_list(actor) do - if is_binary(Enum.at(actor, 0)) do - Enum.at(actor, 0) - else - Enum.find(actor, fn %{"type" => type} -> type in ["Person", "Service", "Application"] end) - |> Map.get("id") + case actor do + [first | _] when is_binary(first) -> + first + + _ -> + Enum.find_value(actor, fn + %{"type" => type, "id" => id} + when type in ["Person", "Service", "Application"] and is_binary(id) -> + id + + _ -> + nil + end) end end @@ -28,10 +44,6 @@ defmodule Pleroma.Object.Containment do id end - def get_actor(%{"actor" => nil, "attributedTo" => actor}) when not is_nil(actor) do - get_actor(%{"actor" => actor}) - end - def get_object(%{"object" => id}) when is_binary(id) do id end diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index 17652a0de..0254d6dc1 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -118,6 +118,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do when objtype in ~w[Question Answer Audio Video Image Event Article Note Page] do with {:ok, object_data} <- object + |> CommonFixes.maybe_set_attributed_to_from_activity(create_activity) |> CommonFixes.maybe_add_language_from_activity(create_activity) |> cast_and_apply_and_stringify_with_history(), meta = Keyword.put(meta, :object_data, object_data), @@ -172,6 +173,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do {_, {:ok, object_data, _}} <- {:object_validation, object + |> CommonFixes.maybe_set_attributed_to_from_activity(update_activity) |> CommonFixes.maybe_add_language_from_activity(update_activity) |> validate(meta)}, meta = Keyword.put(meta, :object_data, object_data), diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex index 631f301af..c5defd6f4 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex @@ -91,6 +91,17 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do |> Map.put("attributedTo", actor) end + def maybe_set_attributed_to_from_activity(object, activity) when is_map(object) do + if is_nil(object["attributedTo"]) and is_nil(object["actor"]) do + case Containment.get_actor(activity) do + actor when is_binary(actor) -> Map.put(object, "attributedTo", actor) + _ -> object + end + else + object + end + end + def fix_activity_context(data, %Object{data: %{"context" => object_context}}) do data |> Map.put("context", object_context) diff --git a/test/fixtures/fedidev.fun/missing_attributed_to.json b/test/fixtures/fedidev.fun/missing_attributed_to.json new file mode 100644 index 000000000..c2e644e84 --- /dev/null +++ b/test/fixtures/fedidev.fun/missing_attributed_to.json @@ -0,0 +1,25 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams" + ], + "type": "Create", + "id": "https://fedidev.fun/activity/missing-attributedto", + "actor": "https://fedidev.fun/users/alice", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:42:00Z", + "object": { + "type": "Note", + "id": "https://fedidev.fun/objects/note-missing-attributedto", + "attributedTo": null, + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [], + "published": "2025-11-21T14:42:00Z", + "content": "object without attributedTo" + } +} + diff --git a/test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_missing_attributed_to_test.exs b/test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_missing_attributed_to_test.exs new file mode 100644 index 000000000..c08e20fc9 --- /dev/null +++ b/test/pleroma/web/activity_pub/transmogrifier/fedidev_fun_missing_attributed_to_test.exs @@ -0,0 +1,49 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.FedidevFunMissingAttributedToTest do + use Oban.Testing, repo: Pleroma.Repo + use Pleroma.DataCase + + alias Pleroma.Object + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Transmogrifier + + import Pleroma.Factory + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + defp ensure_remote_actor!(ap_id) when is_binary(ap_id) do + case User.get_cached_by_ap_id(ap_id) do + %User{} -> + :ok + + _ -> + insert(:user, + local: false, + ap_id: ap_id, + follower_address: ap_id <> "/followers", + following_address: ap_id <> "/following", + featured_address: ap_id <> "/collections/featured", + inbox: ap_id <> "/inbox" + ) + + :ok + end + end + + test "inherits attributedTo from surrounding activity" do + data = File.read!("test/fixtures/fedidev.fun/missing_attributed_to.json") |> Jason.decode!() + ensure_remote_actor!(data["actor"]) + + assert {:ok, activity} = Transmogrifier.handle_incoming(data) + object = Object.normalize(activity.data["object"], fetch: false) + + assert object.data["attributedTo"] == data["actor"] + assert object.data["actor"] == data["actor"] + end +end From be327ca982654a37d0dc0f977a8e59ded9df1c33 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Wed, 14 Jan 2026 03:17:50 +0100 Subject: [PATCH 021/391] Switch Phoenix back to upstream See for `:sec_websocket_protocol` -> `:sec_websocket_headers` --- changelog.d/phoenix-sec-websocket-headers.change | 1 + lib/pleroma/web.ex | 2 +- lib/pleroma/web/endpoint.ex | 2 +- lib/pleroma/web/mastodon_api/websocket_handler.ex | 12 +++++++----- mix.exs | 3 +-- mix.lock | 2 +- 6 files changed, 12 insertions(+), 10 deletions(-) create mode 100644 changelog.d/phoenix-sec-websocket-headers.change diff --git a/changelog.d/phoenix-sec-websocket-headers.change b/changelog.d/phoenix-sec-websocket-headers.change new file mode 100644 index 000000000..b3cb4ad3f --- /dev/null +++ b/changelog.d/phoenix-sec-websocket-headers.change @@ -0,0 +1 @@ +Switch patched Phoenix 1.7.14 back to upstream with Phoenix 1.8.0+ diff --git a/lib/pleroma/web.ex b/lib/pleroma/web.ex index e7e7e96f9..1c1333aac 100644 --- a/lib/pleroma/web.ex +++ b/lib/pleroma/web.ex @@ -30,7 +30,7 @@ defmodule Pleroma.Web do def controller do quote do - use Phoenix.Controller, namespace: Pleroma.Web + use Phoenix.Controller, formats: [json: "View", html: "View"] import Plug.Conn diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index bab3c9fd0..fa0bd6c12 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -14,7 +14,7 @@ defmodule Pleroma.Web.Endpoint do websocket: [ path: "/", compress: false, - connect_info: [:sec_websocket_protocol], + connect_info: [:sec_websocket_headers], error_handler: {Pleroma.Web.MastodonAPI.WebsocketHandler, :handle_error, []}, fullsweep_after: 20 ] diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex index 3ed1cdd6c..616f35912 100644 --- a/lib/pleroma/web/mastodon_api/websocket_handler.ex +++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex @@ -245,12 +245,14 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do Plug.Conn.send_resp(conn, 404, "Not Found") end - defp find_access_token(%{ - connect_info: %{sec_websocket_protocol: [token]} - }), - do: token - defp find_access_token(%{params: %{"access_token" => token}}), do: token + defp find_access_token(%{connect_info: %{sec_websocket_headers: sec_headers}}), + do: + Enum.find_value(sec_headers, fn + {"sec-websocket-protocol", v} -> v + _ -> nil + end) + defp find_access_token(_), do: nil end diff --git a/mix.exs b/mix.exs index 48ec9b68f..9f45864e2 100644 --- a/mix.exs +++ b/mix.exs @@ -123,8 +123,7 @@ defmodule Pleroma.Mixfile do # Type `mix help deps` for examples and options. defp deps do [ - {:phoenix, - git: "https://github.com/feld/phoenix", branch: "v1.7.14-websocket-headers", override: true}, + {:phoenix, "~> 1.8.0"}, {:phoenix_ecto, "~> 4.4"}, {:ecto_sql, "~> 3.10"}, {:ecto_enum, "~> 1.4"}, diff --git a/mix.lock b/mix.lock index c469f4f01..60fa6f6b4 100644 --- a/mix.lock +++ b/mix.lock @@ -102,7 +102,7 @@ "open_api_spex": {:hex, :open_api_spex, "3.22.0", "fbf90dc82681dc042a4ee79853c8e989efbba73d9e87439085daf849bbf8bc20", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "dd751ddbdd709bb4a5313e9a24530da6e66594773c7242a0c2592cbd9f589063"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "1.2.1", "9cbe354b58121075bd20eb83076900a3832324b7dd171a6895fab57b6bb2752c", [:mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}], "hexpm", "d3b40a4a4630f0b442f19eca891fcfeeee4c40871936fed2f68e1c4faa30481f"}, - "phoenix": {:git, "https://github.com/feld/phoenix", "fb6dc76c657422e49600896c64aab4253fceaef6", [branch: "v1.7.14-websocket-headers"]}, + "phoenix": {:hex, :phoenix, "1.8.3", "49ac5e485083cb1495a905e47eb554277bdd9c65ccb4fc5100306b350151aa95", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, 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]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "36169f95cc2e155b78be93d9590acc3f462f1e5438db06e6248613f27c80caec"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.5", "c4ef322acd15a574a8b1a08eff0ee0a85e73096b53ce1403b6563709f15e1cea", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "26ec3208eef407f31b748cadd044045c6fd485fbff168e35963d2f9dfff28d4b"}, "phoenix_html": {:hex, :phoenix_html, "3.3.4", "42a09fc443bbc1da37e372a5c8e6755d046f22b9b11343bf885067357da21cb3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0249d3abec3714aff3415e7ee3d9786cb325be3151e6c4b3021502c585bf53fb"}, "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [: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]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"}, From 6f86883cca5024c57ba8b0ddecd6ca1161d62dd5 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Wed, 14 Jan 2026 05:15:28 +0100 Subject: [PATCH 022/391] Web: remove legacy :set_put_layout plug Phoenix 1.8 requires a View module with put_layout so can't set it that early. It was introduced in 2019 with commit 1097ce6d9f06a7552652c5990cee12e7b7b3cc59 but nothing seems to provide app.html (anymore?) and it would likely better be set by something like OAuthController / OAuthView. --- lib/pleroma/web.ex | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/pleroma/web.ex b/lib/pleroma/web.ex index 1c1333aac..bf7fa86a3 100644 --- a/lib/pleroma/web.ex +++ b/lib/pleroma/web.ex @@ -39,12 +39,6 @@ defmodule Pleroma.Web do alias Pleroma.Web.Router.Helpers, as: Routes - plug(:set_put_layout) - - defp set_put_layout(conn, _) do - put_layout(conn, Pleroma.Config.get(:app_layout, "app.html")) - end - # Marks plugs intentionally skipped and blocks their execution if present in plugs chain defp skip_plug(conn, plug_modules) do plug_modules From 10ab71d1577dd54719cdf81172707f4080a95d49 Mon Sep 17 00:00:00 2001 From: Dryusdan Date: Wed, 21 Jan 2026 17:55:31 +0100 Subject: [PATCH 023/391] Improve Debian 13 OTP dependencies and remove Debian 12 in source installation Cause of Elexir ~> 1.15 (mandatory in Pleroma 2.10) that don't support in Debian 12, I fix doc and replace Debian 12 by Debian 13 And replace a dependencies in OTP release + add a new dependencies for Debian 13. --- changelog.d/improve-doc-debian-13.change | 1 + docs/installation/debian_based_en.md | 4 ++-- docs/installation/otp_en.md | 6 +++++- 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 changelog.d/improve-doc-debian-13.change diff --git a/changelog.d/improve-doc-debian-13.change b/changelog.d/improve-doc-debian-13.change new file mode 100644 index 000000000..c8f62ef71 --- /dev/null +++ b/changelog.d/improve-doc-debian-13.change @@ -0,0 +1 @@ +Improve Debian 13 OTP dependencies and remove Debian 12 in source installation diff --git a/docs/installation/debian_based_en.md b/docs/installation/debian_based_en.md index 30f48792d..df45e8366 100644 --- a/docs/installation/debian_based_en.md +++ b/docs/installation/debian_based_en.md @@ -4,7 +4,7 @@ ## Installation -This guide will assume you are on Debian 12 (“bookworm”) or later. This guide should also work with Ubuntu 22.04 (“jammy”) and later. It also assumes that you have administrative rights, either as root or a user with [sudo permissions](https://www.digitalocean.com/community/tutorials/how-to-add-delete-and-grant-sudo-privileges-to-users-on-a-debian-vps). If you want to run this guide with root, ignore the `sudo` at the beginning of the lines, unless it calls a user like `sudo -Hu pleroma`; in this case, use `su -s $SHELL -c 'command'` instead. +This guide will assume you are on Debian 13 (“Trixie”) or later. This guide should also work with Ubuntu 24.04 (“noble”) and later. It also assumes that you have administrative rights, either as root or a user with [sudo permissions](https://www.digitalocean.com/community/tutorials/how-to-add-delete-and-grant-sudo-privileges-to-users-on-a-debian-vps). If you want to run this guide with root, ignore the `sudo` at the beginning of the lines, unless it calls a user like `sudo -Hu pleroma`; in this case, use `su -s $SHELL -c 'command'` instead. {! backend/installation/generic_dependencies.include !} @@ -69,7 +69,7 @@ cd /opt/pleroma sudo -Hu pleroma mix deps.get ``` -* Generate the configuration: +* Generate the configuration: ```shell sudo -Hu pleroma MIX_ENV=prod mix pleroma.instance gen diff --git a/docs/installation/otp_en.md b/docs/installation/otp_en.md index 123dccf3a..f3a571510 100644 --- a/docs/installation/otp_en.md +++ b/docs/installation/otp_en.md @@ -2,7 +2,7 @@ {! backend/installation/otp_vs_from_source.include !} -This guide covers a installation using OTP releases as built by the Pleroma project, it is meant as a fallback to distribution packages/recipes which are the preferred installation method. +This guide covers a installation using OTP releases as built by the Pleroma project, it is meant as a fallback to distribution packages/recipes which are the preferred installation method. To install Pleroma from source, please check out the corresponding guide for your distro. ## Pre-requisites @@ -48,6 +48,10 @@ Other than things bundled in the OTP release Pleroma depends on: ``` apt install curl unzip libncurses5 postgresql postgresql-contrib nginx certbot libmagic-dev ``` + On Debian 13, replace `libncurses5` by `libncurses6` and add `libvips42t64`. So + ``` + apt install curl unzip libncurses6 libvips42t64 postgresql postgresql-contrib nginx certbot libmagic-dev + ``` ### Installing optional packages From 145b38b3149acb93bf1cf72693fe881f8b3eaa74 Mon Sep 17 00:00:00 2001 From: Dryusdan Date: Wed, 21 Jan 2026 20:15:55 +0100 Subject: [PATCH 024/391] Reset deleted double space --- docs/installation/debian_based_en.md | 2 +- docs/installation/otp_en.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation/debian_based_en.md b/docs/installation/debian_based_en.md index df45e8366..b6730ce19 100644 --- a/docs/installation/debian_based_en.md +++ b/docs/installation/debian_based_en.md @@ -69,7 +69,7 @@ cd /opt/pleroma sudo -Hu pleroma mix deps.get ``` -* Generate the configuration: +* Generate the configuration: ```shell sudo -Hu pleroma MIX_ENV=prod mix pleroma.instance gen diff --git a/docs/installation/otp_en.md b/docs/installation/otp_en.md index f3a571510..4b88b11e0 100644 --- a/docs/installation/otp_en.md +++ b/docs/installation/otp_en.md @@ -2,7 +2,7 @@ {! backend/installation/otp_vs_from_source.include !} -This guide covers a installation using OTP releases as built by the Pleroma project, it is meant as a fallback to distribution packages/recipes which are the preferred installation method. +This guide covers a installation using OTP releases as built by the Pleroma project, it is meant as a fallback to distribution packages/recipes which are the preferred installation method. To install Pleroma from source, please check out the corresponding guide for your distro. ## Pre-requisites From 177edbbb2c7258876e1c204130cd3f47667c7e11 Mon Sep 17 00:00:00 2001 From: Dryusdan Date: Thu, 22 Jan 2026 08:50:50 +0100 Subject: [PATCH 025/391] Add more RUM versions --- docs/installation/otp_en.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/installation/otp_en.md b/docs/installation/otp_en.md index 4b88b11e0..d11cccccb 100644 --- a/docs/installation/otp_en.md +++ b/docs/installation/otp_en.md @@ -48,7 +48,7 @@ Other than things bundled in the OTP release Pleroma depends on: ``` apt install curl unzip libncurses5 postgresql postgresql-contrib nginx certbot libmagic-dev ``` - On Debian 13, replace `libncurses5` by `libncurses6` and add `libvips42t64`. So + On Debian 13 and Ubuntu 24.04, replace `libncurses5` by `libncurses6` and add `libvips42t64`. So ``` apt install curl unzip libncurses6 libvips42t64 postgresql postgresql-contrib nginx certbot libmagic-dev ``` @@ -93,8 +93,12 @@ RUM indexes are an alternative indexing scheme that is not included in PostgreSQ === "Debian/Ubuntu" ``` - # Available only on Buster/19.04 - apt install postgresql-11-rum + # On Trixie and Ubuntu 25.04 + apt install postgresql-17-rum + ``` + # On Ubuntu 24.04 + ``` + apt install postgresql-16-rum ``` #### (Optional) Performance configuration From ce188baa924d6275393147d80ffd804cb71895f8 Mon Sep 17 00:00:00 2001 From: Dryusdan Date: Thu, 22 Jan 2026 08:55:14 +0100 Subject: [PATCH 026/391] Add more Postgresql-rum version --- docs/installation/otp_en.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/installation/otp_en.md b/docs/installation/otp_en.md index d11cccccb..a1238b160 100644 --- a/docs/installation/otp_en.md +++ b/docs/installation/otp_en.md @@ -96,10 +96,18 @@ RUM indexes are an alternative indexing scheme that is not included in PostgreSQ # On Trixie and Ubuntu 25.04 apt install postgresql-17-rum ``` + # On Debian 12 + ``` + apt install postgresql-15-rum + ``` # On Ubuntu 24.04 ``` apt install postgresql-16-rum ``` + # On Ubuntu 22.04 + ``` + apt install postgresql-14-rum + ``` #### (Optional) Performance configuration It is encouraged to check [Optimizing your PostgreSQL performance](../configuration/postgresql.md) document, for tips on PostgreSQL tuning. From 9901e6367e760f5022397224aecf4aab31db10cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Sun, 25 Jan 2026 19:47:21 +0100 Subject: [PATCH 027/391] Fix event type field name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- lib/pleroma/web/admin_api/views/webhook_view.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/admin_api/views/webhook_view.ex b/lib/pleroma/web/admin_api/views/webhook_view.ex index 552aba4f1..0b839090b 100644 --- a/lib/pleroma/web/admin_api/views/webhook_view.ex +++ b/lib/pleroma/web/admin_api/views/webhook_view.ex @@ -26,7 +26,7 @@ defmodule Pleroma.Web.AdminAPI.WebhookView do def render("event.json", %{type: type, object: object}) do %{ - type: type, + event: type, created_at: Utils.to_masto_date(NaiveDateTime.utc_now()), object: object } From 8eca6ca87c0f3ee7482fe7211d9dd1bedf71c7f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 27 Feb 2026 23:11:37 +0100 Subject: [PATCH 028/391] webhook fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../controllers/webhook_controller.ex | 6 ++-- .../mastodon_api/admin/views/account_view.ex | 2 +- lib/pleroma/webhook.ex | 30 ++++++++----------- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/lib/pleroma/web/admin_api/controllers/webhook_controller.ex b/lib/pleroma/web/admin_api/controllers/webhook_controller.ex index dd9712a31..cc606916e 100644 --- a/lib/pleroma/web/admin_api/controllers/webhook_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/webhook_controller.ex @@ -40,17 +40,18 @@ defmodule Pleroma.Web.AdminAPI.WebhookController do end def create(%{body_params: params} = conn, _) do - with webhook <- Webhook.create(params) do + with {:ok, webhook} <- Webhook.create(params) do render(conn, "show.json", webhook: webhook) end end def update(%{body_params: params} = conn, %{id: id}) do with %Webhook{internal: false} = webhook <- Webhook.get(id), - webhook <- Webhook.update(webhook, params) do + {:ok, webhook} <- Webhook.update(webhook, params) do render(conn, "show.json", webhook: webhook) else %Webhook{internal: true} -> {:error, :forbidden} + nil -> {:error, :not_found} end end @@ -60,6 +61,7 @@ defmodule Pleroma.Web.AdminAPI.WebhookController do render(conn, "show.json", webhook: webhook) else %Webhook{internal: true} -> {:error, :forbidden} + nil -> {:error, :not_found} end end diff --git a/lib/pleroma/web/mastodon_api/admin/views/account_view.ex b/lib/pleroma/web/mastodon_api/admin/views/account_view.ex index 40fac94ca..bf36d2c97 100644 --- a/lib/pleroma/web/mastodon_api/admin/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/admin/views/account_view.ex @@ -27,7 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.Admin.AccountView do role: role(user), confirmed: user.is_confirmed, sensitized: nil, - suspened: nil, + suspended: nil, silenced: nil, disabled: !user.is_active, approved: user.is_approved, diff --git a/lib/pleroma/webhook.ex b/lib/pleroma/webhook.ex index f1e1b7678..83ce344fa 100644 --- a/lib/pleroma/webhook.ex +++ b/lib/pleroma/webhook.ex @@ -28,6 +28,7 @@ defmodule Pleroma.Webhook do def get_by_type(type) do __MODULE__ |> where([w], ^type in w.events) + |> where([w], w.enabled == true) |> Repo.all() end @@ -48,21 +49,15 @@ defmodule Pleroma.Webhook do end def create(params) do - {:ok, webhook} = - %__MODULE__{} - |> changeset(params) - |> Repo.insert() - - webhook + %__MODULE__{} + |> changeset(params) + |> Repo.insert() end def update(%__MODULE__{} = webhook, params) do - {:ok, webhook} = - webhook - |> update_changeset(params) - |> Repo.update() - webhook + |> update_changeset(params) + |> Repo.update() end def delete(webhook), do: webhook |> Repo.delete() @@ -80,12 +75,13 @@ defmodule Pleroma.Webhook do |> Repo.update() end - defp strip_events(params) do - if Map.has_key?(params, :events) do - params - |> Map.put(:events, Enum.filter(params[:events], &Enum.member?(@event_types, &1))) - else - params + defp strip_events(changeset) do + case get_change(changeset, :events) do + nil -> + changeset + + events -> + put_change(changeset, :events, Enum.filter(events, &(&1 in @event_types))) end end From e4f2f0994c395933b6f3590e60ec2743d531b1b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 27 Feb 2026 23:13:15 +0100 Subject: [PATCH 029/391] Use encode16(case: :lower) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- lib/pleroma/webhook/notify.ex | 2 +- test/pleroma/webhook/notify_test.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/webhook/notify.ex b/lib/pleroma/webhook/notify.ex index 7dbfc52a0..743bb4012 100644 --- a/lib/pleroma/webhook/notify.ex +++ b/lib/pleroma/webhook/notify.ex @@ -78,6 +78,6 @@ defmodule Pleroma.Webhook.Notify do end defp signature(body, secret) do - :crypto.mac(:hmac, :sha256, secret, body) |> Base.encode16() + :crypto.mac(:hmac, :sha256, secret, body) |> Base.encode16(case: :lower) end end diff --git a/test/pleroma/webhook/notify_test.exs b/test/pleroma/webhook/notify_test.exs index e03890aab..c99489940 100644 --- a/test/pleroma/webhook/notify_test.exs +++ b/test/pleroma/webhook/notify_test.exs @@ -66,7 +66,7 @@ defmodule Pleroma.Webhook.NotifyTest do {"X-Hub-Signature", "sha256=" <> signature} = Enum.find(headers, fn {key, _} -> key == "X-Hub-Signature" end) - assert signature == :crypto.mac(:hmac, :sha256, secret, body) |> Base.encode16() + assert signature == :crypto.mac(:hmac, :sha256, secret, body) |> Base.encode16(case: :lower) %Tesla.Env{status: 200, body: ""} end) From 3a87250e15f46b276171f93ba99b8475aa483ebf Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Tue, 3 Mar 2026 12:17:34 +0400 Subject: [PATCH 030/391] Add ParadeDB external search backend Introduce an optional search backend that indexes public/unlisted Notes into a separate ParadeDB (pg_search) Postgres and queries it for status search results. --- changelog.d/paradedb-search.add | 1 + config/config.exs | 8 + config/description.exs | 29 +++- docs/configuration/search.md | 34 ++++ lib/pleroma/application.ex | 23 ++- lib/pleroma/search/paradedb.ex | 213 ++++++++++++++++++++++++++ lib/pleroma/search/paradedb/client.ex | 24 +++ lib/pleroma/search/paradedb/repo.ex | 24 +++ test/pleroma/search/paradedb_test.exs | 168 ++++++++++++++++++++ test/support/mocks.ex | 2 + 10 files changed, 520 insertions(+), 6 deletions(-) create mode 100644 changelog.d/paradedb-search.add create mode 100644 lib/pleroma/search/paradedb.ex create mode 100644 lib/pleroma/search/paradedb/client.ex create mode 100644 lib/pleroma/search/paradedb/repo.ex create mode 100644 test/pleroma/search/paradedb_test.exs diff --git a/changelog.d/paradedb-search.add b/changelog.d/paradedb-search.add new file mode 100644 index 000000000..00a6e2361 --- /dev/null +++ b/changelog.d/paradedb-search.add @@ -0,0 +1 @@ +Add ParadeDB (pg_search) as an optional external search backend. diff --git a/config/config.exs b/config/config.exs index 683805fe3..7e374b22b 100644 --- a/config/config.exs +++ b/config/config.exs @@ -938,6 +938,14 @@ config :pleroma, Pleroma.Search.Meilisearch, private_key: nil, initial_indexing_chunk_size: 100_000 +config :pleroma, Pleroma.Search.ParadeDB, + url: nil, + table: "pleroma_search_documents" + +config :pleroma, Pleroma.Search.ParadeDB.Repo, + pool_size: 10, + prepare: :unnamed + config :pleroma, Pleroma.Application, background_migrators: true, internal_fetch: true, diff --git a/config/description.exs b/config/description.exs index c388d17c3..de454dfb2 100644 --- a/config/description.exs +++ b/config/description.exs @@ -3503,7 +3503,12 @@ config :pleroma, :config_description, [ key: :module, type: :keyword, description: "Selected search module.", - suggestion: [Pleroma.Search.DatabaseSearch, Pleroma.Search.Meilisearch] + suggestion: [ + Pleroma.Search.DatabaseSearch, + Pleroma.Search.Meilisearch, + Pleroma.Search.QdrantSearch, + Pleroma.Search.ParadeDB + ] } ] }, @@ -3536,6 +3541,28 @@ config :pleroma, :config_description, [ } ] }, + %{ + group: :pleroma, + key: Pleroma.Search.ParadeDB, + type: :group, + description: "ParadeDB settings.", + children: [ + %{ + key: :url, + type: :string, + description: + "ParadeDB database URL (separate Postgres instance). " <> + "Can also be set via `PARADEDB_DATABASE_URL`.", + suggestion: ["postgres://postgres:postgres@127.0.0.1:5432/paradedb"] + }, + %{ + key: :table, + type: :string, + description: "Table name used for search documents.", + suggestion: ["pleroma_search_documents"] + } + ] + }, %{ group: :pleroma, key: Pleroma.Language.LanguageDetector, diff --git a/docs/configuration/search.md b/docs/configuration/search.md index d34f84d4f..780904d10 100644 --- a/docs/configuration/search.md +++ b/docs/configuration/search.md @@ -145,3 +145,37 @@ This will clear **all** the posts from the search index. Note, that deleted post there is no need to actually clear the whole index, unless you want **all** of it gone. That said, the index does not hold any information that cannot be re-created from the database, it should also generally be a lot smaller than the size of your database. Still, the size depends on the amount of text in posts. + +## ParadeDB + +[ParadeDB](https://www.paradedb.com/) is a Postgres extension that provides BM25 full-text search. In Pleroma, it can be used as an +external search backend by pointing Pleroma at a separate Postgres instance with the `pg_search` extension installed. + +Pleroma will maintain a small `pleroma_search_documents` table in that database (via the existing search indexing queue) and run search +queries against it. + +To use it, set the search module to `Pleroma.Search.ParadeDB` and configure the ParadeDB database URL: + +> config :pleroma, Pleroma.Search, module: Pleroma.Search.ParadeDB +> +> config :pleroma, Pleroma.Search.ParadeDB, +> url: System.get_env("PARADEDB_DATABASE_URL") || "postgres://postgres:postgres@127.0.0.1:5432/paradedb", +> table: "pleroma_search_documents" + +Then, create the table and BM25 index once: + +=== "From Source" + ```sh + mix pleroma.search.indexer create_index + ``` + +After that, run an initial indexing pass to backfill existing posts: + +=== "From Source" + ```sh + mix pleroma.search.indexer index + ``` + +Note: Like the other external search backends, only public/unlisted Notes are indexed. + +Indexing runs via the Oban `search_indexing` queue, so ensure that queue is enabled (not paused) in your Oban configuration. diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 8e1c5de0d..8de71b816 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -80,12 +80,15 @@ defmodule Pleroma.Application do [ Pleroma.PromEx, Pleroma.LDAP, - Pleroma.Repo, - Config.TransferTask, - Pleroma.Emoji, - Pleroma.Web.Plugs.RateLimiter.Supervisor, - {Task.Supervisor, name: Pleroma.TaskSupervisor} + Pleroma.Repo ] ++ + search_children() ++ + [ + Config.TransferTask, + Pleroma.Emoji, + Pleroma.Web.Plugs.RateLimiter.Supervisor, + {Task.Supervisor, name: Pleroma.TaskSupervisor} + ] ++ cachex_children() ++ http_children(adapter) ++ [ @@ -159,6 +162,16 @@ defmodule Pleroma.Application do ] end + defp search_children do + case Config.get([Pleroma.Search, :module]) do + Pleroma.Search.ParadeDB -> + [Pleroma.Search.ParadeDB.Repo] + + _ -> + [] + end + end + defp emoji_packs_expiration, do: expiration(default: :timer.seconds(5 * 60), interval: :timer.seconds(60)) diff --git a/lib/pleroma/search/paradedb.ex b/lib/pleroma/search/paradedb.ex new file mode 100644 index 000000000..963d573e8 --- /dev/null +++ b/lib/pleroma/search/paradedb.ex @@ -0,0 +1,213 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Search.ParadeDB do + @behaviour Pleroma.Search.SearchBackend + + require Logger + + import Ecto.Query + + import Pleroma.Search.DatabaseSearch, + only: [ + maybe_fetch: 3, + maybe_restrict_author: 2, + maybe_restrict_blocked: 2, + maybe_restrict_local: 2 + ] + + import Pleroma.Search.Meilisearch, only: [object_to_search_data: 1] + + alias Pleroma.Activity + alias Pleroma.Config.Getting, as: Config + + @max_limit 40 + @default_table "pleroma_search_documents" + + defp client_impl do + Config.get([Pleroma.Search.ParadeDB, :client_impl]) || + Pleroma.Search.ParadeDB.Client.Postgres + end + + defp table do + Config.get([Pleroma.Search.ParadeDB, :table], @default_table) + end + + @impl true + def create_index do + with {:ok, _} <- client_impl().query("CREATE EXTENSION IF NOT EXISTS pg_search", []), + {:ok, _} <- client_impl().query(create_table_sql(table()), []), + {:ok, _} <- client_impl().query(create_object_id_index_sql(table()), []), + {:ok, _} <- client_impl().query(create_bm25_index_sql(table()), []) do + :ok + else + {:error, error} -> + Logger.error("ParadeDB create_index failed: #{inspect(error)}") + {:error, error} + end + end + + @impl true + def drop_index do + with {:ok, _} <- client_impl().query("DROP TABLE IF EXISTS #{table()}", []) do + :ok + else + {:error, error} -> + Logger.error("ParadeDB drop_index failed: #{inspect(error)}") + {:error, error} + end + end + + @impl true + def healthcheck_endpoints, do: nil + + @impl true + def add_to_index(activity) do + maybe_search_data = object_to_search_data(activity.object) + + if activity.data["type"] == "Create" and maybe_search_data do + actor_ap_id = activity.data["actor"] + published_at = DateTime.from_unix!(maybe_search_data.published) + + sql = + """ + INSERT INTO #{table()} (id, object_id, object_ap_id, actor_ap_id, content, published_at) + VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT (id) DO UPDATE SET + object_id = EXCLUDED.object_id, + object_ap_id = EXCLUDED.object_ap_id, + actor_ap_id = EXCLUDED.actor_ap_id, + content = EXCLUDED.content, + published_at = EXCLUDED.published_at + """ + + params = [ + activity.id, + maybe_search_data.id, + maybe_search_data.ap, + actor_ap_id, + maybe_search_data.content, + published_at + ] + + case client_impl().query(sql, params) do + {:ok, _} -> + :ok + + {:error, error} -> + Logger.error("ParadeDB add_to_index failed: #{inspect(error)}") + {:error, error} + end + else + :ok + end + end + + @impl true + def remove_from_index(object) do + sql = "DELETE FROM #{table()} WHERE object_id = $1" + + case client_impl().query(sql, [object.id]) do + {:ok, _} -> + :ok + + {:error, error} -> + Logger.error("ParadeDB remove_from_index failed: #{inspect(error)}") + {:error, error} + end + end + + @impl true + def search(user, query, options \\ []) do + limit = options |> Keyword.get(:limit, @max_limit) |> min(@max_limit) + offset = Keyword.get(options, :offset, 0) + author = Keyword.get(options, :author) + + case search_ids(query, limit, offset, author) do + {:ok, []} -> + maybe_fetch([], user, query) + + {:ok, ids} -> + ids + |> fetch_activities(user, query, author) + + {:error, error} -> + Logger.error("ParadeDB search failed: #{inspect(error)}") + maybe_fetch([], user, query) + end + end + + defp fetch_activities(ids, user, query, author) do + dumped_ids = Enum.map(ids, &FlakeId.from_string/1) + + from(a in Activity, where: a.id in ^dumped_ids) + |> Activity.with_preloaded_object() + |> Activity.restrict_deactivated_users() + |> maybe_restrict_local(user) + |> maybe_restrict_author(author) + |> maybe_restrict_blocked(user) + |> order_by( + [a], + fragment( + "array_position(?, ?)", + ^dumped_ids, + a.id + ) + ) + |> Pleroma.Repo.all() + |> maybe_fetch(user, query) + end + + defp search_ids(query, limit, offset, author) do + base_sql = "SELECT id FROM #{table()} WHERE content &&& $1" + params = [query] + + {base_sql, params} = + case author do + %Pleroma.User{ap_id: ap_id} when is_binary(ap_id) -> + {base_sql <> " AND actor_ap_id = $2", params ++ [ap_id]} + + _ -> + {base_sql, params} + end + + limit_arg = length(params) + 1 + offset_arg = length(params) + 2 + + sql = + base_sql <> + " ORDER BY published_at DESC LIMIT $#{limit_arg} OFFSET $#{offset_arg}" + + params = params ++ [limit, offset] + + with {:ok, %{rows: rows}} <- client_impl().query(sql, params) do + {:ok, Enum.map(rows, fn [id] -> id end)} + end + end + + defp create_table_sql(table) do + """ + CREATE TABLE IF NOT EXISTS #{table} ( + id text PRIMARY KEY, + object_id bigint NOT NULL, + object_ap_id text NOT NULL, + actor_ap_id text, + content text NOT NULL, + published_at timestamptz + ) + """ + end + + defp create_object_id_index_sql(table) do + "CREATE INDEX IF NOT EXISTS #{table}_object_id_idx ON #{table} (object_id)" + end + + defp create_bm25_index_sql(table) do + """ + CREATE INDEX IF NOT EXISTS #{table}_bm25_idx ON #{table} + USING bm25 (id, content, (actor_ap_id::pdb.literal), published_at) + WITH (key_field = 'id') + """ + end +end diff --git a/lib/pleroma/search/paradedb/client.ex b/lib/pleroma/search/paradedb/client.ex new file mode 100644 index 000000000..d6dce0dd4 --- /dev/null +++ b/lib/pleroma/search/paradedb/client.ex @@ -0,0 +1,24 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Search.ParadeDB.Client do + @moduledoc false + + @type sql :: String.t() + @type params :: list() + @type result :: %{optional(:rows) => list(list())} + + @callback query(sql(), params()) :: {:ok, result()} | {:error, any()} +end + +defmodule Pleroma.Search.ParadeDB.Client.Postgres do + @moduledoc false + + @behaviour Pleroma.Search.ParadeDB.Client + + @impl true + def query(sql, params \\ []) do + Ecto.Adapters.SQL.query(Pleroma.Search.ParadeDB.Repo, sql, params) + end +end diff --git a/lib/pleroma/search/paradedb/repo.ex b/lib/pleroma/search/paradedb/repo.ex new file mode 100644 index 000000000..684271847 --- /dev/null +++ b/lib/pleroma/search/paradedb/repo.ex @@ -0,0 +1,24 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Search.ParadeDB.Repo do + use Ecto.Repo, + otp_app: :pleroma, + adapter: Ecto.Adapters.Postgres, + migration_timestamps: [type: :naive_datetime_usec] + + @doc """ + Loads the ParadeDB database URL from `PARADEDB_DATABASE_URL`. + + If the env var is not set, falls back to `config :pleroma, Pleroma.Search.ParadeDB, url: ...`. + """ + def init(_, opts) do + config = Application.get_env(:pleroma, Pleroma.Search.ParadeDB, []) + + url = System.get_env("PARADEDB_DATABASE_URL") || config[:url] + url = if is_nil(url), do: nil, else: to_string(url) + + {:ok, Keyword.put(opts, :url, url)} + end +end diff --git a/test/pleroma/search/paradedb_test.exs b/test/pleroma/search/paradedb_test.exs new file mode 100644 index 000000000..c55e2aa47 --- /dev/null +++ b/test/pleroma/search/paradedb_test.exs @@ -0,0 +1,168 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Search.ParadeDBTest do + use Pleroma.DataCase, async: true + use Oban.Testing, repo: Pleroma.Repo + + import Mox + import Pleroma.Factory + + alias Pleroma.Search.ParadeDB + alias Pleroma.Search.ParadeDB.ClientMock + alias Pleroma.UnstubbedConfigMock, as: Config + alias Pleroma.Web.CommonAPI + alias Pleroma.Workers.SearchIndexingWorker + + describe "ParadeDB" do + test "indexes a public post on creation" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "guys i just don't wanna leave the swamp", + visibility: "public" + }) + + args = %{"op" => "add_to_index", "activity" => activity.id} + + assert_enqueued(worker: SearchIndexingWorker, args: args) + + ClientMock + |> expect(:query, fn sql, params -> + assert sql =~ "INSERT INTO pleroma_search_documents" + + [activity_id, object_id, object_ap_id, actor_ap_id, content, published_at] = params + + assert activity_id == activity.id + assert is_integer(object_id) + assert is_binary(object_ap_id) + assert actor_ap_id == activity.data["actor"] + assert content == "guys i just don't wanna leave the swamp" + assert %DateTime{} = published_at + + send(self(), :inserted) + {:ok, %{}} + end) + + Config + |> expect(:get, 3, fn + [Pleroma.Search, :module], nil -> + ParadeDB + + [Pleroma.Search.ParadeDB, :client_impl], nil -> + ClientMock + + [Pleroma.Search.ParadeDB, :table], "pleroma_search_documents" -> + "pleroma_search_documents" + end) + + assert :ok = perform_job(SearchIndexingWorker, args) + assert_received(:inserted) + end + + test "doesn't index posts that are not public" do + user = insert(:user) + + Enum.each(["private", "direct"], fn visibility -> + {:ok, activity} = + CommonAPI.post(user, %{ + status: "guys i just don't wanna leave the swamp", + visibility: visibility + }) + + args = %{"op" => "add_to_index", "activity" => activity.id} + assert_enqueued(worker: SearchIndexingWorker, args: args) + + Config + |> expect(:get, fn + [Pleroma.Search, :module], nil -> + ParadeDB + end) + + assert :ok = perform_job(SearchIndexingWorker, args) + end) + end + + test "deletes posts from index when deleted locally" do + user = insert(:user) + + ClientMock + |> expect(:query, 2, fn sql, params -> + cond do + String.contains?(sql, "INSERT INTO pleroma_search_documents") -> + send(self(), :inserted) + {:ok, %{}} + + String.contains?(sql, "DELETE FROM pleroma_search_documents") -> + assert [object_id] = params + assert is_integer(object_id) + + send(self(), :deleted) + {:ok, %{}} + + true -> + flunk("Unexpected ParadeDB SQL: #{inspect(sql)}") + end + end) + + Config + |> expect(:get, 6, fn + [Pleroma.Search, :module], nil -> + ParadeDB + + [Pleroma.Search.ParadeDB, :client_impl], nil -> + ClientMock + + [Pleroma.Search.ParadeDB, :table], "pleroma_search_documents" -> + "pleroma_search_documents" + end) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "guys i just don't wanna leave the swamp", + visibility: "public" + }) + + args = %{"op" => "add_to_index", "activity" => activity.id} + assert_enqueued(worker: SearchIndexingWorker, args: args) + assert :ok = perform_job(SearchIndexingWorker, args) + assert_received(:inserted) + + {:ok, _} = CommonAPI.delete(activity.id, user) + + delete_args = %{"op" => "remove_from_index", "object" => activity.object.id} + assert_enqueued(worker: SearchIndexingWorker, args: delete_args) + assert :ok = perform_job(SearchIndexingWorker, delete_args) + assert_received(:deleted) + end + + test "search returns activities in backend order" do + user = insert(:user) + + {:ok, activity1} = CommonAPI.post(user, %{status: "first swamp", visibility: "public"}) + {:ok, activity2} = CommonAPI.post(user, %{status: "second swamp", visibility: "public"}) + + ClientMock + |> expect(:query, fn sql, params -> + assert sql =~ "SELECT id FROM pleroma_search_documents" + assert ["swamp", _limit, _offset] = params + + {:ok, %{rows: [[activity2.id], [activity1.id]]}} + end) + + Config + |> expect(:get, 2, fn + [Pleroma.Search.ParadeDB, :client_impl], nil -> + ClientMock + + [Pleroma.Search.ParadeDB, :table], "pleroma_search_documents" -> + "pleroma_search_documents" + end) + + assert [%{id: ^activity2.id}, %{id: ^activity1.id}] = + ParadeDB.search(nil, "swamp", limit: 40, offset: 0) + end + end +end diff --git a/test/support/mocks.ex b/test/support/mocks.ex index 7e1244220..84465509a 100644 --- a/test/support/mocks.ex +++ b/test/support/mocks.ex @@ -46,3 +46,5 @@ Mox.defmock(Pleroma.SignatureMock, for: Pleroma.Signature.API) Mox.defmock(Pleroma.Web.ActivityPub.TransmogrifierMock, for: Pleroma.Web.ActivityPub.Transmogrifier.API ) + +Mox.defmock(Pleroma.Search.ParadeDB.ClientMock, for: Pleroma.Search.ParadeDB.Client) From 911d84411dc4eabe8bbca8d90cb898ca9f52f4bd Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Tue, 3 Mar 2026 12:36:01 +0400 Subject: [PATCH 031/391] Add ParadeDB docker integration test Add a skipped-by-default integration test that boots a ParadeDB container, creates the BM25 index, indexes posts, and verifies search results against the real backend. --- .../search/paradedb_integration_test.exs | 209 ++++++++++++++++++ test/test_helper.exs | 2 +- 2 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 test/pleroma/search/paradedb_integration_test.exs diff --git a/test/pleroma/search/paradedb_integration_test.exs b/test/pleroma/search/paradedb_integration_test.exs new file mode 100644 index 000000000..04e6a9d59 --- /dev/null +++ b/test/pleroma/search/paradedb_integration_test.exs @@ -0,0 +1,209 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Search.ParadeDBIntegrationTest do + use Pleroma.DataCase, async: false + + @moduletag :integration + @moduletag timeout: 180_000 + + import Mox + import Pleroma.Factory + + alias Pleroma.Activity + alias Pleroma.Search.ParadeDB + alias Pleroma.Web.CommonAPI + + @image "paradedb/paradedb:latest" + @db_user "postgres" + @db_password "postgres" + @db_name "paradedb" + @docker_startup_timeout_ms 60_000 + + setup do + Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) + :ok + end + + setup_all do + with :ok <- ensure_docker_available() do + container = "pleroma-paradedb-it-#{System.unique_integer([:positive])}" + + on_exit(fn -> + _ = docker(["rm", "-f", container]) + end) + + {id, 0} = + docker([ + "run", + "--rm", + "--name", + container, + "-e", + "POSTGRES_USER=#{@db_user}", + "-e", + "POSTGRES_PASSWORD=#{@db_password}", + "-e", + "POSTGRES_DB=#{@db_name}", + "-p", + "5432", + "-d", + @image + ]) + + container_id = String.trim(id) + + host_port = + container + |> wait_for_port!(@docker_startup_timeout_ms) + + wait_for_pg_ready!(container, @docker_startup_timeout_ms) + + url = "postgres://#{@db_user}:#{@db_password}@127.0.0.1:#{host_port}/#{@db_name}" + + prev_env = System.get_env("PARADEDB_DATABASE_URL") + System.put_env("PARADEDB_DATABASE_URL", url) + + on_exit(fn -> + if is_nil(prev_env) do + System.delete_env("PARADEDB_DATABASE_URL") + else + System.put_env("PARADEDB_DATABASE_URL", prev_env) + end + end) + + {:ok, _pid} = start_supervised(Pleroma.Search.ParadeDB.Repo) + + table = "pleroma_search_documents_it_#{System.unique_integer([:positive])}" + Pleroma.Config.put([Pleroma.Search.ParadeDB, :table], table) + + {:ok, + %{ + container: container, + container_id: container_id, + url: url, + table: table + }} + end + end + + test "indexes and searches against real ParadeDB" do + :ok = ParadeDB.drop_index() + assert :ok = ParadeDB.create_index() + + user = insert(:user) + + {:ok, a1} = + CommonAPI.post(user, %{status: "paradedb integration first", visibility: "public"}) + + :timer.sleep(1100) + + {:ok, a2} = + CommonAPI.post(user, %{status: "paradedb integration second", visibility: "public"}) + + {:ok, private} = + CommonAPI.post(user, %{status: "paradedb integration private", visibility: "private"}) + + a1 = Activity.get_by_id_with_object(a1.id) + a2 = Activity.get_by_id_with_object(a2.id) + private = Activity.get_by_id_with_object(private.id) + + assert :ok = ParadeDB.add_to_index(a1) + assert :ok = ParadeDB.add_to_index(a2) + assert :ok = ParadeDB.add_to_index(private) + + results = ParadeDB.search(nil, "paradedb integration", limit: 40, offset: 0) + ids = Enum.map(results, & &1.id) + + assert a1.id in ids + assert a2.id in ids + refute private.id in ids + assert List.first(ids) == a2.id + + # Removing an object should remove it from the ParadeDB index. + assert :ok = ParadeDB.remove_from_index(a2.object) + + results_after_delete = ParadeDB.search(nil, "paradedb integration", limit: 40, offset: 0) + ids_after_delete = Enum.map(results_after_delete, & &1.id) + + assert a1.id in ids_after_delete + refute a2.id in ids_after_delete + end + + defp ensure_docker_available do + if is_nil(System.find_executable("docker")) do + {:skip, "Docker not installed"} + else + case docker(["info"]) do + {_out, 0} -> :ok + {out, _} -> {:skip, "Docker not available: #{String.trim(out)}"} + end + end + end + + defp wait_for_port!(container, timeout_ms) do + deadline = System.monotonic_time(:millisecond) + timeout_ms + + wait = fn wait -> + case docker(["port", container, "5432/tcp"]) do + {out, 0} when byte_size(out) > 0 -> + out + |> String.split("\n", trim: true) + |> List.first() + |> parse_host_port!() + + _ -> + if System.monotonic_time(:millisecond) > deadline do + raise "Timed out waiting for docker port mapping" + else + :timer.sleep(250) + wait.(wait) + end + end + end + + wait.(wait) + end + + defp parse_host_port!(mapping) do + mapping + |> String.trim() + |> String.split(":") + |> List.last() + |> String.to_integer() + end + + defp wait_for_pg_ready!(container, timeout_ms) do + deadline = System.monotonic_time(:millisecond) + timeout_ms + + wait = fn wait -> + case docker([ + "exec", + container, + "pg_isready", + "-U", + @db_user, + "-d", + @db_name + ]) do + {_out, 0} -> + :ok + + _ -> + if System.monotonic_time(:millisecond) > deadline do + raise "Timed out waiting for ParadeDB Postgres to become ready" + else + :timer.sleep(250) + wait.(wait) + end + end + end + + wait.(wait) + end + + defp docker(args) do + System.cmd("docker", args, stderr_to_stdout: true) + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs index dc6c05a74..bbc00b7df 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -4,7 +4,7 @@ ExUnit.configure(capture_log: true, max_cases: System.schedulers_online()) -ExUnit.start(exclude: [:federated]) +ExUnit.start(exclude: [:federated, :integration]) if match?({:unix, :darwin}, :os.type()) do excluded = ExUnit.configuration() |> Keyword.get(:exclude, []) From efb75488d9f0f8cd8d9e629ec6c925420d7b174c Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Tue, 3 Mar 2026 12:49:23 +0400 Subject: [PATCH 032/391] Tighten ParadeDB integration test Remove warnings that break --warnings-as-errors, add TCP readiness check, and start the ParadeDB Repo with a small pool to reduce connection churn while booting the dockerized backend. --- .../search/paradedb_integration_test.exs | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/test/pleroma/search/paradedb_integration_test.exs b/test/pleroma/search/paradedb_integration_test.exs index 04e6a9d59..1da131c6b 100644 --- a/test/pleroma/search/paradedb_integration_test.exs +++ b/test/pleroma/search/paradedb_integration_test.exs @@ -8,7 +8,6 @@ defmodule Pleroma.Search.ParadeDBIntegrationTest do @moduletag :integration @moduletag timeout: 180_000 - import Mox import Pleroma.Factory alias Pleroma.Activity @@ -59,6 +58,7 @@ defmodule Pleroma.Search.ParadeDBIntegrationTest do |> wait_for_port!(@docker_startup_timeout_ms) wait_for_pg_ready!(container, @docker_startup_timeout_ms) + wait_for_tcp_ready!(host_port, @docker_startup_timeout_ms) url = "postgres://#{@db_user}:#{@db_password}@127.0.0.1:#{host_port}/#{@db_name}" @@ -73,7 +73,7 @@ defmodule Pleroma.Search.ParadeDBIntegrationTest do end end) - {:ok, _pid} = start_supervised(Pleroma.Search.ParadeDB.Repo) + {:ok, _pid} = start_supervised({Pleroma.Search.ParadeDB.Repo, pool_size: 1}) table = "pleroma_search_documents_it_#{System.unique_integer([:positive])}" Pleroma.Config.put([Pleroma.Search.ParadeDB, :table], table) @@ -203,6 +203,28 @@ defmodule Pleroma.Search.ParadeDBIntegrationTest do wait.(wait) end + defp wait_for_tcp_ready!(port, timeout_ms) do + deadline = System.monotonic_time(:millisecond) + timeout_ms + + wait = fn wait -> + case :gen_tcp.connect(~c"127.0.0.1", port, [:binary, active: false], 500) do + {:ok, socket} -> + :gen_tcp.close(socket) + :ok + + {:error, _} -> + if System.monotonic_time(:millisecond) > deadline do + raise "Timed out waiting for TCP port #{port} to accept connections" + else + :timer.sleep(250) + wait.(wait) + end + end + end + + wait.(wait) + end + defp docker(args) do System.cmd("docker", args, stderr_to_stdout: true) end From dee7ac0415e0de1c1f2d44d1dcd1202d5c7d099c Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Wed, 4 Mar 2026 13:04:41 +0400 Subject: [PATCH 033/391] Fix ParadeDB key field handling Use uuid ids in the ParadeDB documents table to satisfy BM25 key_field requirements and adjust indexing/search to dump FlakeIds consistently. Also fix the ParadeDB backend unit test to avoid invalid pinning on struct fields. --- lib/pleroma/search/paradedb.ex | 74 +++++++++++++++++---------- test/pleroma/search/paradedb_test.exs | 14 +++-- 2 files changed, 58 insertions(+), 30 deletions(-) diff --git a/lib/pleroma/search/paradedb.ex b/lib/pleroma/search/paradedb.ex index 963d573e8..e5d3018d7 100644 --- a/lib/pleroma/search/paradedb.ex +++ b/lib/pleroma/search/paradedb.ex @@ -67,37 +67,43 @@ defmodule Pleroma.Search.ParadeDB do maybe_search_data = object_to_search_data(activity.object) if activity.data["type"] == "Create" and maybe_search_data do + dumped_activity_id = dump_activity_id(activity.id) actor_ap_id = activity.data["actor"] published_at = DateTime.from_unix!(maybe_search_data.published) - sql = - """ - INSERT INTO #{table()} (id, object_id, object_ap_id, actor_ap_id, content, published_at) - VALUES ($1, $2, $3, $4, $5, $6) - ON CONFLICT (id) DO UPDATE SET - object_id = EXCLUDED.object_id, - object_ap_id = EXCLUDED.object_ap_id, - actor_ap_id = EXCLUDED.actor_ap_id, - content = EXCLUDED.content, - published_at = EXCLUDED.published_at - """ + if is_nil(dumped_activity_id) do + Logger.error("ParadeDB add_to_index failed: invalid activity id #{inspect(activity.id)}") + :ok + else + sql = + """ + INSERT INTO #{table()} (id, object_id, object_ap_id, actor_ap_id, content, published_at) + VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT (id) DO UPDATE SET + object_id = EXCLUDED.object_id, + object_ap_id = EXCLUDED.object_ap_id, + actor_ap_id = EXCLUDED.actor_ap_id, + content = EXCLUDED.content, + published_at = EXCLUDED.published_at + """ - params = [ - activity.id, - maybe_search_data.id, - maybe_search_data.ap, - actor_ap_id, - maybe_search_data.content, - published_at - ] + params = [ + dumped_activity_id, + maybe_search_data.id, + maybe_search_data.ap, + actor_ap_id, + maybe_search_data.content, + published_at + ] - case client_impl().query(sql, params) do - {:ok, _} -> - :ok + case client_impl().query(sql, params) do + {:ok, _} -> + :ok - {:error, error} -> - Logger.error("ParadeDB add_to_index failed: #{inspect(error)}") - {:error, error} + {:error, error} -> + Logger.error("ParadeDB add_to_index failed: #{inspect(error)}") + {:error, error} + end end else :ok @@ -139,7 +145,10 @@ defmodule Pleroma.Search.ParadeDB do end defp fetch_activities(ids, user, query, author) do - dumped_ids = Enum.map(ids, &FlakeId.from_string/1) + dumped_ids = + ids + |> Enum.map(&dump_activity_id/1) + |> Enum.reject(&is_nil/1) from(a in Activity, where: a.id in ^dumped_ids) |> Activity.with_preloaded_object() @@ -189,7 +198,7 @@ defmodule Pleroma.Search.ParadeDB do defp create_table_sql(table) do """ CREATE TABLE IF NOT EXISTS #{table} ( - id text PRIMARY KEY, + id uuid PRIMARY KEY, object_id bigint NOT NULL, object_ap_id text NOT NULL, actor_ap_id text, @@ -210,4 +219,15 @@ defmodule Pleroma.Search.ParadeDB do WITH (key_field = 'id') """ end + + defp dump_activity_id(<<_::binary-size(16)>> = uuid), do: uuid + + defp dump_activity_id(id) when is_binary(id) do + case FlakeId.Ecto.CompatType.dump(id) do + {:ok, <<_::binary-size(16)>> = uuid} -> uuid + _ -> nil + end + end + + defp dump_activity_id(_), do: nil end diff --git a/test/pleroma/search/paradedb_test.exs b/test/pleroma/search/paradedb_test.exs index c55e2aa47..3338c78ef 100644 --- a/test/pleroma/search/paradedb_test.exs +++ b/test/pleroma/search/paradedb_test.exs @@ -35,7 +35,9 @@ defmodule Pleroma.Search.ParadeDBTest do [activity_id, object_id, object_ap_id, actor_ap_id, content, published_at] = params - assert activity_id == activity.id + {:ok, dumped_activity_id} = FlakeId.Ecto.CompatType.dump(activity.id) + + assert activity_id == dumped_activity_id assert is_integer(object_id) assert is_binary(object_ap_id) assert actor_ap_id == activity.data["actor"] @@ -144,12 +146,18 @@ defmodule Pleroma.Search.ParadeDBTest do {:ok, activity1} = CommonAPI.post(user, %{status: "first swamp", visibility: "public"}) {:ok, activity2} = CommonAPI.post(user, %{status: "second swamp", visibility: "public"}) + activity1_id = activity1.id + activity2_id = activity2.id + + {:ok, dumped_activity1_id} = FlakeId.Ecto.CompatType.dump(activity1_id) + {:ok, dumped_activity2_id} = FlakeId.Ecto.CompatType.dump(activity2_id) + ClientMock |> expect(:query, fn sql, params -> assert sql =~ "SELECT id FROM pleroma_search_documents" assert ["swamp", _limit, _offset] = params - {:ok, %{rows: [[activity2.id], [activity1.id]]}} + {:ok, %{rows: [[dumped_activity2_id], [dumped_activity1_id]]}} end) Config @@ -161,7 +169,7 @@ defmodule Pleroma.Search.ParadeDBTest do "pleroma_search_documents" end) - assert [%{id: ^activity2.id}, %{id: ^activity1.id}] = + assert [%{id: ^activity2_id}, %{id: ^activity1_id}] = ParadeDB.search(nil, "swamp", limit: 40, offset: 0) end end From 74da3e01f7f0b27fcbbec313db363118c51388a8 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 5 Mar 2026 16:36:22 +0400 Subject: [PATCH 034/391] Harden ParadeDB search result safety Re-apply public Create visibility checks when resolving backend hits, improve UUID/FlakeId id parsing, and add tests for poisoned hits and UUID-string ids. Also tighten the docker integration test with localhost-only binding and config restoration. --- lib/pleroma/search/paradedb.ex | 41 ++++++++++-- .../search/paradedb_integration_test.exs | 13 +++- test/pleroma/search/paradedb_test.exs | 63 +++++++++++++++++++ 3 files changed, 111 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/search/paradedb.ex b/lib/pleroma/search/paradedb.ex index e5d3018d7..01cbc323f 100644 --- a/lib/pleroma/search/paradedb.ex +++ b/lib/pleroma/search/paradedb.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Search.ParadeDB do @behaviour Pleroma.Search.SearchBackend require Logger + require Pleroma.Constants import Ecto.Query @@ -153,6 +154,7 @@ defmodule Pleroma.Search.ParadeDB do from(a in Activity, where: a.id in ^dumped_ids) |> Activity.with_preloaded_object() |> Activity.restrict_deactivated_users() + |> maybe_restrict_public(user) |> maybe_restrict_local(user) |> maybe_restrict_author(author) |> maybe_restrict_blocked(user) @@ -168,6 +170,25 @@ defmodule Pleroma.Search.ParadeDB do |> maybe_fetch(user, query) end + defp maybe_restrict_public(query, %Pleroma.User{}) do + intended_recipients = [ + Pleroma.Constants.as_public(), + Pleroma.Web.ActivityPub.Utils.as_local_public() + ] + + from(a in query, + where: fragment("?->>'type' = 'Create'", a.data), + where: fragment("? && ?", ^intended_recipients, a.recipients) + ) + end + + defp maybe_restrict_public(query, _user) do + from(a in query, + where: fragment("?->>'type' = 'Create'", a.data), + where: ^Pleroma.Constants.as_public() in a.recipients + ) + end + defp search_ids(query, limit, offset, author) do base_sql = "SELECT id FROM #{table()} WHERE content &&& $1" params = [query] @@ -186,7 +207,7 @@ defmodule Pleroma.Search.ParadeDB do sql = base_sql <> - " ORDER BY published_at DESC LIMIT $#{limit_arg} OFFSET $#{offset_arg}" + " ORDER BY published_at DESC, id DESC LIMIT $#{limit_arg} OFFSET $#{offset_arg}" params = params ++ [limit, offset] @@ -223,11 +244,23 @@ defmodule Pleroma.Search.ParadeDB do defp dump_activity_id(<<_::binary-size(16)>> = uuid), do: uuid defp dump_activity_id(id) when is_binary(id) do - case FlakeId.Ecto.CompatType.dump(id) do - {:ok, <<_::binary-size(16)>> = uuid} -> uuid - _ -> nil + case Ecto.UUID.dump(id) do + {:ok, <<_::binary-size(16)>> = uuid} -> + uuid + + :error -> + dump_flake_activity_id(id) end end defp dump_activity_id(_), do: nil + + defp dump_flake_activity_id(id) do + case FlakeId.Ecto.CompatType.dump(id) do + {:ok, <<_::binary-size(16)>> = uuid} -> uuid + _ -> nil + end + rescue + _ -> nil + end end diff --git a/test/pleroma/search/paradedb_integration_test.exs b/test/pleroma/search/paradedb_integration_test.exs index 1da131c6b..42d12c4d6 100644 --- a/test/pleroma/search/paradedb_integration_test.exs +++ b/test/pleroma/search/paradedb_integration_test.exs @@ -14,7 +14,7 @@ defmodule Pleroma.Search.ParadeDBIntegrationTest do alias Pleroma.Search.ParadeDB alias Pleroma.Web.CommonAPI - @image "paradedb/paradedb:latest" + @image System.get_env("PARADEDB_TEST_IMAGE", "paradedb/paradedb:latest") @db_user "postgres" @db_password "postgres" @db_name "paradedb" @@ -46,7 +46,7 @@ defmodule Pleroma.Search.ParadeDBIntegrationTest do "-e", "POSTGRES_DB=#{@db_name}", "-p", - "5432", + "127.0.0.1::5432", "-d", @image ]) @@ -75,9 +75,18 @@ defmodule Pleroma.Search.ParadeDBIntegrationTest do {:ok, _pid} = start_supervised({Pleroma.Search.ParadeDB.Repo, pool_size: 1}) + previous_table = Pleroma.Config.get([Pleroma.Search.ParadeDB, :table], nil) table = "pleroma_search_documents_it_#{System.unique_integer([:positive])}" Pleroma.Config.put([Pleroma.Search.ParadeDB, :table], table) + on_exit(fn -> + if is_nil(previous_table) do + Pleroma.Config.delete([Pleroma.Search.ParadeDB, :table]) + else + Pleroma.Config.put([Pleroma.Search.ParadeDB, :table], previous_table) + end + end) + {:ok, %{ container: container, diff --git a/test/pleroma/search/paradedb_test.exs b/test/pleroma/search/paradedb_test.exs index 3338c78ef..c9bb216df 100644 --- a/test/pleroma/search/paradedb_test.exs +++ b/test/pleroma/search/paradedb_test.exs @@ -172,5 +172,68 @@ defmodule Pleroma.Search.ParadeDBTest do assert [%{id: ^activity2_id}, %{id: ^activity1_id}] = ParadeDB.search(nil, "swamp", limit: 40, offset: 0) end + + test "search excludes non-public activities from backend hits" do + user = insert(:user) + + {:ok, public_activity} = + CommonAPI.post(user, %{status: "public swamp", visibility: "public"}) + + {:ok, private_activity} = + CommonAPI.post(user, %{status: "private swamp", visibility: "private"}) + + public_activity_id = public_activity.id + + {:ok, dumped_public_id} = FlakeId.Ecto.CompatType.dump(public_activity.id) + {:ok, dumped_private_id} = FlakeId.Ecto.CompatType.dump(private_activity.id) + + ClientMock + |> expect(:query, fn sql, params -> + assert sql =~ "SELECT id FROM pleroma_search_documents" + assert ["swamp", _limit, _offset] = params + + {:ok, %{rows: [[dumped_private_id], [dumped_public_id]]}} + end) + + Config + |> expect(:get, 2, fn + [Pleroma.Search.ParadeDB, :client_impl], nil -> + ClientMock + + [Pleroma.Search.ParadeDB, :table], "pleroma_search_documents" -> + "pleroma_search_documents" + end) + + assert [%{id: ^public_activity_id}] = ParadeDB.search(nil, "swamp", limit: 40, offset: 0) + end + + test "search supports backend ids returned as uuid strings" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "uuid swamp", visibility: "public"}) + + activity_id = activity.id + {:ok, dumped_id} = FlakeId.Ecto.CompatType.dump(activity_id) + {:ok, uuid_string} = Ecto.UUID.load(dumped_id) + + ClientMock + |> expect(:query, fn sql, params -> + assert sql =~ "SELECT id FROM pleroma_search_documents" + assert ["swamp", _limit, _offset] = params + + {:ok, %{rows: [[uuid_string]]}} + end) + + Config + |> expect(:get, 2, fn + [Pleroma.Search.ParadeDB, :client_impl], nil -> + ClientMock + + [Pleroma.Search.ParadeDB, :table], "pleroma_search_documents" -> + "pleroma_search_documents" + end) + + assert [%{id: ^activity_id}] = ParadeDB.search(nil, "swamp", limit: 40, offset: 0) + end end end From 7efd296afbf49e5fb4ea8cdf659e1f0cdedd7de9 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 5 Mar 2026 17:12:03 +0400 Subject: [PATCH 035/391] Add fuzzy matching to ParadeDB search Make ParadeDB search typo-tolerant via configurable fuzzy distance and document the new setting. Add coverage for fuzzy query SQL and id parsing, and keep integration test stability under connection load. --- config/config.exs | 3 +- config/description.exs | 6 +++ docs/configuration/search.md | 5 +- lib/pleroma/search/paradedb.ex | 23 ++++++++- .../search/paradedb_integration_test.exs | 2 +- test/pleroma/search/paradedb_test.exs | 47 +++++++++++++++++-- 6 files changed, 79 insertions(+), 7 deletions(-) diff --git a/config/config.exs b/config/config.exs index 7e374b22b..01dd09a18 100644 --- a/config/config.exs +++ b/config/config.exs @@ -940,7 +940,8 @@ config :pleroma, Pleroma.Search.Meilisearch, config :pleroma, Pleroma.Search.ParadeDB, url: nil, - table: "pleroma_search_documents" + table: "pleroma_search_documents", + fuzzy_distance: 0 config :pleroma, Pleroma.Search.ParadeDB.Repo, pool_size: 10, diff --git a/config/description.exs b/config/description.exs index de454dfb2..9a00afdcd 100644 --- a/config/description.exs +++ b/config/description.exs @@ -3560,6 +3560,12 @@ config :pleroma, :config_description, [ type: :string, description: "Table name used for search documents.", suggestion: ["pleroma_search_documents"] + }, + %{ + key: :fuzzy_distance, + type: :integer, + description: "Edit-distance used for fuzzy token matching (0 disables fuzziness, max 2).", + suggestion: [0, 1, 2] } ] }, diff --git a/docs/configuration/search.md b/docs/configuration/search.md index 780904d10..c0d3f8462 100644 --- a/docs/configuration/search.md +++ b/docs/configuration/search.md @@ -160,7 +160,8 @@ To use it, set the search module to `Pleroma.Search.ParadeDB` and configure the > > config :pleroma, Pleroma.Search.ParadeDB, > url: System.get_env("PARADEDB_DATABASE_URL") || "postgres://postgres:postgres@127.0.0.1:5432/paradedb", -> table: "pleroma_search_documents" +> table: "pleroma_search_documents", +> fuzzy_distance: 0 Then, create the table and BM25 index once: @@ -178,4 +179,6 @@ After that, run an initial indexing pass to backfill existing posts: Note: Like the other external search backends, only public/unlisted Notes are indexed. +Set `fuzzy_distance` to `1` or `2` to allow typo-tolerant matching (`2` is the maximum ParadeDB supports). + Indexing runs via the Oban `search_indexing` queue, so ensure that queue is enabled (not paused) in your Oban configuration. diff --git a/lib/pleroma/search/paradedb.ex b/lib/pleroma/search/paradedb.ex index 01cbc323f..57e5599b5 100644 --- a/lib/pleroma/search/paradedb.ex +++ b/lib/pleroma/search/paradedb.ex @@ -35,6 +35,22 @@ defmodule Pleroma.Search.ParadeDB do Config.get([Pleroma.Search.ParadeDB, :table], @default_table) end + defp fuzzy_distance do + case Config.get([Pleroma.Search.ParadeDB, :fuzzy_distance], 0) do + value when is_integer(value) and value in 0..2 -> + value + + value when is_binary(value) -> + case Integer.parse(value) do + {parsed, ""} when parsed in 0..2 -> parsed + _ -> 0 + end + + _ -> + 0 + end + end + @impl true def create_index do with {:ok, _} <- client_impl().query("CREATE EXTENSION IF NOT EXISTS pg_search", []), @@ -190,7 +206,9 @@ defmodule Pleroma.Search.ParadeDB do end defp search_ids(query, limit, offset, author) do - base_sql = "SELECT id FROM #{table()} WHERE content &&& $1" + base_sql = + "SELECT id FROM #{table()} WHERE content &&& #{search_query_param_sql(fuzzy_distance())}" + params = [query] {base_sql, params} = @@ -216,6 +234,9 @@ defmodule Pleroma.Search.ParadeDB do end end + defp search_query_param_sql(0), do: "$1" + defp search_query_param_sql(distance), do: "$1::pdb.fuzzy(#{distance})" + defp create_table_sql(table) do """ CREATE TABLE IF NOT EXISTS #{table} ( diff --git a/test/pleroma/search/paradedb_integration_test.exs b/test/pleroma/search/paradedb_integration_test.exs index 42d12c4d6..743dcbaff 100644 --- a/test/pleroma/search/paradedb_integration_test.exs +++ b/test/pleroma/search/paradedb_integration_test.exs @@ -73,7 +73,7 @@ defmodule Pleroma.Search.ParadeDBIntegrationTest do end end) - {:ok, _pid} = start_supervised({Pleroma.Search.ParadeDB.Repo, pool_size: 1}) + {:ok, _pid} = start_supervised({Pleroma.Search.ParadeDB.Repo, pool_size: 5}) previous_table = Pleroma.Config.get([Pleroma.Search.ParadeDB, :table], nil) table = "pleroma_search_documents_it_#{System.unique_integer([:positive])}" diff --git a/test/pleroma/search/paradedb_test.exs b/test/pleroma/search/paradedb_test.exs index c9bb216df..c4fb57cb3 100644 --- a/test/pleroma/search/paradedb_test.exs +++ b/test/pleroma/search/paradedb_test.exs @@ -161,12 +161,15 @@ defmodule Pleroma.Search.ParadeDBTest do end) Config - |> expect(:get, 2, fn + |> expect(:get, 3, fn [Pleroma.Search.ParadeDB, :client_impl], nil -> ClientMock [Pleroma.Search.ParadeDB, :table], "pleroma_search_documents" -> "pleroma_search_documents" + + [Pleroma.Search.ParadeDB, :fuzzy_distance], 0 -> + 0 end) assert [%{id: ^activity2_id}, %{id: ^activity1_id}] = @@ -196,12 +199,15 @@ defmodule Pleroma.Search.ParadeDBTest do end) Config - |> expect(:get, 2, fn + |> expect(:get, 3, fn [Pleroma.Search.ParadeDB, :client_impl], nil -> ClientMock [Pleroma.Search.ParadeDB, :table], "pleroma_search_documents" -> "pleroma_search_documents" + + [Pleroma.Search.ParadeDB, :fuzzy_distance], 0 -> + 0 end) assert [%{id: ^public_activity_id}] = ParadeDB.search(nil, "swamp", limit: 40, offset: 0) @@ -225,15 +231,50 @@ defmodule Pleroma.Search.ParadeDBTest do end) Config - |> expect(:get, 2, fn + |> expect(:get, 3, fn [Pleroma.Search.ParadeDB, :client_impl], nil -> ClientMock [Pleroma.Search.ParadeDB, :table], "pleroma_search_documents" -> "pleroma_search_documents" + + [Pleroma.Search.ParadeDB, :fuzzy_distance], 0 -> + 0 end) assert [%{id: ^activity_id}] = ParadeDB.search(nil, "swamp", limit: 40, offset: 0) end + + test "search uses configured fuzzy distance" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "runing shoes", visibility: "public"}) + + activity_id = activity.id + {:ok, dumped_id} = FlakeId.Ecto.CompatType.dump(activity_id) + + ClientMock + |> expect(:query, fn sql, params -> + assert sql =~ "SELECT id FROM pleroma_search_documents" + assert sql =~ "$1::pdb.fuzzy(1)" + assert ["running shoes", _limit, _offset] = params + + {:ok, %{rows: [[dumped_id]]}} + end) + + Config + |> expect(:get, 3, fn + [Pleroma.Search.ParadeDB, :client_impl], nil -> + ClientMock + + [Pleroma.Search.ParadeDB, :table], "pleroma_search_documents" -> + "pleroma_search_documents" + + [Pleroma.Search.ParadeDB, :fuzzy_distance], 0 -> + 1 + end) + + assert [%{id: ^activity_id}] = ParadeDB.search(nil, "running shoes", limit: 40, offset: 0) + end end end From 4e65bbf13469d8147dc780c7e655fefdb12bdebf Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 5 Mar 2026 17:34:48 +0400 Subject: [PATCH 036/391] Fix ParadeDB fuzzy query parameter casting Cast the bound search parameter through text before so Postgrex can handle the prepared statement type inference. Add matching test assertion updates. --- lib/pleroma/search/paradedb.ex | 2 +- test/pleroma/search/paradedb_test.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/search/paradedb.ex b/lib/pleroma/search/paradedb.ex index 57e5599b5..a6649a894 100644 --- a/lib/pleroma/search/paradedb.ex +++ b/lib/pleroma/search/paradedb.ex @@ -235,7 +235,7 @@ defmodule Pleroma.Search.ParadeDB do end defp search_query_param_sql(0), do: "$1" - defp search_query_param_sql(distance), do: "$1::pdb.fuzzy(#{distance})" + defp search_query_param_sql(distance), do: "($1::text)::pdb.fuzzy(#{distance})" defp create_table_sql(table) do """ diff --git a/test/pleroma/search/paradedb_test.exs b/test/pleroma/search/paradedb_test.exs index c4fb57cb3..3a3b8298f 100644 --- a/test/pleroma/search/paradedb_test.exs +++ b/test/pleroma/search/paradedb_test.exs @@ -256,7 +256,7 @@ defmodule Pleroma.Search.ParadeDBTest do ClientMock |> expect(:query, fn sql, params -> assert sql =~ "SELECT id FROM pleroma_search_documents" - assert sql =~ "$1::pdb.fuzzy(1)" + assert sql =~ "($1::text)::pdb.fuzzy(1)" assert ["running shoes", _limit, _offset] = params {:ok, %{rows: [[dumped_id]]}} From cae3c9a980945f107b2cd6a321b0d1d1e0d243ea Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 5 Mar 2026 17:39:34 +0400 Subject: [PATCH 037/391] Add ParadeDB fuzzy integration coverage Extend the docker-backed ParadeDB integration test to verify typo-tolerant status search with fuzzy_distance enabled, covering prepared-parameter fuzzy casts against a real backend. --- test/pleroma/search/paradedb_integration_test.exs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/pleroma/search/paradedb_integration_test.exs b/test/pleroma/search/paradedb_integration_test.exs index 743dcbaff..2687260cf 100644 --- a/test/pleroma/search/paradedb_integration_test.exs +++ b/test/pleroma/search/paradedb_integration_test.exs @@ -130,6 +130,20 @@ defmodule Pleroma.Search.ParadeDBIntegrationTest do refute private.id in ids assert List.first(ids) == a2.id + previous_fuzzy_distance = Pleroma.Config.get([Pleroma.Search.ParadeDB, :fuzzy_distance], 0) + Pleroma.Config.put([Pleroma.Search.ParadeDB, :fuzzy_distance], 1) + + on_exit(fn -> + Pleroma.Config.put([Pleroma.Search.ParadeDB, :fuzzy_distance], previous_fuzzy_distance) + end) + + fuzzy_results = ParadeDB.search(nil, "paradedb integraion", limit: 40, offset: 0) + fuzzy_ids = Enum.map(fuzzy_results, & &1.id) + + assert a1.id in fuzzy_ids + assert a2.id in fuzzy_ids + refute private.id in fuzzy_ids + # Removing an object should remove it from the ParadeDB index. assert :ok = ParadeDB.remove_from_index(a2.object) From 0e0931a3c23d8c966aef8ded80e82da6f3c4205e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 19 Sep 2025 16:55:40 +0200 Subject: [PATCH 038/391] MastoAPI: /api/v1/admin/accounts, /api/v1/admin/reports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/mastodon-admin.add | 1 + lib/pleroma/moderation_log.ex | 10 + lib/pleroma/user.ex | 6 + lib/pleroma/user/query.ex | 19 +- lib/pleroma/web/api_spec.ex | 7 + .../mastodon_admin/account_operation.ex | 258 ++++++++++++++++++ .../mastodon_admin/report_operation.ex | 150 ++++++++++ .../admin/controllers/account_controller.ex | 220 +++++++++++++++ .../admin/controllers/report_controller.ex | 107 ++++++++ .../mastodon_api/admin/views/account_view.ex | 64 +++++ .../mastodon_api/admin/views/report_view.ex | 56 ++++ lib/pleroma/web/router.ex | 40 +++ .../controllers/account_controller_test.exs | 175 ++++++++++++ .../controllers/report_controller_test.exs | 118 ++++++++ 14 files changed, 1230 insertions(+), 1 deletion(-) create mode 100644 changelog.d/mastodon-admin.add create mode 100644 lib/pleroma/web/api_spec/operations/mastodon_admin/account_operation.ex create mode 100644 lib/pleroma/web/api_spec/operations/mastodon_admin/report_operation.ex create mode 100644 lib/pleroma/web/mastodon_api/admin/controllers/account_controller.ex create mode 100644 lib/pleroma/web/mastodon_api/admin/controllers/report_controller.ex create mode 100644 lib/pleroma/web/mastodon_api/admin/views/account_view.ex create mode 100644 lib/pleroma/web/mastodon_api/admin/views/report_view.ex create mode 100644 test/pleroma/web/mastodon_api/admin/controllers/account_controller_test.exs create mode 100644 test/pleroma/web/mastodon_api/admin/controllers/report_controller_test.exs diff --git a/changelog.d/mastodon-admin.add b/changelog.d/mastodon-admin.add new file mode 100644 index 000000000..64f2ca987 --- /dev/null +++ b/changelog.d/mastodon-admin.add @@ -0,0 +1 @@ +Implement basics of Mastodon admin API \ No newline at end of file diff --git a/lib/pleroma/moderation_log.ex b/lib/pleroma/moderation_log.ex index 90219312c..260c139ec 100644 --- a/lib/pleroma/moderation_log.ex +++ b/lib/pleroma/moderation_log.ex @@ -346,6 +346,16 @@ defmodule Pleroma.ModerationLog do "@#{actor_nickname} approved users: #{users_to_nicknames_string(users)}" end + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "reject", + "subject" => users + } + }) do + "@#{actor_nickname} rejected users: #{users_to_nicknames_string(users)}" + end + def get_log_entry_message(%ModerationLog{ data: %{ "actor" => %{"nickname" => actor_nickname}, diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 75da41da9..4b421f389 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1964,6 +1964,12 @@ defmodule Pleroma.User do def approve(%User{} = user), do: {:ok, user} + def reject(%User{is_approved: false} = user) do + delete(user) + end + + def reject(%User{} = _user), do: {:error, "User is approved"} + def confirm(users) when is_list(users) do Repo.transaction(fn -> Enum.map(users, fn user -> diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex index cd9586452..b877b52b2 100644 --- a/lib/pleroma/user/query.ex +++ b/lib/pleroma/user/query.ex @@ -63,7 +63,8 @@ defmodule Pleroma.User.Query do limit: pos_integer(), actor_types: [String.t()], birthday_day: pos_integer(), - birthday_month: pos_integer() + birthday_month: pos_integer(), + staff: boolean() } | map() @@ -179,6 +180,10 @@ defmodule Pleroma.User.Query do defp compose_query({:external, _}, query), do: location_query(query, false) + defp compose_query({:active, false}, query) do + where(query, [u], u.is_active == false or u.is_approved == false or u.is_confirmed == false) + end + defp compose_query({:active, _}, query) do where(query, [u], u.is_active == true) |> where([u], u.is_approved == true) @@ -202,6 +207,10 @@ defmodule Pleroma.User.Query do where(query, [u], u.is_confirmed != ^bool) end + defp compose_query({:need_approval, false}, query) do + where(query, [u], u.is_approved == true) + end + defp compose_query({:need_approval, _}, query) do where(query, [u], u.is_approved == false) end @@ -285,6 +294,14 @@ defmodule Pleroma.User.Query do |> where([u], fragment("date_part('month', ?)", u.birthday) == ^month) end + defp compose_query({:staff, false}, query) do + where(query, [u], u.is_admin == false and u.is_moderator == false) + end + + defp compose_query({:staff, _}, query) do + where(query, [u], u.is_admin == true or u.is_moderator == true) + end + defp compose_query(_unsupported_param, query), do: query defp location_query(query, local) do diff --git a/lib/pleroma/web/api_spec.ex b/lib/pleroma/web/api_spec.ex index 5006167ea..ff6363603 100644 --- a/lib/pleroma/web/api_spec.ex +++ b/lib/pleroma/web/api_spec.ex @@ -108,6 +108,13 @@ defmodule Pleroma.Web.ApiSpec do "Announcement management" ] }, + %{ + "name" => "Administration (Mastodon API)", + "tags" => [ + "User administration (Mastodon API)", + "Report management (Mastodon API)" + ] + }, %{"name" => "Applications", "tags" => ["Applications", "Push subscriptions"]}, %{ "name" => "Current account", diff --git a/lib/pleroma/web/api_spec/operations/mastodon_admin/account_operation.ex b/lib/pleroma/web/api_spec/operations/mastodon_admin/account_operation.ex new file mode 100644 index 000000000..23dae0306 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/mastodon_admin/account_operation.ex @@ -0,0 +1,258 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.MastodonAdmin.AccountOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.Account + alias Pleroma.Web.ApiSpec.Schemas.ApiError + alias Pleroma.Web.ApiSpec.Schemas.FlakeID + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def index_operation do + %Operation{ + tags: ["User administration (Mastodon API)"], + summary: "View accounts by criteria", + operationId: "MastodonAdmin.AccountController.index", + description: "View accounts matching certain criteria for filtering, up to 40 at a time.", + security: [%{"oAuth" => ["admin:read:accounts"]}], + parameters: + [ + Operation.parameter(:local, :query, :boolean, "Filter for local accounts?"), + Operation.parameter(:remote, :query, :boolean, "Filter for remote accounts?"), + Operation.parameter( + :by_domain, + :query, + :string, + "Filter by the given domain (not implemented yet)" + ), + Operation.parameter( + :active, + :query, + :boolean, + "Filter for currently active accounts??" + ), + Operation.parameter( + :pending, + :query, + :boolean, + "Filter for currently pending accounts?" + ), + Operation.parameter( + :disabled, + :query, + :boolean, + "Filter for currently disabled accounts?" + ), + Operation.parameter( + :sensitized, + :query, + :boolean, + "Filter for currently sensitized accounts? (not implemented yet)" + ), + Operation.parameter( + :silenced, + :query, + :boolean, + "Filter for currently silenced accounts? (not implemented yet)" + ), + Operation.parameter( + :suspended, + :query, + :boolean, + "Filter for currently suspended accounts? (not implemented yet)" + ), + Operation.parameter(:username, :query, :string, "Username to search for"), + Operation.parameter(:display_name, :query, :string, "Display name to search for"), + Operation.parameter(:email, :query, :string, "Lookup a user with this email"), + Operation.parameter( + :ip, + :query, + :string, + "Lookup users by this IP address (not implemented yet)" + ), + Operation.parameter(:staff, :query, :boolean, "Filter for staff accounts?") + ] ++ + pagination_params(), + responses: %{ + 200 => + Operation.response("Account", "application/json", %Schema{ + title: "ArrayOfAccounts", + type: :array, + items: account() + }), + 401 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def show_operation do + %Operation{ + tags: ["User administration (Mastodon API)"], + summary: "View a specific account", + operationId: "MastodonAdmin.AccountController.show", + description: "View admin-level information about the given account.", + security: [%{"oAuth" => ["admin:read:accounts"]}], + parameters: [ + Operation.parameter(:id, :path, :string, "ID of the account") + ], + responses: %{ + 200 => Operation.response("Account", "application/json", account()), + 401 => Operation.response("Error", "application/json", ApiError), + 404 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def account_action_operation do + %Operation{ + tags: ["User administration (Mastodon API)"], + summary: "Perform an action against an account", + operationId: "MastodonAdmin.AccountController.account_action", + description: + "Perform an action against an account and log this action in the moderation history.", + security: [%{"oAuth" => ["admin:write:accounts"]}], + parameters: [ + Operation.parameter(:id, :path, :string, "ID of the account") + ], + requestBody: + request_body( + "Parameters", + %Schema{ + type: :object, + properties: %{ + type: %Schema{ + type: :string, + enum: ["none", "disable", "sensitive", "silence", "suspend"] + }, + report_id: %Schema{ + type: :string, + nullable: true, + description: "ID of an associated report that caused this action to be taken" + } + } + }, + required: true + ), + responses: %{ + 204 => no_content_response(), + 401 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def delete_operation do + %Operation{ + tags: ["User administration (Mastodon API)"], + summary: "Delete a specific account", + operationId: "MastodonAdmin.AccountController.delete", + description: "Delete the given account.", + security: [%{"oAuth" => ["admin:write:accounts"]}], + parameters: [ + Operation.parameter(:id, :path, :string, "ID of the account") + ], + responses: %{ + 200 => Operation.response("Account", "application/json", account()), + 401 => Operation.response("Error", "application/json", ApiError), + 404 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def enable_operation do + %Operation{ + tags: ["User administration (Mastodon API)"], + summary: "Re-enable account", + operationId: "MastodonAdmin.AccountController.enable", + description: "Re-enable a local account whose login is currently disabled.", + security: [%{"oAuth" => ["admin:write:accounts"]}], + parameters: [ + Operation.parameter(:id, :path, :string, "ID of the account") + ], + responses: %{ + 200 => Operation.response("Account", "application/json", account()), + 401 => Operation.response("Error", "application/json", ApiError), + 404 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def approve_operation do + %Operation{ + tags: ["User administration (Mastodon API)"], + summary: "Approve pending account", + operationId: "MastodonAdmin.AccountController.approve", + description: "Approve the given local account if it is currently pending approval.", + parameters: [ + Operation.parameter(:id, :path, :string, "ID of the account") + ], + responses: %{ + 200 => Operation.response("Account", "application/json", account()), + 401 => Operation.response("Error", "application/json", ApiError), + 404 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def reject_operation do + %Operation{ + tags: ["User administration (Mastodon API)"], + summary: "Reject pending account", + operationId: "MastodonAdmin.AccountController.reject", + description: "Reject the given local account if it is currently pending approval.", + parameters: [ + Operation.parameter(:id, :path, :string, "ID of the account") + ], + responses: %{ + 200 => Operation.response("Account", "application/json", account()), + 400 => Operation.response("Error", "application/json", ApiError), + 401 => Operation.response("Error", "application/json", ApiError), + 404 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def account do + %Schema{ + title: "Admin::Account", + description: "Admin-level information about a given account.", + type: :object, + properties: %{ + id: FlakeID, + username: %Schema{type: :string}, + domain: %Schema{type: :string, nullable: true}, + created_at: %Schema{type: :string, format: "date-time"}, + email: %Schema{type: :string, format: "email", nullable: true}, + ip: %Schema{type: :string, nullable: true}, + ips: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + ip: %Schema{type: :string}, + used_at: %Schema{type: :string, format: "date-time"} + } + } + }, + locale: %Schema{type: :string, format: "date-time", nullable: true}, + invite_request: %Schema{type: :string, format: "date-time", nullable: true}, + role: %Schema{type: :string, nullable: true}, + confirmed: %Schema{type: :boolean}, + approved: %Schema{type: :boolean}, + disabled: %Schema{type: :boolean}, + silenced: %Schema{type: :boolean, nullable: true}, + suspended: %Schema{type: :boolean, nullable: true}, + account: Account, + created_by_application_id: %Schema{type: :string, nullable: true}, + invited_by_account_id: %Schema{type: :string, nullable: true} + } + } + end +end diff --git a/lib/pleroma/web/api_spec/operations/mastodon_admin/report_operation.ex b/lib/pleroma/web/api_spec/operations/mastodon_admin/report_operation.ex new file mode 100644 index 000000000..274ce3bd9 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/mastodon_admin/report_operation.ex @@ -0,0 +1,150 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.MastodonAdmin.ReportOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.MastodonAdmin.AccountOperation + alias Pleroma.Web.ApiSpec.Schemas.ApiError + alias Pleroma.Web.ApiSpec.Schemas.FlakeID + alias Pleroma.Web.ApiSpec.Schemas.Status + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def index_operation do + %Operation{ + tags: ["Report management (Mastodon API)"], + summary: "View all reports", + operationId: "MastodonAdmin.ReportController.index", + description: + "View all reports. Pagination may be done with HTTP Link header in the response.", + security: [%{"oAuth" => ["admin:read:reports"]}], + parameters: + [ + Operation.parameter(:resolved, :query, :boolean, "Filter for resolved reports"), + Operation.parameter(:account_id, :query, :string, "Filter by author account id"), + Operation.parameter( + :target_account_id, + :query, + :string, + "Filter by report target account id (not implemented)" + ) + ] ++ + pagination_params(), + responses: %{ + 200 => + Operation.response("Reports", "application/json", %Schema{ + title: "ArrayOfReports", + type: :array, + items: report() + }), + 401 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def show_operation do + %Operation{ + tags: ["Report management (Mastodon API)"], + summary: "View a single report", + operationId: "MastodonAdmin.ReportController.show", + description: "View information about the report with the given ID.", + security: [%{"oAuth" => ["admin:read:reports"]}], + parameters: [ + Operation.parameter(:id, :path, :string, "ID of the report") + ], + responses: %{ + 200 => Operation.response("Report", "application/json", report()), + 401 => Operation.response("Error", "application/json", ApiError), + 404 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def resolve_operation do + %Operation{ + tags: ["Report management (Mastodon API)"], + summary: "Mark as resolved", + operationId: "MastodonAdmin.ReportController.resolve", + description: "Mark a report as resolved with no further action taken.", + security: [%{"oAuth" => ["admin:write:reports"]}], + parameters: [ + Operation.parameter(:id, :path, :string, "ID of the report") + ], + responses: %{ + 200 => Operation.response("Report", "application/json", report()), + 400 => Operation.response("Error", "application/json", ApiError), + 401 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def reopen_operation do + %Operation{ + tags: ["Report management (Mastodon API)"], + summary: "Re-open report", + operationId: "MastodonAdmin.ReportController.reopen", + description: "Reopen a currently closed report.", + security: [%{"oAuth" => ["admin:write:reports"]}], + parameters: [ + Operation.parameter(:id, :path, :string, "ID of the report") + ], + responses: %{ + 200 => Operation.response("Report", "application/json", report()), + 400 => Operation.response("Error", "application/json", ApiError), + 401 => Operation.response("Error", "application/json", ApiError) + } + } + end + + defp report do + %Schema{ + title: "Admin::Report", + type: :object, + properties: %{ + id: FlakeID, + action_taken: %Schema{type: :boolean}, + action_taken_at: %Schema{type: :string, format: "date-time", nullable: true}, + category: %Schema{type: :string, enum: ["spam", "violation", "other"]}, + comment: %Schema{type: :string, nullable: true}, + forwarded: %Schema{type: :boolean, nullable: true}, + created_at: %Schema{type: :string, format: "date-time"}, + updated_at: %Schema{type: :string, format: "date-time"}, + account: AccountOperation.account(), + target_account: AccountOperation.account(), + assigned_account: %Schema{ + oneOf: [ + AccountOperation.account(), + %Schema{type: :null} + ] + }, + action_taken_by_account: %Schema{ + oneOf: [ + AccountOperation.account(), + %Schema{type: :null} + ] + }, + statuses: %Schema{ + type: :array, + items: Status + }, + rules: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + id: %Schema{type: :integer}, + text: %Schema{type: :string} + } + } + } + } + } + end +end diff --git a/lib/pleroma/web/mastodon_api/admin/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/admin/controllers/account_controller.ex new file mode 100644 index 000000000..a1eac7fe3 --- /dev/null +++ b/lib/pleroma/web/mastodon_api/admin/controllers/account_controller.ex @@ -0,0 +1,220 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.Admin.AccountController do + use Pleroma.Web, :controller + + import Pleroma.Web.ControllerHelper, + only: [ + add_link_headers: 2, + assign_account_by_id: 2, + json_response: 3 + ] + + alias Pleroma.Activity + alias Pleroma.ModerationLog + alias Pleroma.Pagination + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.Pipeline + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Plugs.OAuthScopesPlug + + @filter_params ~W( + local external active needing_approval deactivated nickname name email staff + ) + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + + plug( + OAuthScopesPlug, + %{scopes: ["admin:read:accounts"]} + when action in [:index, :show] + ) + + plug( + OAuthScopesPlug, + %{scopes: ["admin:write:accounts"]} + when action in [ + :delete, + :enable, + :account_action, + :approve, + :reject + ] + ) + + plug( + :assign_account_by_id + when action in [ + :show, + :delete, + :enable, + :account_action, + :approve, + :reject + ] + ) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.MastodonAdmin.AccountOperation + + def index(conn, params) do + users = + params + |> build_criteria() + |> User.Query.build() + |> Pagination.fetch_paginated(params) + + conn + |> add_link_headers(users) + |> render("index.json", users: users) + end + + def show(%{assigns: %{user: _admin, account: user}} = conn, _params) do + render(conn, "show.json", user: user) + end + + def account_action( + %{assigns: %{user: admin, account: user}, body_params: %{type: type} = body_params} = + conn, + _params + ) do + {:ok, _user} = handle_account_action(user, admin, type) + + resolve_report(admin, body_params) + + json_response(conn, :no_content, "") + end + + def delete(%{assigns: %{user: admin, account: user}} = conn, _params) do + {:ok, delete_data, _} = Builder.delete(admin, user.ap_id) + Pipeline.common_pipeline(delete_data, local: true) + + ModerationLog.insert_log(%{ + actor: admin, + subject: [user], + action: "delete" + }) + + render(conn, "show.json", user: user) + end + + def enable(%{assigns: %{user: admin, account: user}} = conn, _params) do + {:ok, user} = User.set_activation(user, true) + + ModerationLog.insert_log(%{ + actor: admin, + subject: [user], + action: "activate" + }) + + render(conn, "show.json", user: user) + end + + def approve(%{assigns: %{user: admin, account: user}} = conn, _params) do + {:ok, user} = User.approve(user) + + ModerationLog.insert_log(%{ + actor: admin, + subject: [user], + action: "approve" + }) + + render(conn, "show.json", user: user) + end + + def reject(%{assigns: %{user: admin, account: user}} = conn, _params) do + with {:ok, _} <- User.reject(user) do + ModerationLog.insert_log(%{ + actor: admin, + subject: [user], + action: "reject" + }) + + render(conn, "show.json", user: user) + else + {:error, error} -> + json_response(conn, :bad_request, %{error: error}) + end + end + + defp handle_account_action(%User{local: true} = user, admin, "disable") do + ModerationLog.insert_log(%{ + actor: admin, + subject: [user], + action: "deactivate" + }) + + User.set_activation(user, false) + end + + defp handle_account_action(user, _admin, _type) do + {:ok, user} + end + + defp build_criteria(params) do + %{} + |> maybe_filter_local(params) + |> maybe_filter_external(params) + |> maybe_filter_active(params) + |> maybe_filter_needing_approval(params) + |> maybe_filter_deactivated(params) + |> maybe_filter_nickname(params) + |> maybe_filter_name(params) + |> maybe_filter_email(params) + |> maybe_filter_staff(params) + end + + defp maybe_filter_local(criteria, %{local: true} = _params), + do: Map.put(criteria, :local, true) + + defp maybe_filter_local(criteria, %{local: false} = _params), + do: Map.put(criteria, :external, true) + + defp maybe_filter_external(criteria, %{remote: true} = _params), + do: Map.put(criteria, :external, true) + + defp maybe_filter_external(criteria, %{remote: false} = _params), + do: Map.put(criteria, :local, true) + + defp maybe_filter_active(criteria, %{active: active} = _params), + do: Map.put(criteria, :active, active) + + defp maybe_filter_needing_approval(criteria, %{pending: need_approval} = _params), + do: Map.put(criteria, :need_approval, need_approval) + + defp maybe_filter_deactivated(criteria, %{disabled: deactivated} = _params), + do: Map.put(criteria, :deactivated, deactivated) + + defp maybe_filter_nickname(criteria, %{username: nickname} = _params), + do: Map.put(criteria, :nickname, nickname) + + defp maybe_filter_name(criteria, %{display_name: name} = _params), + do: Map.put(criteria, :name, name) + + defp maybe_filter_email(criteria, %{email: email} = _params), + do: Map.put(criteria, :email, email) + + defp maybe_filter_staff(criteria, %{staff: staff} = _params), + do: Map.put(criteria, :staff, staff) + + for filter_param <- @filter_params do + defp unquote(:"maybe_filter_#{filter_param}")(criteria, _params), do: criteria + end + + defp resolve_report(admin, %{report_id: id}) do + with {:ok, activity} <- CommonAPI.update_report_state(id, "resolved"), + report <- Activity.get_by_id_with_user_actor(activity.id) do + ModerationLog.insert_log(%{ + action: "report_update", + actor: admin, + subject: activity, + subject_actor: report.user_actor + }) + end + end + + defp resolve_report(_admin, _params) do + end +end diff --git a/lib/pleroma/web/mastodon_api/admin/controllers/report_controller.ex b/lib/pleroma/web/mastodon_api/admin/controllers/report_controller.ex new file mode 100644 index 000000000..217a97cc9 --- /dev/null +++ b/lib/pleroma/web/mastodon_api/admin/controllers/report_controller.ex @@ -0,0 +1,107 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.Admin.ReportController do + use Pleroma.Web, :controller + + import Pleroma.Web.ControllerHelper, + only: [ + add_link_headers: 2, + json_response: 3 + ] + + alias Pleroma.Activity + alias Pleroma.ModerationLog + alias Pleroma.Pagination + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.AdminAPI.Report + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Plugs.OAuthScopesPlug + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + + plug(OAuthScopesPlug, %{scopes: ["admin:read:reports"]} when action in [:index, :show]) + + plug(OAuthScopesPlug, %{scopes: ["admin:write:reports"]} when action in [:resolve, :reopen]) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.MastodonAdmin.ReportOperation + + def index(conn, params) do + opts = + %{} + |> Map.put(:type, "Flag") + |> Map.put(:skip_preload, true) + |> Map.put(:preload_report_notes, true) + |> Map.put(:total, true) + |> restrict_state(params) + |> restrict_actor(params) + + reports = + ActivityPub.fetch_activities_query([], opts) + |> Pagination.fetch_paginated(params) + + conn + |> add_link_headers(reports) + |> render("index.json", reports: reports) + end + + def show(conn, %{id: id}) do + with %Activity{} = report <- Activity.get_report(id) do + render(conn, "show.json", Report.extract_report_info(report)) + else + _ -> {:error, :not_found} + end + end + + def resolve(%{assigns: %{user: admin}} = conn, %{id: id}) do + with {:ok, activity} <- CommonAPI.update_report_state(id, "resolved"), + report <- Activity.get_by_id_with_user_actor(activity.id) do + ModerationLog.insert_log(%{ + action: "report_update", + actor: admin, + subject: activity, + subject_actor: report.user_actor + }) + + render(conn, "show.json", Report.extract_report_info(activity)) + else + {:error, error} -> + json_response(conn, :bad_request, %{error: error}) + end + end + + def reopen(%{assigns: %{user: admin}} = conn, %{id: id}) do + with {:ok, activity} <- CommonAPI.update_report_state(id, "open"), + report <- Activity.get_by_id_with_user_actor(activity.id) do + ModerationLog.insert_log(%{ + action: "report_update", + actor: admin, + subject: activity, + subject_actor: report.user_actor + }) + + render(conn, "show.json", Report.extract_report_info(report)) + else + {:error, error} -> + json_response(conn, :bad_request, %{error: error}) + end + end + + defp restrict_state(opts, %{resolved: true}), do: Map.put(opts, :state, "resolved") + + defp restrict_state(opts, %{resolved: false}), do: Map.put(opts, :state, "open") + + defp restrict_state(opts, _params), do: opts + + defp restrict_actor(opts, %{account_id: actor}) do + with %User{ap_id: ap_id} <- User.get_by_id(actor) do + Map.put(opts, :actor_id, ap_id) + else + _ -> Map.put(opts, :actor_id, actor) + end + end + + defp restrict_actor(opts, _params), do: opts +end diff --git a/lib/pleroma/web/mastodon_api/admin/views/account_view.ex b/lib/pleroma/web/mastodon_api/admin/views/account_view.ex new file mode 100644 index 000000000..20ccfd51c --- /dev/null +++ b/lib/pleroma/web/mastodon_api/admin/views/account_view.ex @@ -0,0 +1,64 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.Admin.AccountView do + use Pleroma.Web, :view + + alias Pleroma.User + alias Pleroma.Web.CommonAPI.Utils + alias Pleroma.Web.MastodonAPI + + def render("index.json", %{users: users}) do + render_many(users, __MODULE__, "show.json", as: :user) + end + + def render("show.json", %{user: user}) do + account = + MastodonAPI.AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + + %{ + id: user.id, + username: username_from_nickname(user.nickname), + domain: domain_from_nickname(user.nickname), + created_at: Utils.to_masto_date(user.inserted_at), + email: user.email, + ip: nil, + ips: [], + locale: nil, + invite_request: user.registration_reason, + role: role(user), + confirmed: user.is_confirmed, + approved: user.is_approved, + disabled: !user.is_active, + silenced: nil, + suspended: nil, + account: account + } + end + + defp username_from_nickname(string) when is_binary(string) do + hd(String.split(string, "@")) + end + + defp username_from_nickname(_), do: nil + + defp domain_from_nickname(string) when is_binary(string) do + String.split(string, "@") + |> Enum.at(1, nil) + end + + defp domain_from_nickname(_), do: nil + + defp role(%User{is_admin: true}) do + "admin" + end + + defp role(%User{is_moderator: true}) do + "moderator" + end + + defp role(_user) do + nil + end +end diff --git a/lib/pleroma/web/mastodon_api/admin/views/report_view.ex b/lib/pleroma/web/mastodon_api/admin/views/report_view.ex new file mode 100644 index 000000000..664f58b03 --- /dev/null +++ b/lib/pleroma/web/mastodon_api/admin/views/report_view.ex @@ -0,0 +1,56 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.Admin.ReportView do + use Pleroma.Web, :view + + alias Pleroma.HTML + alias Pleroma.Web.AdminAPI.Report + alias Pleroma.Web.CommonAPI.Utils + alias Pleroma.Web.MastodonAPI.Admin.AccountView + alias Pleroma.Web.MastodonAPI.StatusView + + def render("index.json", %{reports: reports}) do + reports + |> Enum.map(&Report.extract_report_info/1) + |> Enum.map(&render(__MODULE__, "show.json", &1)) + end + + def render("show.json", %{ + report: report, + user: account, + account: target_account, + statuses: statuses + }) do + created_at = Utils.to_masto_date(report.data["published"]) + + content = + unless is_nil(report.data["content"]) do + HTML.filter_tags(report.data["content"]) + else + nil + end + + %{ + id: report.id, + action_taken: report.data["state"] != "open", + action_taken_at: nil, + category: "other", + comment: content, + forwarded: nil, + created_at: created_at, + updated_at: created_at, + account: AccountView.render("show.json", %{user: account}), + target_account: AccountView.render("show.json", %{user: target_account}), + assigned_account: nil, + action_taken_by_account: nil, + statuses: + StatusView.render("index.json", %{ + activities: statuses, + as: :activity + }), + rules: [] + } + end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 20ac1c67b..a8ba84036 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -418,6 +418,46 @@ defmodule Pleroma.Web.Router do delete("/chats/:id/messages/:message_id", ChatController, :delete_message) end + # Mastodon AdminAPI: admins and mods (staff) can perform these actions (if privileged by role) + scope "/api/v1/admin", Pleroma.Web.MastodonAPI.Admin do + pipe_through([:require_privileged_role_users_read]) + + get("/accounts", AccountController, :index) + get("/accounts/:id", AccountController, :show) + end + + # Mastodon AdminAPI: admins and mods (staff) can perform these actions (if privileged by role) + scope "/api/v1/admin", Pleroma.Web.MastodonAPI.Admin do + pipe_through(:require_privileged_role_users_delete) + + delete("/accounts/:id", AccountController, :delete) + end + + # Mastodon AdminAPI: admins and mods (staff) can perform these actions (if privileged by role) + scope "/api/v1/admin", Pleroma.Web.MastodonAPI.Admin do + pipe_through([:require_privileged_role_users_manage_activation_state]) + + post("/accounts/:id/action", AccountController, :account_action) + post("/accounts/:id/enable", AccountController, :enable) + end + + # Mastodon AdminAPI: admins and mods (staff) can perform these actions (if privileged by role) + scope "/api/v1/admin", Pleroma.Web.MastodonAPI.Admin do + pipe_through([:require_privileged_role_users_manage_invites]) + post("/accounts/:id/approve", AccountController, :approve) + post("/accounts/:id/reject", AccountController, :reject) + end + + # Mastodon AdminAPI: admins and mods (staff) can perform these actions (if privileged by role) + scope "/api/v1/admin", Pleroma.Web.MastodonAPI.Admin do + pipe_through([:require_privileged_role_reports_manage_reports]) + + get("/reports", ReportController, :index) + get("/reports/:id", ReportController, :show) + post("/reports/:id/resolve", ReportController, :resolve) + post("/reports/:id/reopen", ReportController, :reopen) + end + # AdminAPI: admins and mods (staff) can perform these actions (if privileged by role) scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do pipe_through(:require_privileged_role_emoji_manage_emoji) diff --git a/test/pleroma/web/mastodon_api/admin/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/admin/controllers/account_controller_test.exs new file mode 100644 index 000000000..7280691d4 --- /dev/null +++ b/test/pleroma/web/mastodon_api/admin/controllers/account_controller_test.exs @@ -0,0 +1,175 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.Admin.AccountControllerTest do + use Pleroma.Web.ConnCase + use Oban.Testing, repo: Pleroma.Repo + + import Pleroma.Factory + + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + + :ok + end + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + {:ok, %{admin: admin, token: token, conn: conn}} + end + + describe "GET /api/v1/admin/accounts" do + test "search by display name", %{conn: conn} do + %{id: id} = insert(:user, name: "Display name") + insert(:user, name: "Other name") + + assert [%{"id" => ^id}] = + conn + |> get("/api/v1/admin/accounts?display_name=Display") + |> json_response_and_validate_schema(200) + end + end + + describe "GET /api/v1/admin/accounts/:id" do + test "show admin-level information", %{conn: conn} do + %{id: id} = + insert(:user, + email: "email@example.com", + is_confirmed: false, + is_moderator: true + ) + + assert %{ + "id" => ^id, + "email" => "email@example.com", + "confirmed" => false, + "role" => "moderator" + } = + conn + |> get("/api/v1/admin/accounts/#{id}") + |> json_response_and_validate_schema(200) + end + end + + describe "DELETE /api/v1/admin/accounts/:id" do + test "delete account", %{conn: conn} do + %{id: id} = user = insert(:user) + + conn + |> delete("/api/v1/admin/accounts/#{id}") + |> json_response_and_validate_schema(200) + + user = Repo.reload!(user) + + assert %{is_active: false} = user + end + end + + describe "POST /api/v1/admin/accounts/:id/action" do + test "disable account", %{conn: conn} do + %{id: id} = user = insert(:user) + + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/admin/accounts/#{id}/action", %{ + "type" => "disable" + }) + |> json_response_and_validate_schema(204) + + user = Repo.reload!(user) + + assert %{is_active: false} = user + end + + test "perform action with assigned report", %{conn: conn} do + [reporter, target_user] = insert_pair(:user) + + {:ok, %{id: report_id} = report} = + CommonAPI.report(reporter, %{ + account_id: target_user.id + }) + + %{id: id} = insert(:user) + + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/admin/accounts/#{id}/action", %{ + "type" => "none", + "report_id" => report_id + }) + |> json_response_and_validate_schema(204) + + report = Repo.reload!(report) + + assert %{data: %{"state" => "resolved"}} = report + end + end + + describe "POST /api/v1/admin/accounts/:id/enable" do + test "enable account", %{conn: conn} do + %{id: id} = user = insert(:user) + User.set_activation(user, false) + + conn + |> post("/api/v1/admin/accounts/#{id}/enable") + |> json_response_and_validate_schema(200) + + user = Repo.reload!(user) + + assert %{is_active: true} = user + end + end + + describe "POST /api/v1/admin/accounts/:id/approve" do + test "approve account", %{conn: conn} do + %{id: id} = user = insert(:user, is_approved: false) + + conn + |> post("/api/v1/admin/accounts/#{id}/approve") + |> json_response_and_validate_schema(200) + + user = Repo.reload!(user) + + assert %{is_approved: true} = user + end + end + + describe "POST /api/v1/admin/accounts/:id/rejct" do + test "reject account", %{conn: conn} do + %{id: id} = user = insert(:user, is_approved: false) + + conn + |> post("/api/v1/admin/accounts/#{id}/reject") + |> json_response_and_validate_schema(200) + + user = Repo.reload!(user) + + assert %{is_active: false} = user + end + + test "do not allow rejecting already accepted accounts", %{conn: conn} do + %{id: id} = user = insert(:user, is_approved: true) + + assert %{"error" => "User is approved"} == + conn + |> post("/api/v1/admin/accounts/#{id}/reject") + |> json_response_and_validate_schema(400) + + user = Repo.reload!(user) + + assert %{is_approved: true} = user + end + end +end diff --git a/test/pleroma/web/mastodon_api/admin/controllers/report_controller_test.exs b/test/pleroma/web/mastodon_api/admin/controllers/report_controller_test.exs new file mode 100644 index 000000000..a0cc5f459 --- /dev/null +++ b/test/pleroma/web/mastodon_api/admin/controllers/report_controller_test.exs @@ -0,0 +1,118 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.Admin.ReportControllerTest do + use Pleroma.Web.ConnCase + use Oban.Testing, repo: Pleroma.Repo + + import Pleroma.Factory + + alias Pleroma.Web.CommonAPI + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + + :ok + end + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + {:ok, %{admin: admin, token: token, conn: conn}} + end + + describe "GET /api/v1/admin/reports" do + test "get reports by state", %{conn: conn} do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %{id: report_id1}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "this user uses an app called Hitler Tusky", + status_ids: [activity.id] + }) + + {:ok, %{id: report_id2}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + status_ids: [activity.id] + }) + + CommonAPI.update_report_state(report_id2, "resolved") + + assert [%{"id" => ^report_id1}] = + conn + |> get("/api/v1/admin/reports?resolved=false") + |> json_response_and_validate_schema(200) + + assert [%{"id" => ^report_id2}] = + conn + |> get("/api/v1/admin/reports?resolved=true") + |> json_response_and_validate_schema(200) + end + end + + describe "GET /api/v1/admin/reports/:id" do + test "get report by id", %{conn: conn} do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %{id: report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + status_ids: [activity.id] + }) + + assert %{"id" => ^report_id} = + conn + |> get("/api/v1/admin/reports/#{report_id}") + |> json_response_and_validate_schema(200) + end + end + + describe "POST /api/v1/admin/reports/:id/resolve" do + test "resolve a report", %{conn: conn} do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %{id: report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + status_ids: [activity.id] + }) + + assert %{"id" => ^report_id, "action_taken" => true} = + conn + |> post("/api/v1/admin/reports/#{report_id}/resolve") + |> json_response_and_validate_schema(200) + end + end + + describe "POST /api/v1/admin/reports/:id/reopen" do + test "reopen a report", %{conn: conn} do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %{id: report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + status_ids: [activity.id] + }) + + CommonAPI.update_report_state(report_id, "resolved") + + assert %{"id" => ^report_id, "action_taken" => false} = + conn + |> post("/api/v1/admin/reports/#{report_id}/reopen") + |> json_response_and_validate_schema(200) + end + end +end From 2c14121389d0dadd0609695b941881eb5d93cbae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 19 Sep 2025 16:59:36 +0200 Subject: [PATCH 039/391] MastoAPI: /api/v2/admin/accounts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- lib/pleroma/user/query.ex | 5 + .../mastodon_admin/account_operation.ex | 118 +++++++++++++++--- .../mastodon_admin/report_operation.ex | 12 +- .../admin/controllers/account_controller.ex | 48 ++++++- lib/pleroma/web/router.ex | 7 ++ 5 files changed, 164 insertions(+), 26 deletions(-) diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex index b877b52b2..33f19c454 100644 --- a/lib/pleroma/user/query.ex +++ b/lib/pleroma/user/query.ex @@ -302,6 +302,11 @@ defmodule Pleroma.User.Query do where(query, [u], u.is_admin == true or u.is_moderator == true) end + defp compose_query({:domain, domain}, query) do + query + |> where([u], like(u.nickname, ^"%@#{domain}")) + end + defp compose_query(_unsupported_param, query), do: query defp location_query(query, local) do diff --git a/lib/pleroma/web/api_spec/operations/mastodon_admin/account_operation.ex b/lib/pleroma/web/api_spec/operations/mastodon_admin/account_operation.ex index 23dae0306..19ccf97fb 100644 --- a/lib/pleroma/web/api_spec/operations/mastodon_admin/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/mastodon_admin/account_operation.ex @@ -19,7 +19,7 @@ defmodule Pleroma.Web.ApiSpec.MastodonAdmin.AccountOperation do def index_operation do %Operation{ tags: ["User administration (Mastodon API)"], - summary: "View accounts by criteria", + summary: "View accounts by criteria (v1)", operationId: "MastodonAdmin.AccountController.index", description: "View accounts matching certain criteria for filtering, up to 40 at a time.", security: [%{"oAuth" => ["admin:read:accounts"]}], @@ -27,12 +27,6 @@ defmodule Pleroma.Web.ApiSpec.MastodonAdmin.AccountOperation do [ Operation.parameter(:local, :query, :boolean, "Filter for local accounts?"), Operation.parameter(:remote, :query, :boolean, "Filter for remote accounts?"), - Operation.parameter( - :by_domain, - :query, - :string, - "Filter by the given domain (not implemented yet)" - ), Operation.parameter( :active, :query, @@ -51,12 +45,6 @@ defmodule Pleroma.Web.ApiSpec.MastodonAdmin.AccountOperation do :boolean, "Filter for currently disabled accounts?" ), - Operation.parameter( - :sensitized, - :query, - :boolean, - "Filter for currently sensitized accounts? (not implemented yet)" - ), Operation.parameter( :silenced, :query, @@ -69,14 +57,31 @@ defmodule Pleroma.Web.ApiSpec.MastodonAdmin.AccountOperation do :boolean, "Filter for currently suspended accounts? (not implemented yet)" ), - Operation.parameter(:username, :query, :string, "Username to search for"), - Operation.parameter(:display_name, :query, :string, "Display name to search for"), + Operation.parameter( + :sensitized, + :query, + :boolean, + "Filter for accounts force-marked as sensitive? (not implemented yet)" + ), + Operation.parameter(:username, :query, :string, "Search for the given username"), + Operation.parameter( + :display_name, + :query, + :string, + "Search for the given display name" + ), + Operation.parameter( + :by_domain, + :query, + :string, + "Filter by the given domain" + ), Operation.parameter(:email, :query, :string, "Lookup a user with this email"), Operation.parameter( :ip, :query, :string, - "Lookup users by this IP address (not implemented yet)" + "Lookup users with this IP address (not implemented yet)" ), Operation.parameter(:staff, :query, :boolean, "Filter for staff accounts?") ] ++ @@ -93,6 +98,87 @@ defmodule Pleroma.Web.ApiSpec.MastodonAdmin.AccountOperation do } end + def index2_operation do + %Operation{ + tags: ["User administration (Mastodon API)"], + summary: "View accounts by criteria (v2)", + operationId: "MastodonAdmin.AccountController.index2", + description: "View accounts matching certain criteria for filtering, up to 40 at a time.", + security: [%{"oAuth" => ["admin:read:accounts"]}], + parameters: + [ + Operation.parameter( + :origin, + :query, + %Schema{type: :string, enum: ["local", "remote"]}, + "Filter for local or remote accounts" + ), + Operation.parameter( + :status, + :query, + %Schema{ + type: :string, + enum: ["active", "inactive", "pending", "disabled", "silenced", "suspended"] + }, + "Filter for active, pending, disabled, silenced or suspended accounts" + ), + Operation.parameter( + :permissions, + :query, + :string, + "Filter for accounts with staff permissions (users that can manage reports). (not implemented yet)" + ), + Operation.parameter( + :role_ids, + :query, + %Schema{ + oneOf: [ + %Schema{type: :array, items: %Schema{type: :string}}, + %Schema{type: :string} + ] + }, + "Filter for users with these roles. (not implemented yet)" + ), + Operation.parameter( + :invited_by, + :query, + :string, + "Lookup users invited by the account with this ID. (not implemented yet)" + ), + Operation.parameter(:username, :query, :string, "Search for the given username"), + Operation.parameter( + :display_name, + :query, + :string, + "Search for the given display name" + ), + Operation.parameter( + :by_domain, + :query, + :string, + "Filter by the given domain" + ), + Operation.parameter(:email, :query, :string, "Lookup a user with this email"), + Operation.parameter( + :ip, + :query, + :string, + "Lookup users with this IP address (not implemented yet)" + ) + ] ++ + pagination_params(), + responses: %{ + 200 => + Operation.response("Account", "application/json", %Schema{ + title: "ArrayOfAccounts", + type: :array, + items: account() + }), + 401 => Operation.response("Error", "application/json", ApiError) + } + } + end + def show_operation do %Operation{ tags: ["User administration (Mastodon API)"], diff --git a/lib/pleroma/web/api_spec/operations/mastodon_admin/report_operation.ex b/lib/pleroma/web/api_spec/operations/mastodon_admin/report_operation.ex index 274ce3bd9..487e7853e 100644 --- a/lib/pleroma/web/api_spec/operations/mastodon_admin/report_operation.ex +++ b/lib/pleroma/web/api_spec/operations/mastodon_admin/report_operation.ex @@ -119,16 +119,12 @@ defmodule Pleroma.Web.ApiSpec.MastodonAdmin.ReportOperation do account: AccountOperation.account(), target_account: AccountOperation.account(), assigned_account: %Schema{ - oneOf: [ - AccountOperation.account(), - %Schema{type: :null} - ] + type: AccountOperation.account(), + nullable: true }, action_taken_by_account: %Schema{ - oneOf: [ - AccountOperation.account(), - %Schema{type: :null} - ] + type: AccountOperation.account(), + nullable: true }, statuses: %Schema{ type: :array, diff --git a/lib/pleroma/web/mastodon_api/admin/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/admin/controllers/account_controller.ex index a1eac7fe3..833bf1db5 100644 --- a/lib/pleroma/web/mastodon_api/admin/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/admin/controllers/account_controller.ex @@ -22,7 +22,7 @@ defmodule Pleroma.Web.MastodonAPI.Admin.AccountController do alias Pleroma.Web.Plugs.OAuthScopesPlug @filter_params ~W( - local external active needing_approval deactivated nickname name email staff + local external active needing_approval deactivated nickname name domain email staff origin status ) plug(Pleroma.Web.ApiSpec.CastAndValidate) @@ -30,7 +30,7 @@ defmodule Pleroma.Web.MastodonAPI.Admin.AccountController do plug( OAuthScopesPlug, %{scopes: ["admin:read:accounts"]} - when action in [:index, :show] + when action in [:index, :index2, :show] ) plug( @@ -71,6 +71,18 @@ defmodule Pleroma.Web.MastodonAPI.Admin.AccountController do |> render("index.json", users: users) end + def index2(conn, params) do + users = + params + |> build_criteria_v2() + |> User.Query.build() + |> Pagination.fetch_paginated(params) + + conn + |> add_link_headers(users) + |> render("index.json", users: users) + end + def show(%{assigns: %{user: _admin, account: user}} = conn, _params) do render(conn, "show.json", user: user) end @@ -162,10 +174,21 @@ defmodule Pleroma.Web.MastodonAPI.Admin.AccountController do |> maybe_filter_deactivated(params) |> maybe_filter_nickname(params) |> maybe_filter_name(params) + |> maybe_filter_domain(params) |> maybe_filter_email(params) |> maybe_filter_staff(params) end + defp build_criteria_v2(params) do + %{} + |> maybe_filter_origin(params) + |> maybe_filter_status(params) + |> maybe_filter_nickname(params) + |> maybe_filter_name(params) + |> maybe_filter_domain(params) + |> maybe_filter_email(params) + end + defp maybe_filter_local(criteria, %{local: true} = _params), do: Map.put(criteria, :local, true) @@ -178,6 +201,12 @@ defmodule Pleroma.Web.MastodonAPI.Admin.AccountController do defp maybe_filter_external(criteria, %{remote: false} = _params), do: Map.put(criteria, :local, true) + defp maybe_filter_origin(criteria, %{origin: "local"} = _params), + do: Map.put(criteria, :local, true) + + defp maybe_filter_origin(criteria, %{origin: "remote"} = _params), + do: Map.put(criteria, :external, true) + defp maybe_filter_active(criteria, %{active: active} = _params), do: Map.put(criteria, :active, active) @@ -187,12 +216,27 @@ defmodule Pleroma.Web.MastodonAPI.Admin.AccountController do defp maybe_filter_deactivated(criteria, %{disabled: deactivated} = _params), do: Map.put(criteria, :deactivated, deactivated) + defp maybe_filter_status(criteria, %{status: "active"} = _params), + do: Map.put(criteria, :active, true) + + defp maybe_filter_status(criteria, %{status: "inactive"} = _params), + do: Map.put(criteria, :active, false) + + defp maybe_filter_status(criteria, %{status: "pending"} = _params), + do: Map.put(criteria, :need_approval, true) + + defp maybe_filter_status(criteria, %{status: "disabled"} = _params), + do: Map.put(criteria, :deactivated, true) + defp maybe_filter_nickname(criteria, %{username: nickname} = _params), do: Map.put(criteria, :nickname, nickname) defp maybe_filter_name(criteria, %{display_name: name} = _params), do: Map.put(criteria, :name, name) + defp maybe_filter_domain(criteria, %{by_domain: domain} = _params), + do: Map.put(criteria, :domain, domain) + defp maybe_filter_email(criteria, %{email: email} = _params), do: Map.put(criteria, :email, email) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index a8ba84036..87568b481 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -458,6 +458,13 @@ defmodule Pleroma.Web.Router do post("/reports/:id/reopen", ReportController, :reopen) end + # Mastodon AdminAPI: admins and mods (staff) can perform these actions (if privileged by role) + scope "/api/v2/admin", Pleroma.Web.MastodonAPI.Admin do + pipe_through([:require_privileged_role_users_read]) + + get("/accounts", AccountController, :index2) + end + # AdminAPI: admins and mods (staff) can perform these actions (if privileged by role) scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do pipe_through(:require_privileged_role_emoji_manage_emoji) From 87e6cd3dc009f977fec7fddb664c73cc525d9fe1 Mon Sep 17 00:00:00 2001 From: mkljczk Date: Tue, 18 Mar 2025 00:34:37 +0100 Subject: [PATCH 040/391] Adapt tests for v2/admin/accounts from Mastodon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: mkljczk Signed-off-by: nicole mikołajczyk --- .../controllers/account_controller_test.exs | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/test/pleroma/web/mastodon_api/admin/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/admin/controllers/account_controller_test.exs index 7280691d4..e06b3a70a 100644 --- a/test/pleroma/web/mastodon_api/admin/controllers/account_controller_test.exs +++ b/test/pleroma/web/mastodon_api/admin/controllers/account_controller_test.exs @@ -42,6 +42,96 @@ defmodule Pleroma.Web.MastodonAPI.Admin.AccountControllerTest do end end + # Adapted from + # https://github.com/mastodon/mastodon/blob/main/spec/requests/api/v2/admin/accounts_spec.rb + describe "GET /api/v2/admin/accounts" do + setup do + remote_account = insert(:user, nickname: "remote@example.org", local: false) + other_remote_account = insert(:user, nickname: "other@foo.bar", local: false) + # suspended_account = insert(:user) + # suspended_remote = insert(:user) + disabled_account = insert(:user, is_active: false) + pending_account = insert(:user, is_approved: false) + admin_account = insert(:user, is_admin: true) + + {:ok, + %{ + remote_account: remote_account, + other_remote_account: other_remote_account, + disabled_account: disabled_account, + pending_account: pending_account, + admin_account: admin_account + }} + end + + # test "returns the correct accounts when called with status active + # and origin local and permissions staff", %{ + # conn: conn, + # admin_account: %{id: admin_account_id} + # } do + # assert [%{"id" => ^admin_account_id}] = + # conn + # |> get("/api/v2/admin/accounts?status=active&origin=local&permissions=staff") + # |> json_response_and_validate_schema(200) + # end + + test "returns the correct accounts when called with by_domain value and origin remote", %{ + conn: conn, + remote_account: %{id: remote_account_id} + } do + assert [%{"id" => ^remote_account_id}] = + conn + |> get("/api/v2/admin/accounts?by_domain=example.org&origin=remote") + |> json_response_and_validate_schema(200) + end + + # test "returns the correct accounts when called with status suspended", %{ + # conn: conn, + # suspended_account: %{id: suspended_account_id} + # } do + # assert [%{"id" => ^suspended_account_id}] = + # conn + # |> get("/api/v2/admin/accounts?status=suspended") + # |> json_response_and_validate_schema(200) + # end + + test "returns the correct accounts when called with status disabled", %{ + conn: conn, + disabled_account: %{id: disabled_account_id} + } do + assert [%{"id" => ^disabled_account_id}] = + conn + |> get("/api/v2/admin/accounts?status=disabled") + |> json_response_and_validate_schema(200) + end + + test "returns the correct accounts when called with status pending", %{ + conn: conn, + pending_account: %{id: pending_account_id} + } do + assert [%{"id" => ^pending_account_id}] = + conn + |> get("/api/v2/admin/accounts?status=pending") + |> json_response_and_validate_schema(200) + end + + test "sets the correct pagination headers with limit param", %{ + conn: conn, + admin_account: %{id: admin_account_id} + } do + response = + conn + |> get("/api/v2/admin/accounts?limit=1") + + next_url = + ~r{<.+?(?/api[^>]+)>; rel=\"next\"} + |> Regex.named_captures(get_resp_header(response, "link") |> Enum.at(0)) + |> Map.get("link") + + next_url =~ "&limit=1&max_id=#{admin_account_id}" + end + end + describe "GET /api/v1/admin/accounts/:id" do test "show admin-level information", %{conn: conn} do %{id: id} = From 8626a337f807d6ab97840419148f187d081d18bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 27 Feb 2026 22:34:18 +0100 Subject: [PATCH 041/391] Mastodon admin API v2 improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../api_spec/operations/mastodon_admin/account_operation.ex | 6 +++--- .../api_spec/operations/mastodon_admin/report_operation.ex | 4 ++-- .../web/mastodon_api/admin/controllers/report_controller.ex | 2 +- .../admin/controllers/account_controller_test.exs | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/mastodon_admin/account_operation.ex b/lib/pleroma/web/api_spec/operations/mastodon_admin/account_operation.ex index 19ccf97fb..e9143364c 100644 --- a/lib/pleroma/web/api_spec/operations/mastodon_admin/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/mastodon_admin/account_operation.ex @@ -31,7 +31,7 @@ defmodule Pleroma.Web.ApiSpec.MastodonAdmin.AccountOperation do :active, :query, :boolean, - "Filter for currently active accounts??" + "Filter for currently active accounts?" ), Operation.parameter( :pending, @@ -327,8 +327,8 @@ defmodule Pleroma.Web.ApiSpec.MastodonAdmin.AccountOperation do } } }, - locale: %Schema{type: :string, format: "date-time", nullable: true}, - invite_request: %Schema{type: :string, format: "date-time", nullable: true}, + locale: %Schema{type: :string, nullable: true}, + invite_request: %Schema{type: :string, nullable: true}, role: %Schema{type: :string, nullable: true}, confirmed: %Schema{type: :boolean}, approved: %Schema{type: :boolean}, diff --git a/lib/pleroma/web/api_spec/operations/mastodon_admin/report_operation.ex b/lib/pleroma/web/api_spec/operations/mastodon_admin/report_operation.ex index 487e7853e..3360cb843 100644 --- a/lib/pleroma/web/api_spec/operations/mastodon_admin/report_operation.ex +++ b/lib/pleroma/web/api_spec/operations/mastodon_admin/report_operation.ex @@ -119,11 +119,11 @@ defmodule Pleroma.Web.ApiSpec.MastodonAdmin.ReportOperation do account: AccountOperation.account(), target_account: AccountOperation.account(), assigned_account: %Schema{ - type: AccountOperation.account(), + oneOf: [AccountOperation.account()], nullable: true }, action_taken_by_account: %Schema{ - type: AccountOperation.account(), + oneOf: [AccountOperation.account()], nullable: true }, statuses: %Schema{ diff --git a/lib/pleroma/web/mastodon_api/admin/controllers/report_controller.ex b/lib/pleroma/web/mastodon_api/admin/controllers/report_controller.ex index 217a97cc9..2d89d13bc 100644 --- a/lib/pleroma/web/mastodon_api/admin/controllers/report_controller.ex +++ b/lib/pleroma/web/mastodon_api/admin/controllers/report_controller.ex @@ -65,7 +65,7 @@ defmodule Pleroma.Web.MastodonAPI.Admin.ReportController do subject_actor: report.user_actor }) - render(conn, "show.json", Report.extract_report_info(activity)) + render(conn, "show.json", Report.extract_report_info(report)) else {:error, error} -> json_response(conn, :bad_request, %{error: error}) diff --git a/test/pleroma/web/mastodon_api/admin/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/admin/controllers/account_controller_test.exs index e06b3a70a..113b242dd 100644 --- a/test/pleroma/web/mastodon_api/admin/controllers/account_controller_test.exs +++ b/test/pleroma/web/mastodon_api/admin/controllers/account_controller_test.exs @@ -236,7 +236,7 @@ defmodule Pleroma.Web.MastodonAPI.Admin.AccountControllerTest do end end - describe "POST /api/v1/admin/accounts/:id/rejct" do + describe "POST /api/v1/admin/accounts/:id/reject" do test "reject account", %{conn: conn} do %{id: id} = user = insert(:user, is_approved: false) From a72f9cff232c6afd7aff8ab41b21d82779d8a086 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 16 Oct 2025 15:03:54 +0200 Subject: [PATCH 042/391] Elixir 1.19: Fix typing violation on struct updates in Pleroma.Marker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit warning: a struct for Pleroma.Marker is expected on struct update: %Pleroma.Marker{marker | user: user} but got type: dynamic() where "marker" was given the type: # type: dynamic() # from: lib/pleroma/marker.ex {:ok, marker} when defining the variable "marker", you must also pattern match on "%Pleroma.Marker{}". hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of: user = some_function() %User{user | name: "John Doe"} it is enough to write: %User{} = user = some_function() %{user | name: "John Doe"} typing violation found at: │ 81 │ {:ok, marker} -> %__MODULE__{marker | user: user} │ ~ │ └─ lib/pleroma/marker.ex:81:24: Pleroma.Marker.get_marker/2 --- lib/pleroma/marker.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/marker.ex b/lib/pleroma/marker.ex index 68b054e4d..4f645240e 100644 --- a/lib/pleroma/marker.ex +++ b/lib/pleroma/marker.ex @@ -78,7 +78,7 @@ defmodule Pleroma.Marker do defp get_marker(user, timeline) do case Repo.find_resource(get_query(user, timeline)) do - {:ok, marker} -> %__MODULE__{marker | user: user} + {:ok, %__MODULE__{} = marker} -> %__MODULE__{marker | user: user} _ -> %__MODULE__{timeline: timeline, user_id: user.id} end end From 0fc046a75e0499a068cbdee82f9c559407a7521b Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 16 Oct 2025 15:06:02 +0200 Subject: [PATCH 043/391] Elixir 1.19: Fix typing violation on struct updates in MFA.Changeset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit warning: a struct for Pleroma.MFA.Settings is expected on struct update: %Pleroma.MFA.Settings{settings | enabled: false} but got type: dynamic() where "settings" was given the type: # type: dynamic() # from: lib/pleroma/mfa/changeset.ex:11:14 settings = Pleroma.MFA.fetch_settings(Ecto.Changeset.apply_changes(changeset)) when defining the variable "settings", you must also pattern match on "%Pleroma.MFA.Settings{}". hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of: user = some_function() %User{user | name: "John Doe"} it is enough to write: %User{} = user = some_function() %{user | name: "John Doe"} typing violation found at: │ 17 │ put_change(changeset, %Settings{settings | enabled: false}) │ ~ │ └─ lib/pleroma/mfa/changeset.ex:17:29: Pleroma.MFA.Changeset.disable/2 --- warning: a struct for Pleroma.MFA.Settings is expected on struct update: %Pleroma.MFA.Settings{ settings | totp: %Pleroma.MFA.Settings.TOTP{confirmed: false, delivery_type: "app", secret: nil} } but got type: dynamic() where "settings" was given the type: # type: dynamic() # from: lib/pleroma/mfa/changeset.ex:23:74 %Pleroma.User{multi_factor_authentication_settings: settings} = user when defining the variable "settings", you must also pattern match on "%Pleroma.MFA.Settings{}". hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of: user = some_function() %User{user | name: "John Doe"} it is enough to write: %User{} = user = some_function() %{user | name: "John Doe"} typing violation found at: │ 25 │ |> put_change(%Settings{settings | totp: %Settings.TOTP{}}) │ ~ │ └─ lib/pleroma/mfa/changeset.ex:25:19: Pleroma.MFA.Changeset.disable_totp/1 --- lib/pleroma/mfa/changeset.ex | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/mfa/changeset.ex b/lib/pleroma/mfa/changeset.ex index 3ec3cfe91..890cb2193 100644 --- a/lib/pleroma/mfa/changeset.ex +++ b/lib/pleroma/mfa/changeset.ex @@ -8,7 +8,7 @@ defmodule Pleroma.MFA.Changeset do alias Pleroma.User def disable(%Ecto.Changeset{} = changeset, force \\ false) do - settings = + %Settings{} = settings = changeset |> Ecto.Changeset.apply_changes() |> MFA.fetch_settings() @@ -20,20 +20,20 @@ defmodule Pleroma.MFA.Changeset do end end - def disable_totp(%User{multi_factor_authentication_settings: settings} = user) do + def disable_totp(%User{multi_factor_authentication_settings: %Settings{} = settings} = user) do user |> put_change(%Settings{settings | totp: %Settings.TOTP{}}) end - def confirm_totp(%User{multi_factor_authentication_settings: settings} = user) do - totp_settings = %Settings.TOTP{settings.totp | confirmed: true} + def confirm_totp(%User{multi_factor_authentication_settings: %Settings{} = settings} = user) do + totp_settings = %Settings.TOTP{%Settings.TOTP{} = settings.totp | confirmed: true} user |> put_change(%Settings{settings | totp: totp_settings, enabled: true}) end def setup_totp(%User{} = user, attrs) do - mfa_settings = MFA.fetch_settings(user) + %Settings{} = mfa_settings = MFA.fetch_settings(user) totp_settings = %Settings.TOTP{} @@ -46,7 +46,7 @@ defmodule Pleroma.MFA.Changeset do def cast_backup_codes(%User{} = user, codes) do user |> put_change(%Settings{ - user.multi_factor_authentication_settings + %Settings{} = user.multi_factor_authentication_settings | backup_codes: codes }) end From 0d3085937ca728c572f19798d21dce12dcf7b89e Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 16 Oct 2025 15:09:03 +0200 Subject: [PATCH 044/391] Elixir 1.19: Fix typing violation on struct updates in Pleroma.Upload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit warning: a struct for Pleroma.Upload is expected on struct update: %Pleroma.Upload{ upload | path: case upload.path do x when x === false or x === nil -> <> x -> x end } but got type: dynamic() where "upload" was given the type: # type: dynamic() # from: lib/pleroma/upload.ex:95:24 {:ok, upload} <- prepare_upload(upload, opts) when defining the variable "upload", you must also pattern match on "%Pleroma.Upload{}". hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of: user = some_function() %User{user | name: "John Doe"} it is enough to write: %User{} = user = some_function() %{user | name: "John Doe"} typing violation found at: │ 96 │ upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"}, │ ~ │ └─ lib/pleroma/upload.ex:96:19: Pleroma.Upload.store/2 --- lib/pleroma/upload.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex index 06d8005bc..350ff6cb3 100644 --- a/lib/pleroma/upload.ex +++ b/lib/pleroma/upload.ex @@ -93,7 +93,7 @@ defmodule Pleroma.Upload do def store(upload, opts \\ []) do opts = get_opts(opts) - with {:ok, upload} <- prepare_upload(upload, opts), + with {:ok, %__MODULE__{} = 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(upload), From 6f62cb0ed38c5d2bbdb6e304a287553bad8584f1 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 16 Oct 2025 15:09:51 +0200 Subject: [PATCH 045/391] Elixir 1.19: Fix typing violation on struct updates in Web.ApiSpec.Cast* MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit warning: a struct for Plug.Conn is expected on struct update: %Plug.Conn{conn | query_params: query_params} but got type: dynamic() where "conn" was given the type: # type: dynamic() # from: lib/pleroma/web/api_spec/cast_and_validate.ex:109:43 conn when defining the variable "conn", you must also pattern match on "%Plug.Conn{}". hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of: user = some_function() %User{user | name: "John Doe"} it is enough to write: %User{} = user = some_function() %{user | name: "John Doe"} typing violation found at: │ 133 │ conn = %Conn{conn | query_params: query_params} │ ~ │ └─ lib/pleroma/web/api_spec/cast_and_validate.ex:133:16: Pleroma.Web.ApiSpec.CastAndValidate.cast_and_validate/6 --- lib/pleroma/web/api_spec/cast_and_validate.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/api_spec/cast_and_validate.ex b/lib/pleroma/web/api_spec/cast_and_validate.ex index 672d1c4a1..57ea8e9b3 100644 --- a/lib/pleroma/web/api_spec/cast_and_validate.ex +++ b/lib/pleroma/web/api_spec/cast_and_validate.ex @@ -106,7 +106,7 @@ defmodule Pleroma.Web.ApiSpec.CastAndValidate do OpenApiSpex.cast_and_validate(spec, operation, conn, content_type, cast_opts) end - defp cast_and_validate(spec, operation, conn, content_type, false = _strict, cast_opts) do + defp cast_and_validate(spec, operation, %Conn{} = conn, content_type, false = _strict, cast_opts) do case OpenApiSpex.cast_and_validate(spec, operation, conn, content_type) do {:ok, conn} -> {:ok, conn} From 7c79e4416f78b317d8a3bda4cacd23f3d955df46 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 16 Oct 2025 15:11:33 +0200 Subject: [PATCH 046/391] Elixir 1.19: Fix typing violation on struct updates in Web.ApiSpec.Rend* MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit warning: a struct for OpenApiSpex.Cast.Error is expected on struct update: %OpenApiSpex.Cast.Error{err | name: err.value} but got type: dynamic(%{..., name: nil, reason: :invalid_enum}) where "err" was given the type: # type: dynamic(%{..., name: nil, reason: :invalid_enum}) # from: lib/pleroma/web/api_spec/render_error.ex:20:45 %{name: nil, reason: :invalid_enum} = err when defining the variable "err", you must also pattern match on "%OpenApiSpex.Cast.Error{}". hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of: user = some_function() %User{user | name: "John Doe"} it is enough to write: %User{} = user = some_function() %{user | name: "John Doe"} typing violation found at: │ 21 │ %OpenApiSpex.Cast.Error{err | name: err.value} │ ~ │ └─ lib/pleroma/web/api_spec/render_error.ex:21:11: Pleroma.Web.ApiSpec.RenderError.call/2 --- lib/pleroma/web/api_spec/render_error.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/api_spec/render_error.ex b/lib/pleroma/web/api_spec/render_error.ex index 3539af6e4..acf510774 100644 --- a/lib/pleroma/web/api_spec/render_error.ex +++ b/lib/pleroma/web/api_spec/render_error.ex @@ -17,10 +17,10 @@ defmodule Pleroma.Web.ApiSpec.RenderError do def call(conn, errors) do errors = Enum.map(errors, fn - %{name: nil, reason: :invalid_enum} = err -> + %OpenApiSpex.Cast.Error{name: nil, reason: :invalid_enum} = err -> %OpenApiSpex.Cast.Error{err | name: err.value} - %{name: nil} = err -> + %OpenApiSpex.Cast.Error{name: nil} = err -> %OpenApiSpex.Cast.Error{err | name: List.last(err.path)} err -> From 6a926bf605b6e33c0fe9242fc0b2c1b8f68b655d Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 16 Oct 2025 15:12:41 +0200 Subject: [PATCH 047/391] Elixir 1.19: Fix typing violation on struct updates in CommonAPI.Activity* MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit warning: a struct for Pleroma.Web.CommonAPI.ActivityDraft is expected on struct update: %Pleroma.Web.CommonAPI.ActivityDraft{draft | object: object} but got type: dynamic() where "draft" was given the type: # type: dynamic() # from: lib/pleroma/web/common_api/activity_draft.ex:91:22 draft when defining the variable "draft", you must also pattern match on "%Pleroma.Web.CommonAPI.ActivityDraft{}". hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of: user = some_function() %User{user | name: "John Doe"} it is enough to write: %User{} = user = some_function() %{user | name: "John Doe"} typing violation found at: │ 102 │ %__MODULE__{draft | object: object} │ ~ │ └─ lib/pleroma/web/common_api/activity_draft.ex:102:5: Pleroma.Web.CommonAPI.ActivityDraft.listen_object/1 --- lib/pleroma/web/common_api/activity_draft.ex | 46 ++++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index c0b98508c..c5959276a 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -88,7 +88,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do |> validate() end - defp listen_object(draft) do + defp listen_object(%__MODULE__{} = draft) do object = draft.params |> Map.take([:album, :artist, :title, :length]) @@ -102,20 +102,20 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do %__MODULE__{draft | object: object} end - defp put_params(draft, params) do + defp put_params(%__MODULE__{} = draft, params) do params = Map.put_new(params, :in_reply_to_status_id, params[:in_reply_to_id]) %__MODULE__{draft | params: params} end - defp status(%{params: %{status: status}} = draft) do + defp status(%__MODULE__{params: %{status: status}} = draft) do %__MODULE__{draft | status: String.trim(status)} end - defp summary(%{params: params} = draft) do + defp summary(%__MODULE__{params: params} = draft) do %__MODULE__{draft | summary: Map.get(params, :spoiler_text, "")} end - defp full_payload(%{status: status, summary: summary} = draft) do + defp full_payload(%__MODULE__{status: status, summary: summary} = draft) do full_payload = String.trim(status <> summary) case Utils.validate_character_limit(full_payload, draft.attachments) do @@ -124,7 +124,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do end end - defp attachments(%{params: params} = draft) do + defp attachments(%__MODULE__{params: params} = draft) do attachments = Utils.attachments_from_ids(params, draft.user) draft = %__MODULE__{draft | attachments: attachments} @@ -134,9 +134,9 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do end end - defp in_reply_to(%{params: %{in_reply_to_status_id: ""}} = draft), do: draft + defp in_reply_to(%__MODULE__{params: %{in_reply_to_status_id: ""}} = draft), do: draft - defp in_reply_to(%{params: %{in_reply_to_status_id: id}} = draft) when is_binary(id) do + defp in_reply_to(%__MODULE__{params: %{in_reply_to_status_id: id}} = draft) when is_binary(id) do # If a post was deleted all its activities (except the newly added Delete) are purged too, # thus lookup by Create db ID will yield nil just as if it never existed in the first place. # @@ -166,13 +166,13 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do end end - defp in_reply_to(%{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft) do + defp in_reply_to(%__MODULE__{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft) do %__MODULE__{draft | in_reply_to: in_reply_to} end defp in_reply_to(draft), do: draft - defp quote_post(%{params: %{quoted_status_id: id}} = draft) when not_empty_string(id) do + defp quote_post(%__MODULE__{params: %{quoted_status_id: id}} = draft) when not_empty_string(id) do case Activity.get_by_id_with_object(id) do %Activity{} = activity -> %__MODULE__{draft | quote_post: activity} @@ -188,12 +188,12 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp quote_post(draft), do: draft - defp in_reply_to_conversation(draft) do + defp in_reply_to_conversation(%__MODULE__{} = draft) do in_reply_to_conversation = Participation.get(draft.params[:in_reply_to_conversation_id]) %__MODULE__{draft | in_reply_to_conversation: in_reply_to_conversation} end - defp visibility(%{params: params} = draft) do + defp visibility(%__MODULE__{params: params} = draft) do case CommonAPI.get_visibility(params, draft.in_reply_to, draft.in_reply_to_conversation) do {visibility, "direct"} when visibility != "direct" -> add_error(draft, dgettext("errors", "The message visibility must be direct")) @@ -226,14 +226,14 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp quoting_visibility(draft), do: draft - defp expires_at(draft) do + defp expires_at(%__MODULE__{} = draft) do case CommonAPI.check_expiry_date(draft.params[:expires_in]) do {:ok, expires_at} -> %__MODULE__{draft | expires_at: expires_at} {:error, message} -> add_error(draft, message) end end - defp poll(draft) do + defp poll(%__MODULE__{} = draft) do case Utils.make_poll_data(draft.params) do {:ok, {poll, poll_emoji}} -> %__MODULE__{draft | extra: poll, emoji: Map.merge(draft.emoji, poll_emoji)} @@ -243,7 +243,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do end end - defp content(%{mentions: mentions} = draft) do + defp content(%__MODULE__{mentions: mentions} = draft) do {content_html, mentioned_users, tags} = Utils.make_content_html(draft) mentioned_ap_ids = @@ -257,22 +257,22 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do %__MODULE__{draft | content_html: content_html, mentions: mentions, tags: tags} end - defp to_and_cc(draft) do + defp to_and_cc(%__MODULE__{} = draft) do {to, cc} = Utils.get_to_and_cc(draft) %__MODULE__{draft | to: to, cc: cc} end - defp context(draft) do + defp context(%__MODULE__{} = draft) do context = Utils.make_context(draft.in_reply_to, draft.in_reply_to_conversation) %__MODULE__{draft | context: context} end - defp sensitive(draft) do + defp sensitive(%__MODULE__{} = draft) do sensitive = draft.params[:sensitive] %__MODULE__{draft | sensitive: sensitive} end - defp language(draft) do + defp language(%__MODULE__{} = draft) do language = with language <- draft.params[:language], true <- good_locale_code?(language) do @@ -284,7 +284,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do %__MODULE__{draft | language: language} end - defp object(draft) do + defp object(%__MODULE__{} = draft) do emoji = Map.merge(Pleroma.Emoji.Formatter.get_emoji_map(draft.full_payload), draft.emoji) # Sometimes people create posts with subject containing emoji, @@ -328,12 +328,12 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do %__MODULE__{draft | object: object} end - defp preview?(draft) do + defp preview?(%__MODULE__{} = draft) do preview? = Pleroma.Web.Utils.Params.truthy_param?(draft.params[:preview]) %__MODULE__{draft | preview?: preview?} end - defp changes(draft) do + defp changes(%__MODULE__{} = draft) do direct? = draft.visibility == "direct" additional = %{"cc" => draft.cc, "directMessage" => direct?} @@ -359,7 +359,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp with_valid(%{valid?: true} = draft, func), do: func.(draft) defp with_valid(draft, _func), do: draft - defp add_error(draft, message) do + defp add_error(%__MODULE__{} = draft, message) do %__MODULE__{draft | valid?: false, errors: [message | draft.errors]} end From 9040434886526e082bf675e46e8cadb3f64e7fe9 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 9 Jan 2026 16:40:19 +0100 Subject: [PATCH 048/391] Elixir 1.19: Fix typing violations in ActivityPubTest --- test/pleroma/web/activity_pub/activity_pub_test.exs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs index 37ec87a9b..13146619a 100644 --- a/test/pleroma/web/activity_pub/activity_pub_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_test.exs @@ -1524,8 +1524,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do %{test_file: test_file} end - test "strips / from filename", %{test_file: file} do - file = %Plug.Upload{file | filename: "../../../../../nested/bad.jpg"} + test "strips / from filename", %{test_file: %Plug.Upload{} = file} do + file = %{file | filename: "../../../../../nested/bad.jpg"} {:ok, %Object{} = object} = ActivityPub.upload(file) [%{"href" => href}] = object.data["url"] assert Regex.match?(~r"/bad.jpg$", href) @@ -1786,10 +1786,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do {:ok, list} = Pleroma.List.create(%{title: "foo"}, user) {:ok, list} = Pleroma.List.follow(list, member) - {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) + {:ok, %Activity{} = activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) activity = Repo.preload(activity, :bookmark) - activity = %Activity{activity | thread_muted?: !!activity.thread_muted?} + activity = %{activity | thread_muted?: !!activity.thread_muted?} assert ActivityPub.fetch_activities([], %{user: user}) == [activity] end @@ -1989,7 +1989,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do assert User.following?(follower, old_user) assert User.following?(follower_move_opted_out, old_user) - assert {:ok, activity} = ActivityPub.move(old_user, new_user) + assert {:ok, %Activity{} = activity} = ActivityPub.move(old_user, new_user) assert %Activity{ actor: ^old_ap_id, @@ -2021,7 +2021,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do assert User.following?(follower_move_opted_out, old_user) refute User.following?(follower_move_opted_out, new_user) - activity = %Activity{activity | object: nil} + activity = %{activity | object: nil} assert [%Notification{activity: ^activity}] = Notification.for_user(follower) From 349d858fdade27c7a016a55ac69a435dadb59d2c Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 9 Jan 2026 16:42:26 +0100 Subject: [PATCH 049/391] Elixir 1.19: Fix typing violation in MediaControllerTest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit warning: a struct for Plug.Upload is expected on struct update: %Plug.Upload{image | filename: "../../../../../nested/file.jpg"} but got type: dynamic() where "image" was given the type: # type: dynamic() # from: test/pleroma/web/mastodon_api/controllers/media_controller_test.exs:132:42 %{conn: conn, image: image} when defining the variable "image", you must also pattern match on "%Plug.Upload{}". hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of: user = some_function() %User{user | name: "John Doe"} it is enough to write: %User{} = user = some_function() %{user | name: "John Doe"} typing violation found at: │ 133 │ image = %Plug.Upload{ │ ~ │ └─ test/pleroma/web/mastodon_api/controllers/media_controller_test.exs:133:15: Pleroma.Web.MastodonAPI.MediaControllerTest."test Upload media Do not allow nested filename"/1 --- .../web/mastodon_api/controllers/media_controller_test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs index ae86078d7..fc083fd0e 100644 --- a/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs @@ -129,8 +129,8 @@ 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{ + test "Do not allow nested filename", %{conn: conn, image: %Plug.Upload{} = image} do + image = %{ image | filename: "../../../../../nested/file.jpg" } From f3d9880f07e70f4e83e25e13aab537e2ea3c89ba Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 9 Jan 2026 16:44:37 +0100 Subject: [PATCH 050/391] Elixir 1.19: Fix typing violation in RepoTest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit warning: a struct for Pleroma.Web.OAuth.Token is expected on struct update: %Pleroma.Web.OAuth.Token{Pleroma.Factory.insert(:oauth_token) | user: user} but got type: dynamic() you must assign "Pleroma.Factory.insert(:oauth_token)" to variable and pattern match on "%Pleroma.Web.OAuth.Token{}". hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of: user = some_function() %User{user | name: "John Doe"} it is enough to write: %User{} = user = some_function() %{user | name: "John Doe"} typing violation found at: │ 27 │ token = %Pleroma.Web.OAuth.Token{insert(:oauth_token) | user: user} │ ~ │ └─ test/pleroma/repo_test.exs:27:15: Pleroma.RepoTest."test get_assoc/2 get assoc from preloaded data"/1 --- test/pleroma/repo_test.exs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/pleroma/repo_test.exs b/test/pleroma/repo_test.exs index 9c0f5d028..721175bda 100644 --- a/test/pleroma/repo_test.exs +++ b/test/pleroma/repo_test.exs @@ -24,7 +24,8 @@ defmodule Pleroma.RepoTest do describe "get_assoc/2" do test "get assoc from preloaded data" do user = %User{name: "Agent Smith"} - token = %Pleroma.Web.OAuth.Token{insert(:oauth_token) | user: user} + %Pleroma.Web.OAuth.Token{} = token = insert(:oauth_token) + token = %{token | user: user} assert Repo.get_assoc(token, :user) == {:ok, user} end From d1ce6f9833036aacc585963046043f1a5c4f5b14 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 9 Jan 2026 16:48:55 +0100 Subject: [PATCH 051/391] Elixir 1.19: Fix typing violation in MarkerTest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit warning: a struct for Pleroma.Marker is expected on struct update: %Pleroma.Marker{refresh_record(marker) | unread_count: 2} but got type: dynamic() where "marker" was given the type: # type: dynamic() # from: test/pleroma/marker_test.exs:35:14 marker = Pleroma.Factory.insert(:marker, user: user) you must assign "refresh_record(marker)" to variable and pattern match on "%Pleroma.Marker{}". hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of: user = some_function() %User{user | name: "John Doe"} it is enough to write: %User{} = user = some_function() %{user | name: "John Doe"} typing violation found at: │ 43 │ ) == [%Marker{refresh_record(marker) | unread_count: 2}] │ ~ │ └─ test/pleroma/marker_test.exs:43:20: Pleroma.MarkerTest."test get_markers/2 returns user markers"/1 --- test/pleroma/marker_test.exs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/pleroma/marker_test.exs b/test/pleroma/marker_test.exs index 819dde9be..7f573ac7a 100644 --- a/test/pleroma/marker_test.exs +++ b/test/pleroma/marker_test.exs @@ -36,11 +36,12 @@ defmodule Pleroma.MarkerTest do insert(:notification, user: user, activity: insert(:note_activity)) insert(:notification, user: user, activity: insert(:note_activity)) insert(:marker, timeline: "home", user: user) + %Marker{} = refreshed_marker = refresh_record(marker) assert Marker.get_markers( user, ["notifications"] - ) == [%Marker{refresh_record(marker) | unread_count: 2}] + ) == [%{refreshed_marker | unread_count: 2}] end end From 5777d8e8d3c33bc70bb3cda6b7bb72592a310e16 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 9 Jan 2026 17:01:36 +0100 Subject: [PATCH 052/391] Elixir 1.19: Fix deprecation warning when invoking ParallelCompiler warning: you must pass return_diagnostics: true when invoking Kernel.ParallelCompiler functions (elixir 1.19.5) lib/kernel/parallel_compiler.ex:324: Kernel.ParallelCompiler.spawn_workers/3 (pleroma 2.10.0-7-ga7a74d5e-elixir-1-19+test) lib/pleroma/html.ex:13: Pleroma.HTML.compile_scrubbers/0 (pleroma 2.10.0-7-ga7a74d5e-elixir-1-19+test) lib/pleroma/application.ex:48: Pleroma.Application.start/2 (kernel 10.5) application_master.erl:299: :application_master.start_it_old/4 warning: you must pass return_diagnostics: true when invoking Kernel.ParallelCompiler functions (elixir 1.19.5) lib/kernel/parallel_compiler.ex:324: Kernel.ParallelCompiler.spawn_workers/3 (pleroma 2.10.0-7-ga7a74d5e-elixir-1-19+test) lib/pleroma/application.ex:121: Pleroma.Application.load_custom_modules/0 (pleroma 2.10.0-7-ga7a74d5e-elixir-1-19+test) lib/pleroma/application.ex:60: Pleroma.Application.start/2 (kernel 10.5) application_master.erl:299: :application_master.start_it_old/4 --- lib/pleroma/utils.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/utils.ex b/lib/pleroma/utils.ex index 73001c987..61d122a47 100644 --- a/lib/pleroma/utils.ex +++ b/lib/pleroma/utils.ex @@ -17,7 +17,7 @@ defmodule Pleroma.Utils do dir |> File.ls!() |> Enum.map(&Path.join(dir, &1)) - |> Kernel.ParallelCompiler.compile() + |> Kernel.ParallelCompiler.compile(return_diagnostics: true) end @doc """ From 94ea3dffd1c49fadc6a80c34127ea59c7fd87311 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 9 Jan 2026 17:05:31 +0100 Subject: [PATCH 053/391] Elixir 1.19: Fix ConfigDBTest regex tests It is not possible match regexes anymore as this worked by accident previously. Instead, at least check that the sources of the regex (the regex itself) match. Notice the +1 difference in the regex Reference below. 1) test to_elixir_types/1 complex keyword with sigil (Pleroma.ConfigDBTest) test/pleroma/config_db_test.exs:460 Assertion with == failed code: assert ConfigDB.to_elixir_types([ %{"tuple" => [":federated_timeline_removal", []]}, %{"tuple" => [":reject", ["~r/comp[lL][aA][iI][nN]er/"]]}, %{"tuple" => [":replace", []]} ]) == [federated_timeline_removal: [], reject: [~r/comp[lL][aA][iI][nN]er/], replace: []] left: [federated_timeline_removal: [], reject: [%Regex{opts: [], re_pattern: {:re_pattern, 0, 0, 0, #Reference<0.230935836.591265794.259515>}, source: "comp[lL][aA][iI][nN]er"}], replace: []] right: [federated_timeline_removal: [], reject: [%Regex{opts: [], re_pattern: {:re_pattern, 0, 0, 0, #Reference<0.230935836.591265794.259516>}, source: "comp[lL][aA][iI][nN]er"}], replace: []] stacktrace: test/pleroma/config_db_test.exs:461: (test) --- test/pleroma/config_db_test.exs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/pleroma/config_db_test.exs b/test/pleroma/config_db_test.exs index d68e4e6fa..59beba48e 100644 --- a/test/pleroma/config_db_test.exs +++ b/test/pleroma/config_db_test.exs @@ -273,24 +273,24 @@ defmodule Pleroma.ConfigDBTest do end test "sigil" do - assert ConfigDB.to_elixir_types("~r[comp[lL][aA][iI][nN]er]") == ~r/comp[lL][aA][iI][nN]er/ + assert ConfigDB.to_elixir_types("~r[comp[lL][aA][iI][nN]er]").source == ~r/comp[lL][aA][iI][nN]er/.source end test "link sigil" do - assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/") == ~r/https:\/\/example.com/ + assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/").source == ~r/https:\/\/example.com/.source end test "link sigil with um modifiers" do - assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/um") == - ~r/https:\/\/example.com/um + assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/um").source == + ~r/https:\/\/example.com/um.source end test "link sigil with i modifier" do - assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/i") == ~r/https:\/\/example.com/i + assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/i").source == ~r/https:\/\/example.com/i.source end test "link sigil with s modifier" do - assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/s") == ~r/https:\/\/example.com/s + assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/s").source == ~r/https:\/\/example.com/s.source end test "raise if valid delimiter not found" do @@ -460,11 +460,11 @@ defmodule Pleroma.ConfigDBTest do test "complex keyword with sigil" do assert ConfigDB.to_elixir_types([ %{"tuple" => [":federated_timeline_removal", []]}, - %{"tuple" => [":reject", ["~r/comp[lL][aA][iI][nN]er/"]]}, + %{"tuple" => [":reject", [~r/comp[lL][aA][iI][nN]er/.source]]}, %{"tuple" => [":replace", []]} ]) == [ federated_timeline_removal: [], - reject: [~r/comp[lL][aA][iI][nN]er/], + reject: [~r/comp[lL][aA][iI][nN]er/.source], replace: [] ] end From cbacc0f146eaad98eb31574142a076658ae98f3f Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 9 Jan 2026 17:13:10 +0100 Subject: [PATCH 054/391] Elixir 1.19: Fix MRFTest regex tests It is no longer possible to match regexes. Instead at least match that the sources of the regexes (regexes themselves) are the same. Notice the +1 Reference number below. 2) test subdomain_match/2 wildcard domains with one subdomain (Pleroma.Web.ActivityPub.MRFTest) test/pleroma/web/activity_pub/mrf_test.exs:36 Assertion with == failed code: assert regexes == [~r/^(.*\.)*unsafe.tld$/i] left: [%Regex{opts: [:caseless], re_pattern: {:re_pattern, 1, 0, 0, #Reference<0.378940835.3277193222.129648>}, source: "^(.*\\.)*unsafe.tld$"}] right: [%Regex{opts: [:caseless], re_pattern: {:re_pattern, 1, 0, 0, #Reference<0.378940835.3277193222.129649>}, source: "^(.*\\.)*unsafe.tld$"}] stacktrace: test/pleroma/web/activity_pub/mrf_test.exs:39: (test) --- test/pleroma/web/activity_pub/mrf_test.exs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/test/pleroma/web/activity_pub/mrf_test.exs b/test/pleroma/web/activity_pub/mrf_test.exs index 25548e3da..6656a01b8 100644 --- a/test/pleroma/web/activity_pub/mrf_test.exs +++ b/test/pleroma/web/activity_pub/mrf_test.exs @@ -11,17 +11,21 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do alias Pleroma.Web.ActivityPub.MRF test "subdomains_regex/1" do - assert MRF.subdomains_regex(["unsafe.tld", "*.unsafe.tld"]) == [ - ~r/^unsafe.tld$/i, - ~r/^(.*\.)*unsafe.tld$/i + regexes = MRF.subdomains_regex(["unsafe.tld", "*.unsafe.tld"]) + matchable_regexes = Enum.map(regexes, fn r -> r.source end) + + assert matchable_regexes == [ + ~r/^unsafe.tld$/i.source, + ~r/^(.*\.)*unsafe.tld$/i.source ] end describe "subdomain_match/2" do test "common domains" do regexes = MRF.subdomains_regex(["unsafe.tld", "unsafe2.tld"]) + matchable_regexes = Enum.map(regexes, fn r -> r.source end) - assert regexes == [~r/^unsafe.tld$/i, ~r/^unsafe2.tld$/i] + assert matchable_regexes == [~r/^unsafe.tld$/i.source, ~r/^unsafe2.tld$/i.source] assert MRF.subdomain_match?(regexes, "unsafe.tld") assert MRF.subdomain_match?(regexes, "unsafe2.tld") @@ -31,8 +35,9 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do test "wildcard domains with one subdomain" do regexes = MRF.subdomains_regex(["*.unsafe.tld"]) + matchable_regexes = Enum.map(regexes, fn r -> r.source end) - assert regexes == [~r/^(.*\.)*unsafe.tld$/i] + assert matchable_regexes == [~r/^(.*\.)*unsafe.tld$/i.source] assert MRF.subdomain_match?(regexes, "unsafe.tld") assert MRF.subdomain_match?(regexes, "sub.unsafe.tld") @@ -42,8 +47,9 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do test "wildcard domains with two subdomains" do regexes = MRF.subdomains_regex(["*.unsafe.tld"]) + matchable_regexes = Enum.map(regexes, fn r -> r.source end) - assert regexes == [~r/^(.*\.)*unsafe.tld$/i] + assert matchable_regexes == [~r/^(.*\.)*unsafe.tld$/i.source] assert MRF.subdomain_match?(regexes, "unsafe.tld") assert MRF.subdomain_match?(regexes, "sub.sub.unsafe.tld") @@ -53,8 +59,9 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do test "matches are case-insensitive" do regexes = MRF.subdomains_regex(["UnSafe.TLD", "UnSAFE2.Tld"]) + matchable_regexes = Enum.map(regexes, fn r -> r.source end) - assert regexes == [~r/^UnSafe.TLD$/i, ~r/^UnSAFE2.Tld$/i] + assert matchable_regexes == [~r/^UnSafe.TLD$/i.source, ~r/^UnSAFE2.Tld$/i.source] assert MRF.subdomain_match?(regexes, "UNSAFE.TLD") assert MRF.subdomain_match?(regexes, "UNSAFE2.TLD") From 3c7cd8b043237fb0ece9c314b73e335dfd28ffcb Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 9 Jan 2026 17:28:06 +0100 Subject: [PATCH 055/391] Elixir 1.19: Fix Mastodon StatusControllerTest DateTime difference --- .../web/mastodon_api/controllers/status_controller_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs index 298e92366..8477ae03f 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -3068,7 +3068,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do |> json_response_and_validate_schema(:ok) {:ok, a_expires_at, 0} = DateTime.from_iso8601(a_expires_at) - assert DateTime.diff(expires_at, a_expires_at) == 0 + assert DateTime.diff(DateTime.truncate(expires_at, :second), DateTime.truncate(a_expires_at, :second)) == 0 %{conn: conn} = oauth_access(["read:statuses"]) From dd2f0a32d8a0ab918cf840b937158e5b6d9418b3 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 9 Jan 2026 20:16:30 +0100 Subject: [PATCH 056/391] lint --- lib/pleroma/mfa/changeset.ex | 7 ++++--- lib/pleroma/web/api_spec/cast_and_validate.ex | 9 ++++++++- lib/pleroma/web/common_api/activity_draft.ex | 10 +++++++--- test/pleroma/config_db_test.exs | 12 ++++++++---- test/pleroma/web/activity_pub/activity_pub_test.exs | 3 ++- .../controllers/status_controller_test.exs | 6 +++++- 6 files changed, 34 insertions(+), 13 deletions(-) diff --git a/lib/pleroma/mfa/changeset.ex b/lib/pleroma/mfa/changeset.ex index 890cb2193..2045c3a7c 100644 --- a/lib/pleroma/mfa/changeset.ex +++ b/lib/pleroma/mfa/changeset.ex @@ -8,7 +8,8 @@ defmodule Pleroma.MFA.Changeset do alias Pleroma.User def disable(%Ecto.Changeset{} = changeset, force \\ false) do - %Settings{} = settings = + %Settings{} = + settings = changeset |> Ecto.Changeset.apply_changes() |> MFA.fetch_settings() @@ -26,7 +27,7 @@ defmodule Pleroma.MFA.Changeset do end def confirm_totp(%User{multi_factor_authentication_settings: %Settings{} = settings} = user) do - totp_settings = %Settings.TOTP{%Settings.TOTP{} = settings.totp | confirmed: true} + totp_settings = %Settings.TOTP{(%Settings.TOTP{} = settings.totp) | confirmed: true} user |> put_change(%Settings{settings | totp: totp_settings, enabled: true}) @@ -46,7 +47,7 @@ defmodule Pleroma.MFA.Changeset do def cast_backup_codes(%User{} = user, codes) do user |> put_change(%Settings{ - %Settings{} = user.multi_factor_authentication_settings + (%Settings{} = user.multi_factor_authentication_settings) | backup_codes: codes }) end diff --git a/lib/pleroma/web/api_spec/cast_and_validate.ex b/lib/pleroma/web/api_spec/cast_and_validate.ex index 57ea8e9b3..95bd4d9cf 100644 --- a/lib/pleroma/web/api_spec/cast_and_validate.ex +++ b/lib/pleroma/web/api_spec/cast_and_validate.ex @@ -106,7 +106,14 @@ defmodule Pleroma.Web.ApiSpec.CastAndValidate do OpenApiSpex.cast_and_validate(spec, operation, conn, content_type, cast_opts) end - defp cast_and_validate(spec, operation, %Conn{} = conn, content_type, false = _strict, cast_opts) do + defp cast_and_validate( + spec, + operation, + %Conn{} = conn, + content_type, + false = _strict, + cast_opts + ) do case OpenApiSpex.cast_and_validate(spec, operation, conn, content_type) do {:ok, conn} -> {:ok, conn} diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index c5959276a..6c84ef656 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -136,7 +136,8 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp in_reply_to(%__MODULE__{params: %{in_reply_to_status_id: ""}} = draft), do: draft - defp in_reply_to(%__MODULE__{params: %{in_reply_to_status_id: id}} = draft) when is_binary(id) do + defp in_reply_to(%__MODULE__{params: %{in_reply_to_status_id: id}} = draft) + when is_binary(id) do # If a post was deleted all its activities (except the newly added Delete) are purged too, # thus lookup by Create db ID will yield nil just as if it never existed in the first place. # @@ -166,13 +167,16 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do end end - defp in_reply_to(%__MODULE__{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft) do + defp in_reply_to( + %__MODULE__{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft + ) do %__MODULE__{draft | in_reply_to: in_reply_to} end defp in_reply_to(draft), do: draft - defp quote_post(%__MODULE__{params: %{quoted_status_id: id}} = draft) when not_empty_string(id) do + defp quote_post(%__MODULE__{params: %{quoted_status_id: id}} = draft) + when not_empty_string(id) do case Activity.get_by_id_with_object(id) do %Activity{} = activity -> %__MODULE__{draft | quote_post: activity} diff --git a/test/pleroma/config_db_test.exs b/test/pleroma/config_db_test.exs index 59beba48e..98b56a146 100644 --- a/test/pleroma/config_db_test.exs +++ b/test/pleroma/config_db_test.exs @@ -273,11 +273,13 @@ defmodule Pleroma.ConfigDBTest do end test "sigil" do - assert ConfigDB.to_elixir_types("~r[comp[lL][aA][iI][nN]er]").source == ~r/comp[lL][aA][iI][nN]er/.source + assert ConfigDB.to_elixir_types("~r[comp[lL][aA][iI][nN]er]").source == + ~r/comp[lL][aA][iI][nN]er/.source end test "link sigil" do - assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/").source == ~r/https:\/\/example.com/.source + assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/").source == + ~r/https:\/\/example.com/.source end test "link sigil with um modifiers" do @@ -286,11 +288,13 @@ defmodule Pleroma.ConfigDBTest do end test "link sigil with i modifier" do - assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/i").source == ~r/https:\/\/example.com/i.source + assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/i").source == + ~r/https:\/\/example.com/i.source end test "link sigil with s modifier" do - assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/s").source == ~r/https:\/\/example.com/s.source + assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/s").source == + ~r/https:\/\/example.com/s.source end test "raise if valid delimiter not found" do diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs index 13146619a..9aafc41a5 100644 --- a/test/pleroma/web/activity_pub/activity_pub_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_test.exs @@ -1786,7 +1786,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do {:ok, list} = Pleroma.List.create(%{title: "foo"}, user) {:ok, list} = Pleroma.List.follow(list, member) - {:ok, %Activity{} = activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) + {:ok, %Activity{} = activity} = + CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) activity = Repo.preload(activity, :bookmark) activity = %{activity | thread_muted?: !!activity.thread_muted?} diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs index 8477ae03f..11e96a6ac 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -3068,7 +3068,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do |> json_response_and_validate_schema(:ok) {:ok, a_expires_at, 0} = DateTime.from_iso8601(a_expires_at) - assert DateTime.diff(DateTime.truncate(expires_at, :second), DateTime.truncate(a_expires_at, :second)) == 0 + + assert DateTime.diff( + DateTime.truncate(expires_at, :second), + DateTime.truncate(a_expires_at, :second) + ) == 0 %{conn: conn} = oauth_access(["read:statuses"]) From 926cad3a76972835d340a7cabb0e90b8c01f51eb Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 10 Jan 2026 16:42:09 +0100 Subject: [PATCH 057/391] Elixir 1.19: Refactor Logger configuration to what Elixir 1.15+ expects Elixir 1.19 started emitting warnings about our use of: config :logger, :console and config :logger, backends: [] which got split in 1.15 to :default_handler, :default_formatter, and LoggerBackends.add(backend) respectively. On Pleroma startup it is now necessary to add the additional logger backends at runtime (done with configure_logger() in application.ex). New config key :pleroma, :logger_backends: [] has replaced the older backends array. The logger_backends package is now used to facilitate usage of other logger backends besides the default one (:logger_std_h from OTP). Warnings were added to Pleroma startup to warn about the usage of :logger, backends and additional logic was introduced to make addition of ExSyslogger to admin's configuration less dumb. Ref: https://hexdocs.pm/logger/1.15.0/Logger.html#module-backends-and-backwards-compatibility --- config/config.exs | 9 +++++---- config/dev.exs | 2 +- config/prod.exs | 3 +-- config/test.exs | 6 +++--- docs/configuration/cheatsheet.md | 25 +++++++++++++------------ lib/pleroma/application.ex | 32 ++++++++++++++++++++++++++++++++ 6 files changed, 55 insertions(+), 22 deletions(-) diff --git a/config/config.exs b/config/config.exs index 5bf2c5c2e..08311146e 100644 --- a/config/config.exs +++ b/config/config.exs @@ -132,11 +132,12 @@ config :pleroma, Pleroma.Web.Endpoint, "SameSite=Lax" ] -# Configures Elixir's Logger -config :logger, backends: [:console] +# config :logger, :console has been changed in 1.15+ and split into +# :default_handler and :default_formatter. It started emitting warnings in 1.19. +# See https://hexdocs.pm/logger/1.19.0/Logger.html#module-backends-and-backwards-compatibility +config :logger, :default_handler, level: :debug -config :logger, :console, - level: :debug, +config :logger, :default_formatter, format: "\n$time $metadata[$level] $message\n", metadata: [:actor, :path, :type, :user] diff --git a/config/dev.exs b/config/dev.exs index 14cf4a6dc..e86bdb7fd 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -36,7 +36,7 @@ config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Local # different ports. # Do not include timestamps in development logs -config :logger, Logger.Backends.Console, format: "$metadata[$level] $message\n" +config :logger, :default_formatter, format: "$metadata[$level] $message\n" # Set a higher stacktrace during development. Avoid configuring such # in production as building large stacktraces may be expensive. diff --git a/config/prod.exs b/config/prod.exs index 2d252bf02..1dfbbe937 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -20,8 +20,7 @@ config :pleroma, Pleroma.Web.Endpoint, config :phoenix, serve_endpoints: true # Do not print debug messages in production -config :logger, Logger.Backends.Console, level: :info -config :logger, :console, level: :info +config :logger, :default_handler, level: :info config :logger, :ex_syslogger, level: :info # ## SSL Support diff --git a/config/test.exs b/config/test.exs index 9652b7d4b..bac5e4df3 100644 --- a/config/test.exs +++ b/config/test.exs @@ -15,9 +15,9 @@ config :pleroma, Pleroma.Captcha, method: Pleroma.Captcha.Mock # Print only warnings and errors during test -config :logger, :console, - level: :warning, - format: "\n[$level] $message\n" +config :logger, :default_handler, level: :warning + +config :logger, :default_formatter, format: "\n[$level] $message\n" config :pleroma, :auth, oauth_consumer_strategies: [] diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 9efa1c8b3..2ab2652b8 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -858,12 +858,14 @@ Web Push Notifications configuration. You can use the mix task `mix web_push.gen * ``private_key``: VAPID private key ## :logger -* `backends`: `:console` is used to send logs to stdout, `{ExSyslogger, :ex_syslogger}` to log to syslog +* Logging to console/stdout is done by default, use `{ExSyslogger, :ex_syslogger}` to log to syslog An example to enable ONLY ExSyslogger (f/ex in ``prod.secret.exs``) with info and debug suppressed: ```elixir -config :logger, - backends: [{ExSyslogger, :ex_syslogger}] +config :pleroma, + logger_backends: [{ExSyslogger, :ex_syslogger}] + +config :logger, default_handler: false config :logger, :ex_syslogger, level: :warning @@ -871,8 +873,8 @@ config :logger, :ex_syslogger, Another example, keeping console output and adding the pid to syslog output: ```elixir -config :logger, - backends: [:console, {ExSyslogger, :ex_syslogger}] +config :pleroma, + logger_backends: [{ExSyslogger, :ex_syslogger}] config :logger, :ex_syslogger, level: :warning, @@ -883,23 +885,22 @@ See: [logger’s documentation](https://hexdocs.pm/logger/Logger.html) and [ex_s An example of logging info to local syslog, but debug to console: ```elixir -config :logger, - backends: [ {ExSyslogger, :ex_syslogger}, :console ], - level: :info +config :pleroma, + logger_backends: [ {ExSyslogger, :ex_syslogger}] config :logger, :ex_syslogger, level: :info, ident: "pleroma", format: "$metadata[$level] $message" -config :logger, :console, - level: :debug, +config :logger, :default_handler, + level: :debug + +config :logger, :default_formatter, format: "\n$time $metadata[$level] $message\n", metadata: [:request_id] ``` - - ## Database options ### RUM indexing for full text search diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 8e1c5de0d..7355a501a 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -43,6 +43,7 @@ defmodule Pleroma.Application do # every time the application is restarted, so we disable module # conflicts at runtime Code.compiler_options(ignore_module_conflict: true) + configure_logger() Pleroma.Telemetry.Logger.attach() Config.Holder.save_default() Pleroma.HTML.compile_scrubbers() @@ -113,6 +114,37 @@ defmodule Pleroma.Application do Supervisor.start_link(children, opts) end + def configure_logger do + if Application.get_env(:logger, :backends) do + Logger.warning( + "'config :logger, backends: [...]' is deprecated syntax due to changes in Elixir. Use 'config :pleroma, :logger_backends: [...]' instead." + ) + end + + Config.get([:logger_backends], []) + |> Enum.each(fn backend -> + backend = backend_to_logger(backend) + + case LoggerBackends.add(backend) do + {:ok, _} -> + Logger.debug("Successfully added logger backend: #{inspect(backend)}") + + {:error, reason} -> + Logger.error("Failed to add logger backend #{inspect(backend)}: #{inspect(reason)}") + end + end) + end + + defp backend_to_logger({:ex_syslogger = backend, name}) do + Logger.warning( + "Configuration {:#{backend}, :#{name}} is incorrect. Use {ExSyslogger, :#{name}} instead!" + ) + + {ExSyslogger, name} + end + + defp backend_to_logger(backend), do: backend + def load_custom_modules do dir = Config.get([:modules, :runtime_dir]) From 0393f0f92dcd68f7615b98139a35055d272e242a Mon Sep 17 00:00:00 2001 From: Phantasm Date: Mon, 12 Jan 2026 18:03:24 +0100 Subject: [PATCH 058/391] Elixir 1.19: Move Logger backends under a specific config key --- docs/configuration/cheatsheet.md | 12 ++++++------ lib/pleroma/application.ex | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 2ab2652b8..01d81f6ca 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -862,8 +862,8 @@ Web Push Notifications configuration. You can use the mix task `mix web_push.gen An example to enable ONLY ExSyslogger (f/ex in ``prod.secret.exs``) with info and debug suppressed: ```elixir -config :pleroma, - logger_backends: [{ExSyslogger, :ex_syslogger}] +config :pleroma, :logger, + backends: [{ExSyslogger, :ex_syslogger}] config :logger, default_handler: false @@ -873,8 +873,8 @@ config :logger, :ex_syslogger, Another example, keeping console output and adding the pid to syslog output: ```elixir -config :pleroma, - logger_backends: [{ExSyslogger, :ex_syslogger}] +config :pleroma, :logger, + backends: [{ExSyslogger, :ex_syslogger}] config :logger, :ex_syslogger, level: :warning, @@ -885,8 +885,8 @@ See: [logger’s documentation](https://hexdocs.pm/logger/Logger.html) and [ex_s An example of logging info to local syslog, but debug to console: ```elixir -config :pleroma, - logger_backends: [ {ExSyslogger, :ex_syslogger}] +config :pleroma, :logger, + backends: [ {ExSyslogger, :ex_syslogger}] config :logger, :ex_syslogger, level: :info, diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 7355a501a..ce5b4f8d6 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -117,11 +117,11 @@ defmodule Pleroma.Application do def configure_logger do if Application.get_env(:logger, :backends) do Logger.warning( - "'config :logger, backends: [...]' is deprecated syntax due to changes in Elixir. Use 'config :pleroma, :logger_backends: [...]' instead." + "'config :logger, backends: [...]' is deprecated syntax due to changes in Elixir. Use 'config :pleroma, :logger, backends: [...]' instead." ) end - Config.get([:logger_backends], []) + Config.get([:logger, :backends], []) |> Enum.each(fn backend -> backend = backend_to_logger(backend) From 3790e6786cce376cbabf2588beed27d0daf9c55c Mon Sep 17 00:00:00 2001 From: Phantasm Date: Mon, 12 Jan 2026 18:21:46 +0100 Subject: [PATCH 059/391] Only warn about bad clear_config usage if warned usage is met --- test/support/helpers.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/support/helpers.ex b/test/support/helpers.ex index 7fa6c31a4..259180e8f 100644 --- a/test/support/helpers.ex +++ b/test/support/helpers.ex @@ -73,7 +73,8 @@ defmodule Pleroma.Tests.Helpers do defmacro clear_config(config_path, temp_setting) do # NOTE: `clear_config([section, key], value)` != `clear_config([section], key: value)` (!) # Displaying a warning to prevent unintentional clearing of all but one keys in section - if Keyword.keyword?(temp_setting) and length(temp_setting) == 1 do + if is_list(config_path) and length(config_path) == 1 and Keyword.keyword?(temp_setting) and + Keyword.keys(temp_setting) == [:key] do Logger.warning( "Please change `clear_config([section], key: value)` to `clear_config([section, key], value)`" ) From eee54a9226b1b4642bb2a71daa0921ba0022ce48 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Mon, 12 Jan 2026 18:24:11 +0100 Subject: [PATCH 060/391] Elixir 1.19: Add tests for new Logger configuration --- test/pleroma/application_test.exs | 37 +++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 test/pleroma/application_test.exs diff --git a/test/pleroma/application_test.exs b/test/pleroma/application_test.exs new file mode 100644 index 000000000..bff44f4ff --- /dev/null +++ b/test/pleroma/application_test.exs @@ -0,0 +1,37 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2026 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ApplicationTest do + use Pleroma.DataCase + + import ExUnit.CaptureLog + + # clear_config/2 can only manipulate env under :pleroma. + describe "config :logger, backends: []" do + setup do + Application.put_env(:logger, :backends, [:console, {ExSyslogger, :ex_syslogger}]) + + on_exit(fn -> + Application.delete_env(:logger, :backends) end) + end + + test "warns on deprecated syntax" do + assert capture_log(fn -> Pleroma.Application.configure_logger() end) =~ + "'config :logger, backends: [...]' is deprecated syntax due to changes in Elixir. Use 'config :pleroma, :logger, backends: [...]' instead." + end + end + + describe "config :pleroma, :logger, backends: [{:ex_syslogger, :ex_syslogger}]" do + setup do + clear_config([:logger, :backends], [{:ex_syslogger, :ex_syslogger}]) + end + + test "is handled" do + log = capture_log(fn -> Pleroma.Application.configure_logger() end) + + assert log =~ "Configuration {:ex_syslogger, :ex_syslogger} is incorrect. Use {ExSyslogger, :ex_syslogger} instead!" + assert log =~ "Successfully added logger backend: {ExSyslogger, :ex_syslogger}" + end + end +end From 3ebf4b105629b137f620de7b71167463ad3ca6be Mon Sep 17 00:00:00 2001 From: Phantasm Date: Mon, 12 Jan 2026 19:31:15 +0100 Subject: [PATCH 061/391] Elixir 1.19: Move deprecated logger syntax check to DeprecationWarnings --- lib/pleroma/application.ex | 6 ------ lib/pleroma/config/deprecation_warnings.ex | 17 ++++++++++++++++- test/pleroma/application_test.exs | 15 --------------- .../config/deprecation_warnings_test.exs | 15 +++++++++++++++ 4 files changed, 31 insertions(+), 22 deletions(-) diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index ce5b4f8d6..1e80c6249 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -115,12 +115,6 @@ defmodule Pleroma.Application do end def configure_logger do - if Application.get_env(:logger, :backends) do - Logger.warning( - "'config :logger, backends: [...]' is deprecated syntax due to changes in Elixir. Use 'config :pleroma, :logger, backends: [...]' instead." - ) - end - Config.get([:logger, :backends], []) |> Enum.each(fn backend -> backend = backend_to_logger(backend) diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex index 58d164dc7..8dabd728f 100644 --- a/lib/pleroma/config/deprecation_warnings.ex +++ b/lib/pleroma/config/deprecation_warnings.ex @@ -218,7 +218,8 @@ defmodule Pleroma.Config.DeprecationWarnings do check_quarantined_instances_tuples(), check_transparency_exclusions_tuples(), check_simple_policy_tuples(), - check_exiftool_filter() + check_exiftool_filter(), + check_deprecated_logger_config() ] |> Enum.reduce(:ok, fn :ok, :ok -> :ok @@ -415,4 +416,18 @@ defmodule Pleroma.Config.DeprecationWarnings do :ok end end + + @spec check_deprecated_logger_config() :: :ok | :error + def check_deprecated_logger_config() do + if Application.get_env(:logger, :backends) do + Logger.warning( + "'config :logger, backends: [...]' is deprecated syntax due to changes in Elixir. " <> + "Use 'config :pleroma, :logger, backends: [...]' instead." + ) + + :error + else + :ok + end + end end diff --git a/test/pleroma/application_test.exs b/test/pleroma/application_test.exs index bff44f4ff..afe628732 100644 --- a/test/pleroma/application_test.exs +++ b/test/pleroma/application_test.exs @@ -7,21 +7,6 @@ defmodule Pleroma.ApplicationTest do import ExUnit.CaptureLog - # clear_config/2 can only manipulate env under :pleroma. - describe "config :logger, backends: []" do - setup do - Application.put_env(:logger, :backends, [:console, {ExSyslogger, :ex_syslogger}]) - - on_exit(fn -> - Application.delete_env(:logger, :backends) end) - end - - test "warns on deprecated syntax" do - assert capture_log(fn -> Pleroma.Application.configure_logger() end) =~ - "'config :logger, backends: [...]' is deprecated syntax due to changes in Elixir. Use 'config :pleroma, :logger, backends: [...]' instead." - end - end - describe "config :pleroma, :logger, backends: [{:ex_syslogger, :ex_syslogger}]" do setup do clear_config([:logger, :backends], [{:ex_syslogger, :ex_syslogger}]) diff --git a/test/pleroma/config/deprecation_warnings_test.exs b/test/pleroma/config/deprecation_warnings_test.exs index fca2324ff..1491f4ea3 100644 --- a/test/pleroma/config/deprecation_warnings_test.exs +++ b/test/pleroma/config/deprecation_warnings_test.exs @@ -388,4 +388,19 @@ defmodule Pleroma.Config.DeprecationWarningsTest do end) =~ "Your config is using the old namespace for the Shoutbox configuration." end + + describe "check_deprecated_logger_config" do + setup do + Application.put_env(:logger, :backends, [:console, {ExSyslogger, :ex_syslogger}]) + + on_exit(fn -> + Application.delete_env(:logger, :backends) end) + end + + test "warns on deprecated syntax" do + assert capture_log(fn -> Pleroma.Config.DeprecationWarnings.check_deprecated_logger_config() end) =~ + "'config :logger, backends: [...]' is deprecated syntax due to changes in Elixir. Use 'config :pleroma, :logger, backends: [...]' instead." + end + end + end From 39409a784df0a726aee3a6e48769344d83cd81f2 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Mon, 12 Jan 2026 19:35:12 +0100 Subject: [PATCH 062/391] Elixir 1.19: Warn on adding :console to Logger backends Logger.Backend.Console still exists, but it is unused in our configuration. Instead logging to console is done from Logger's "default_handler". --- lib/pleroma/application.ex | 28 +++++++++++++++++++--------- test/pleroma/application_test.exs | 11 +++++++++++ 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 1e80c6249..0e8723251 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -117,27 +117,37 @@ defmodule Pleroma.Application do def configure_logger do Config.get([:logger, :backends], []) |> Enum.each(fn backend -> - backend = backend_to_logger(backend) + case backend_to_logger(backend) do + {:ok, logger} -> + add_logger(logger) - case LoggerBackends.add(backend) do - {:ok, _} -> - Logger.debug("Successfully added logger backend: #{inspect(backend)}") - - {:error, reason} -> - Logger.error("Failed to add logger backend #{inspect(backend)}: #{inspect(reason)}") + {:error, :console_backend} -> + Logger.warning(":console is no longer considered a backend and is enabled by default") end end) end + defp add_logger(backend) do + case LoggerBackends.add(backend) do + {:ok, _} -> + Logger.debug("Successfully added logger backend: #{inspect(backend)}") + + {:error, reason} -> + Logger.error("Failed to add logger backend #{inspect(backend)}: #{inspect(reason)}") + end + end + + defp backend_to_logger(:console), do: {:error, :console_backend} + defp backend_to_logger({:ex_syslogger = backend, name}) do Logger.warning( "Configuration {:#{backend}, :#{name}} is incorrect. Use {ExSyslogger, :#{name}} instead!" ) - {ExSyslogger, name} + {:ok, {ExSyslogger, name}} end - defp backend_to_logger(backend), do: backend + defp backend_to_logger(backend), do: {:ok, backend} def load_custom_modules do dir = Config.get([:modules, :runtime_dir]) diff --git a/test/pleroma/application_test.exs b/test/pleroma/application_test.exs index afe628732..48f87818f 100644 --- a/test/pleroma/application_test.exs +++ b/test/pleroma/application_test.exs @@ -19,4 +19,15 @@ defmodule Pleroma.ApplicationTest do assert log =~ "Successfully added logger backend: {ExSyslogger, :ex_syslogger}" end end + + describe "config :pleroma, :logger, :backends: [:console]" do + setup do + clear_config([:logger, :backends], [:console]) + end + + test "emits a warning" do + assert capture_log(fn -> Pleroma.Application.configure_logger() end) =~ + ":console is no longer considered a backend and is enabled by default" + end + end end From 6744a86fe3a70834aac0c248f5b1c4d0680278fe Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 13 Jan 2026 00:36:15 +0100 Subject: [PATCH 063/391] Elixir 1.19: Disable logging with colors in test environment Can break some tests capture logs and then match on an expected text. Since color codes end up being in the captured log. --- config/test.exs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/config/test.exs b/config/test.exs index bac5e4df3..5b7f3b1ed 100644 --- a/config/test.exs +++ b/config/test.exs @@ -17,7 +17,11 @@ config :pleroma, Pleroma.Captcha, # Print only warnings and errors during test config :logger, :default_handler, level: :warning -config :logger, :default_formatter, format: "\n[$level] $message\n" +config :logger, :default_formatter, + format: "\n[$level] $message\n", + colors: [ + enabled: false + ] config :pleroma, :auth, oauth_consumer_strategies: [] From 17a563aeb41e4f2ec18788cf5a58a1a928044a6e Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 13 Jan 2026 00:39:48 +0100 Subject: [PATCH 064/391] Elixir 1.19: Inject @compile autoload opt to MRF policies Descriptions of MRF policies are gathered at compile-time and use Pleroma.Docs.Generator.list_behaviour_implementations func, which depends on modules implementing the queried behaviour to be loaded. Otherwise descriptions for MRFs can be missing from the AdminAPI route. Note: @behaviour cannot be injected in the __using__ macro. It causes a loop in the compiler which leads to memory exhaustion. Hence why @behaviour is still defined separately in the MRFs. --- .../web/activity_pub/mrf/activity_expiration_policy.ex | 2 ++ lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex | 6 +++--- lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex | 4 +++- .../web/activity_pub/mrf/anti_mention_spam_policy.ex | 4 +++- lib/pleroma/web/activity_pub/mrf/drop_policy.ex | 4 +++- lib/pleroma/web/activity_pub/mrf/emoji_policy.ex | 4 +++- lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex | 6 ++++-- lib/pleroma/web/activity_pub/mrf/follow_bot_policy.ex | 3 +++ .../web/activity_pub/mrf/force_bot_unlisted_policy.ex | 7 +++++-- lib/pleroma/web/activity_pub/mrf/force_mention.ex | 3 ++- .../web/activity_pub/mrf/force_mentions_in_content.ex | 4 +++- lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex | 4 +++- lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex | 7 ++++--- lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex | 2 ++ lib/pleroma/web/activity_pub/mrf/keyword_policy.ex | 6 ++++-- .../web/activity_pub/mrf/media_proxy_warming_policy.ex | 2 ++ lib/pleroma/web/activity_pub/mrf/mention_policy.ex | 3 ++- lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex | 2 ++ lib/pleroma/web/activity_pub/mrf/no_op_policy.ex | 2 ++ .../web/activity_pub/mrf/no_placeholder_text_policy.ex | 2 ++ lib/pleroma/web/activity_pub/mrf/normalize_markup.ex | 4 +++- lib/pleroma/web/activity_pub/mrf/nsfw_api_policy.ex | 5 ++++- lib/pleroma/web/activity_pub/mrf/object_age_policy.ex | 6 ++++-- lib/pleroma/web/activity_pub/mrf/policy.ex | 8 ++++++++ lib/pleroma/web/activity_pub/mrf/quiet_reply.ex | 4 +++- .../web/activity_pub/mrf/quote_to_link_tag_policy.ex | 2 ++ lib/pleroma/web/activity_pub/mrf/reject_non_public.ex | 3 ++- lib/pleroma/web/activity_pub/mrf/remote_report_policy.ex | 2 ++ lib/pleroma/web/activity_pub/mrf/simple_policy.ex | 2 ++ lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex | 6 ++++-- lib/pleroma/web/activity_pub/mrf/subchain_policy.ex | 4 +++- lib/pleroma/web/activity_pub/mrf/tag_policy.ex | 7 +++++-- .../web/activity_pub/mrf/user_allow_list_policy.ex | 6 ++++-- lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex | 3 ++- 34 files changed, 105 insertions(+), 34 deletions(-) diff --git a/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex b/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex index 88f6ca028..41abe17c0 100644 --- a/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex @@ -6,6 +6,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy do @moduledoc "Adds expiration to all local Create activities" @behaviour Pleroma.Web.ActivityPub.MRF.Policy + use Pleroma.Web.ActivityPub.MRF.Policy + @impl true def filter(activity) do activity = diff --git a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex index 8ea61aec2..990ff31c9 100644 --- a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex @@ -3,11 +3,11 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do + @moduledoc "Prevent followbots from following with a bit of heuristic" + @behaviour Pleroma.Web.ActivityPub.MRF.Policy alias Pleroma.User - @moduledoc "Prevent followbots from following with a bit of heuristic" - - @behaviour Pleroma.Web.ActivityPub.MRF.Policy + use Pleroma.Web.ActivityPub.MRF.Policy # XXX: this should become User.normalize_by_ap_id() or similar, really. defp normalize_by_ap_id(%{"id" => id}), do: User.get_cached_by_ap_id(id) diff --git a/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex index 2be6d8df4..5c3b66e37 100644 --- a/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex @@ -3,9 +3,11 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy do + @behaviour Pleroma.Web.ActivityPub.MRF.Policy + alias Pleroma.User - @behaviour Pleroma.Web.ActivityPub.MRF.Policy + use Pleroma.Web.ActivityPub.MRF.Policy require Logger diff --git a/lib/pleroma/web/activity_pub/mrf/anti_mention_spam_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_mention_spam_policy.ex index 1d76a307b..c6f5781b3 100644 --- a/lib/pleroma/web/activity_pub/mrf/anti_mention_spam_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/anti_mention_spam_policy.ex @@ -3,11 +3,13 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.AntiMentionSpamPolicy do + @behaviour Pleroma.Web.ActivityPub.MRF.Policy + alias Pleroma.Config alias Pleroma.User require Pleroma.Constants - @behaviour Pleroma.Web.ActivityPub.MRF.Policy + use Pleroma.Web.ActivityPub.MRF.Policy defp user_has_posted?(%User{} = u), do: u.note_count > 0 diff --git a/lib/pleroma/web/activity_pub/mrf/drop_policy.ex b/lib/pleroma/web/activity_pub/mrf/drop_policy.ex index cf07db7f3..4aa1968f7 100644 --- a/lib/pleroma/web/activity_pub/mrf/drop_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/drop_policy.ex @@ -3,10 +3,12 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.DropPolicy do - require Logger @moduledoc "Drop and log everything received" @behaviour Pleroma.Web.ActivityPub.MRF.Policy + require Logger + use Pleroma.Web.ActivityPub.MRF.Policy + @impl true def filter(activity) do Logger.debug("REJECTING #{inspect(activity)}") diff --git a/lib/pleroma/web/activity_pub/mrf/emoji_policy.ex b/lib/pleroma/web/activity_pub/mrf/emoji_policy.ex index 1de5280d9..cc5fd49bf 100644 --- a/lib/pleroma/web/activity_pub/mrf/emoji_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/emoji_policy.ex @@ -3,6 +3,8 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicy do + @behaviour Pleroma.Web.ActivityPub.MRF.Policy + require Pleroma.Constants alias Pleroma.Object.Updater @@ -10,7 +12,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicy do @moduledoc "Reject or force-unlisted emojis with certain URLs or names" - @behaviour Pleroma.Web.ActivityPub.MRF.Policy + use Pleroma.Web.ActivityPub.MRF.Policy defp config_remove_url do Pleroma.Config.get([:mrf_emoji, :remove_url], []) diff --git a/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex index f5983c8a7..6e43e746c 100644 --- a/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex +++ b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex @@ -3,11 +3,13 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do - alias Pleroma.Object - @moduledoc "Ensure a re: is prepended on replies to a post with a Subject" @behaviour Pleroma.Web.ActivityPub.MRF.Policy + alias Pleroma.Object + + use Pleroma.Web.ActivityPub.MRF.Policy + @reply_prefix Regex.compile!("^re:[[:space:]]*", [:caseless]) def history_awareness, do: :auto diff --git a/lib/pleroma/web/activity_pub/mrf/follow_bot_policy.ex b/lib/pleroma/web/activity_pub/mrf/follow_bot_policy.ex index 480a03ef6..4612b2cc5 100644 --- a/lib/pleroma/web/activity_pub/mrf/follow_bot_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/follow_bot_policy.ex @@ -4,12 +4,15 @@ defmodule Pleroma.Web.ActivityPub.MRF.FollowBotPolicy do @behaviour Pleroma.Web.ActivityPub.MRF.Policy + alias Pleroma.Config alias Pleroma.User alias Pleroma.Web.CommonAPI require Logger + use Pleroma.Web.ActivityPub.MRF.Policy + @impl true def filter(activity) do with follower_nickname <- Config.get([:mrf_follow_bot, :follower_nickname]), diff --git a/lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex b/lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex index 3b3251dc3..58d02f433 100644 --- a/lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex @@ -3,9 +3,12 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy do - alias Pleroma.User - @behaviour Pleroma.Web.ActivityPub.MRF.Policy @moduledoc "Remove bot posts from federated timeline" + @behaviour Pleroma.Web.ActivityPub.MRF.Policy + + alias Pleroma.User + + use Pleroma.Web.ActivityPub.MRF.Policy require Pleroma.Constants diff --git a/lib/pleroma/web/activity_pub/mrf/force_mention.ex b/lib/pleroma/web/activity_pub/mrf/force_mention.ex index 4ea23540d..62aff36bc 100644 --- a/lib/pleroma/web/activity_pub/mrf/force_mention.ex +++ b/lib/pleroma/web/activity_pub/mrf/force_mention.ex @@ -3,13 +3,14 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.ForceMention do + @behaviour Pleroma.Web.ActivityPub.MRF.Policy require Pleroma.Constants alias Pleroma.Config alias Pleroma.Object alias Pleroma.User - @behaviour Pleroma.Web.ActivityPub.MRF.Policy + use Pleroma.Web.ActivityPub.MRF.Policy defp get_author(url) do with %Object{data: %{"actor" => actor}} <- Object.normalize(url, fetch: false), diff --git a/lib/pleroma/web/activity_pub/mrf/force_mentions_in_content.ex b/lib/pleroma/web/activity_pub/mrf/force_mentions_in_content.ex index caae365e5..e7c3f2d88 100644 --- a/lib/pleroma/web/activity_pub/mrf/force_mentions_in_content.ex +++ b/lib/pleroma/web/activity_pub/mrf/force_mentions_in_content.ex @@ -3,13 +3,15 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent do + @behaviour Pleroma.Web.ActivityPub.MRF.Policy + require Pleroma.Constants alias Pleroma.Formatter alias Pleroma.Object alias Pleroma.User - @behaviour Pleroma.Web.ActivityPub.MRF.Policy + use Pleroma.Web.ActivityPub.MRF.Policy @impl true def history_awareness, do: :auto diff --git a/lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex b/lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex index 72f2274ed..a3fae3bae 100644 --- a/lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex @@ -3,6 +3,8 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicy do + @behaviour Pleroma.Web.ActivityPub.MRF.Policy + require Pleroma.Constants alias Pleroma.Config @@ -14,7 +16,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicy do Note: This MRF Policy is always enabled, if you want to disable it you have to set empty lists. """ - @behaviour Pleroma.Web.ActivityPub.MRF.Policy + use Pleroma.Web.ActivityPub.MRF.Policy @impl true def history_awareness, do: :manual diff --git a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex index 3a80d0a69..a84a7b0c8 100644 --- a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex @@ -3,13 +3,14 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do + @moduledoc "Block activities with too much mentions (configurable)" + @behaviour Pleroma.Web.ActivityPub.MRF.Policy + alias Pleroma.User require Pleroma.Constants - @moduledoc "Block activities with too much mentions (configurable)" - - @behaviour Pleroma.Web.ActivityPub.MRF.Policy + use Pleroma.Web.ActivityPub.MRF.Policy defp delist_activity(activity, threshold) when threshold > 0 do follower_collection = User.get_cached_by_ap_id(activity["actor"]).follower_address diff --git a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex index 51dcd3918..17879b4bd 100644 --- a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex @@ -6,6 +6,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do @moduledoc "Force a quote line into the message content." @behaviour Pleroma.Web.ActivityPub.MRF.Policy + use Pleroma.Web.ActivityPub.MRF.Policy + defp build_inline_quote(template, url) do quote_line = String.replace(template, "{url}", "#{url}") diff --git a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex index 6ba6fd509..44b79596e 100644 --- a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex @@ -3,13 +3,15 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do + @moduledoc "Reject or Word-Replace activities with a keyword or regex" + @behaviour Pleroma.Web.ActivityPub.MRF.Policy + require Pleroma.Constants alias Pleroma.Web.ActivityPub.MRF.Utils - @moduledoc "Reject or Word-Replace activities with a keyword or regex" - @behaviour Pleroma.Web.ActivityPub.MRF.Policy + use Pleroma.Web.ActivityPub.MRF.Policy defp string_matches?(string, pattern) when is_binary(pattern) do String.contains?(string, pattern) diff --git a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex index 43c2c0449..07046a666 100644 --- a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex @@ -6,6 +6,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do @moduledoc "Preloads any attachments in the MediaProxy cache by prefetching them" @behaviour Pleroma.Web.ActivityPub.MRF.Policy + use Pleroma.Web.ActivityPub.MRF.Policy + alias Pleroma.HTTP alias Pleroma.Web.MediaProxy diff --git a/lib/pleroma/web/activity_pub/mrf/mention_policy.ex b/lib/pleroma/web/activity_pub/mrf/mention_policy.ex index f7bff121f..8e108a0ae 100644 --- a/lib/pleroma/web/activity_pub/mrf/mention_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/mention_policy.ex @@ -4,9 +4,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicy do @moduledoc "Block activities which mention a user" - @behaviour Pleroma.Web.ActivityPub.MRF.Policy + use Pleroma.Web.ActivityPub.MRF.Policy + @impl true def filter(%{"type" => "Create"} = activity) do reject_actors = Pleroma.Config.get([:mrf_mention, :actors], []) diff --git a/lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex b/lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex index 08dd39878..8283fd504 100644 --- a/lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex @@ -6,6 +6,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do @moduledoc "Filter local activities which have no content" @behaviour Pleroma.Web.ActivityPub.MRF.Policy + use Pleroma.Web.ActivityPub.MRF.Policy + alias Pleroma.Web.Endpoint @impl true diff --git a/lib/pleroma/web/activity_pub/mrf/no_op_policy.ex b/lib/pleroma/web/activity_pub/mrf/no_op_policy.ex index 64a5872bc..3adbb598a 100644 --- a/lib/pleroma/web/activity_pub/mrf/no_op_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/no_op_policy.ex @@ -6,6 +6,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoOpPolicy do @moduledoc "Does nothing (lets the messages go through unmodified)" @behaviour Pleroma.Web.ActivityPub.MRF.Policy + use Pleroma.Web.ActivityPub.MRF.Policy + @impl true def filter(activity) do {:ok, activity} diff --git a/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex b/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex index c6f239a5e..2a10c7e97 100644 --- a/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex @@ -6,6 +6,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy do @moduledoc "Ensure no content placeholder is present (such as the dot from mastodon)" @behaviour Pleroma.Web.ActivityPub.MRF.Policy + use Pleroma.Web.ActivityPub.MRF.Policy + @impl true def history_awareness, do: :auto diff --git a/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex b/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex index 91855ef84..de0f5b619 100644 --- a/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex +++ b/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex @@ -4,9 +4,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkup do @moduledoc "Scrub configured hypertext markup" + @behaviour Pleroma.Web.ActivityPub.MRF.Policy + alias Pleroma.HTML - @behaviour Pleroma.Web.ActivityPub.MRF.Policy + use Pleroma.Web.ActivityPub.MRF.Policy @impl true def history_awareness, do: :auto diff --git a/lib/pleroma/web/activity_pub/mrf/nsfw_api_policy.ex b/lib/pleroma/web/activity_pub/mrf/nsfw_api_policy.ex index 52aaf05aa..7e3ec3de5 100644 --- a/lib/pleroma/web/activity_pub/mrf/nsfw_api_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/nsfw_api_policy.ex @@ -38,6 +38,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.NsfwApiPolicy do - `unlist`: Unlist all detected NSFW content? Default: `false` - `reject`: Reject all detected NSFW content (takes precedence)? Default: `false` """ + @behaviour Pleroma.Web.ActivityPub.MRF.Policy + alias Pleroma.Config alias Pleroma.Constants alias Pleroma.HTTP @@ -46,7 +48,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.NsfwApiPolicy do require Logger require Pleroma.Constants - @behaviour Pleroma.Web.ActivityPub.MRF.Policy + use Pleroma.Web.ActivityPub.MRF.Policy + @policy :mrf_nsfw_api def build_request_url(url) do diff --git a/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex index 34905fc21..00f4cf84b 100644 --- a/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex @@ -3,13 +3,15 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do + @moduledoc "Filter activities depending on their age" + @behaviour Pleroma.Web.ActivityPub.MRF.Policy + alias Pleroma.Config alias Pleroma.User require Pleroma.Constants - @moduledoc "Filter activities depending on their age" - @behaviour Pleroma.Web.ActivityPub.MRF.Policy + use Pleroma.Web.ActivityPub.MRF.Policy defp check_date(%{"object" => %{"published" => published}} = activity) do with %DateTime{} = now <- DateTime.utc_now(), diff --git a/lib/pleroma/web/activity_pub/mrf/policy.ex b/lib/pleroma/web/activity_pub/mrf/policy.ex index 08bcac08a..b9a693c9e 100644 --- a/lib/pleroma/web/activity_pub/mrf/policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/policy.ex @@ -15,4 +15,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.Policy do } @callback history_awareness() :: :auto | :manual @optional_callbacks config_description: 0, history_awareness: 0, id_filter: 1 + + defmacro __using__(_) do + quote do + # Otherwise Pleroma.Docs.Generator.list_behaviour_implementations fails + # since the Elixir compiler doesn't automatically load the module since v1.19. + @compile {:autoload, true} + end + end end diff --git a/lib/pleroma/web/activity_pub/mrf/quiet_reply.ex b/lib/pleroma/web/activity_pub/mrf/quiet_reply.ex index b3eb0b390..b47e9edb9 100644 --- a/lib/pleroma/web/activity_pub/mrf/quiet_reply.ex +++ b/lib/pleroma/web/activity_pub/mrf/quiet_reply.ex @@ -6,11 +6,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.QuietReply do @moduledoc """ QuietReply alters the scope of activities from local users when replying by enforcing them to be "Unlisted" or "Quiet Public". This delivers the activity to all the expected recipients and instances, but it will not be published in the Federated / The Whole Known Network timelines. It will still be published to the Home timelines of the user's followers and visible to anyone who opens the thread. """ + @behaviour Pleroma.Web.ActivityPub.MRF.Policy + require Pleroma.Constants alias Pleroma.User - @behaviour Pleroma.Web.ActivityPub.MRF.Policy + use Pleroma.Web.ActivityPub.MRF.Policy @impl true def history_awareness, do: :auto diff --git a/lib/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy.ex b/lib/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy.ex index 2a17b6761..e1ad17585 100644 --- a/lib/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy.ex @@ -6,6 +6,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicy do @moduledoc "Force a Link tag for posts quoting another post. (may break outgoing federation of quote posts with older Pleroma versions)" @behaviour Pleroma.Web.ActivityPub.MRF.Policy + use Pleroma.Web.ActivityPub.MRF.Policy + alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes require Pleroma.Constants diff --git a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex index 9d4a7a405..7d113dbf6 100644 --- a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex +++ b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex @@ -4,11 +4,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do @moduledoc "Rejects non-public (followers-only, direct) activities" + @behaviour Pleroma.Web.ActivityPub.MRF.Policy alias Pleroma.Config alias Pleroma.User - @behaviour Pleroma.Web.ActivityPub.MRF.Policy + use Pleroma.Web.ActivityPub.MRF.Policy require Pleroma.Constants diff --git a/lib/pleroma/web/activity_pub/mrf/remote_report_policy.ex b/lib/pleroma/web/activity_pub/mrf/remote_report_policy.ex index 8422832dd..f9f8d2298 100644 --- a/lib/pleroma/web/activity_pub/mrf/remote_report_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/remote_report_policy.ex @@ -2,6 +2,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.RemoteReportPolicy do @moduledoc "Drop remote reports if they don't contain enough information." @behaviour Pleroma.Web.ActivityPub.MRF.Policy + use Pleroma.Web.ActivityPub.MRF.Policy + alias Pleroma.Config @impl true diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex index a97e8db7b..7045fe90f 100644 --- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex @@ -6,6 +6,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do @moduledoc "Filter activities depending on their origin instance" @behaviour Pleroma.Web.ActivityPub.MRF.Policy + use Pleroma.Web.ActivityPub.MRF.Policy + alias Pleroma.Config alias Pleroma.FollowingRelationship alias Pleroma.User diff --git a/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex index 54f0e6bc1..f1e99aead 100644 --- a/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex @@ -3,12 +3,14 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do + @moduledoc "Detect new emojis by their shortcode and steals them" + @behaviour Pleroma.Web.ActivityPub.MRF.Policy + require Logger alias Pleroma.Config - @moduledoc "Detect new emojis by their shortcode and steals them" - @behaviour Pleroma.Web.ActivityPub.MRF.Policy + use Pleroma.Web.ActivityPub.MRF.Policy defp accept_host?(host), do: host in Config.get([:mrf_steal_emoji, :hosts], []) diff --git a/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex b/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex index 97acca7e8..1fe7b00e9 100644 --- a/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex @@ -3,12 +3,14 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.SubchainPolicy do + @behaviour Pleroma.Web.ActivityPub.MRF.Policy + alias Pleroma.Config alias Pleroma.Web.ActivityPub.MRF require Logger - @behaviour Pleroma.Web.ActivityPub.MRF.Policy + use Pleroma.Web.ActivityPub.MRF.Policy defp lookup_subchain(actor) do with matches <- Config.get([:mrf_subchain, :match_actor]), diff --git a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex index c236a5a99..df5594808 100644 --- a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex @@ -3,8 +3,6 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do - alias Pleroma.User - @behaviour Pleroma.Web.ActivityPub.MRF.Policy @moduledoc """ Apply policies based on user tags @@ -18,6 +16,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do - `mrf_tag:disable-remote-subscription`: Reject non-local follow requests - `mrf_tag:disable-any-subscription`: Reject any follow requests """ + @behaviour Pleroma.Web.ActivityPub.MRF.Policy + + alias Pleroma.User + + use Pleroma.Web.ActivityPub.MRF.Policy require Pleroma.Constants diff --git a/lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex b/lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex index 10cc0e09d..a04ca90bd 100644 --- a/lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex @@ -3,11 +3,13 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy do - alias Pleroma.Config - @moduledoc "Accept-list of users from specified instances" @behaviour Pleroma.Web.ActivityPub.MRF.Policy + alias Pleroma.Config + + use Pleroma.Web.ActivityPub.MRF.Policy + defp filter_by_list(activity, []), do: {:ok, activity} defp filter_by_list(%{"actor" => actor} = activity, allow_list) do diff --git a/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex b/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex index 5671e4cf3..ecba3a2c6 100644 --- a/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex @@ -4,9 +4,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicy do @moduledoc "Filter activities which belong to certain activity vocabularies" - @behaviour Pleroma.Web.ActivityPub.MRF.Policy + use Pleroma.Web.ActivityPub.MRF.Policy + @impl true def filter(%{"type" => "Undo", "object" => object} = activity) do with {:ok, _} <- filter(object) do From 2a23eacee899c69f55d8a3cb6ed8291b56c7187b Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 13 Jan 2026 14:58:32 +0100 Subject: [PATCH 065/391] Elixir 1.19: Only match once on structs Second match is not needed and a simple Map update is recommended by the compiler --- lib/pleroma/marker.ex | 2 +- lib/pleroma/web/api_spec/render_error.ex | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/marker.ex b/lib/pleroma/marker.ex index 4f645240e..fab24d183 100644 --- a/lib/pleroma/marker.ex +++ b/lib/pleroma/marker.ex @@ -78,7 +78,7 @@ defmodule Pleroma.Marker do defp get_marker(user, timeline) do case Repo.find_resource(get_query(user, timeline)) do - {:ok, %__MODULE__{} = marker} -> %__MODULE__{marker | user: user} + {:ok, %__MODULE__{} = marker} -> %{marker | user: user} _ -> %__MODULE__{timeline: timeline, user_id: user.id} end end diff --git a/lib/pleroma/web/api_spec/render_error.ex b/lib/pleroma/web/api_spec/render_error.ex index acf510774..2ba76f250 100644 --- a/lib/pleroma/web/api_spec/render_error.ex +++ b/lib/pleroma/web/api_spec/render_error.ex @@ -18,10 +18,10 @@ defmodule Pleroma.Web.ApiSpec.RenderError do errors = Enum.map(errors, fn %OpenApiSpex.Cast.Error{name: nil, reason: :invalid_enum} = err -> - %OpenApiSpex.Cast.Error{err | name: err.value} + %{err | name: err.value} %OpenApiSpex.Cast.Error{name: nil} = err -> - %OpenApiSpex.Cast.Error{err | name: List.last(err.path)} + %{err | name: List.last(err.path)} err -> err From c455e0225581e3ebe43e9ef51a7ee393d8e87bdd Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 13 Jan 2026 15:21:41 +0100 Subject: [PATCH 066/391] Elixir 1.19: Add non-test Elixir files in test/ to mix.exs filter This is a new filter introduced in 1.19. --- mix.exs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mix.exs b/mix.exs index dc9fa31f5..60064e846 100644 --- a/mix.exs +++ b/mix.exs @@ -15,6 +15,13 @@ defmodule Pleroma.Mixfile do aliases: aliases(), deps: deps(), test_coverage: [tool: :covertool, summary: true], + test_ignore_filters: [ + "test/credo/check/consistency/file_location.ex", + "test/fixtures/config/temp.exported_from_db.secret.exs", + "test/fixtures/config/temp.secret.exs", + "test/fixtures/modules/good_mrf.ex", + "test/fixtures/modules/runtime_module.ex" + ], # Docs name: "Pleroma", homepage_url: "https://pleroma.social/", From a4c929c527353f99cd11aeb9c08a44f212632f12 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 13 Jan 2026 20:44:26 +0100 Subject: [PATCH 067/391] Lint --- lib/pleroma/config/deprecation_warnings.ex | 4 ++-- lib/pleroma/web/activity_pub/mrf/keyword_policy.ex | 1 - test/pleroma/application_test.exs | 6 ++++-- test/pleroma/config/deprecation_warnings_test.exs | 10 ++++++---- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex index 8dabd728f..8f56a3771 100644 --- a/lib/pleroma/config/deprecation_warnings.ex +++ b/lib/pleroma/config/deprecation_warnings.ex @@ -418,11 +418,11 @@ defmodule Pleroma.Config.DeprecationWarnings do end @spec check_deprecated_logger_config() :: :ok | :error - def check_deprecated_logger_config() do + def check_deprecated_logger_config do if Application.get_env(:logger, :backends) do Logger.warning( "'config :logger, backends: [...]' is deprecated syntax due to changes in Elixir. " <> - "Use 'config :pleroma, :logger, backends: [...]' instead." + "Use 'config :pleroma, :logger, backends: [...]' instead." ) :error diff --git a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex index 44b79596e..33f32e781 100644 --- a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex @@ -10,7 +10,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do alias Pleroma.Web.ActivityPub.MRF.Utils - use Pleroma.Web.ActivityPub.MRF.Policy defp string_matches?(string, pattern) when is_binary(pattern) do diff --git a/test/pleroma/application_test.exs b/test/pleroma/application_test.exs index 48f87818f..b4a29fc9b 100644 --- a/test/pleroma/application_test.exs +++ b/test/pleroma/application_test.exs @@ -15,7 +15,9 @@ defmodule Pleroma.ApplicationTest do test "is handled" do log = capture_log(fn -> Pleroma.Application.configure_logger() end) - assert log =~ "Configuration {:ex_syslogger, :ex_syslogger} is incorrect. Use {ExSyslogger, :ex_syslogger} instead!" + assert log =~ + "Configuration {:ex_syslogger, :ex_syslogger} is incorrect. Use {ExSyslogger, :ex_syslogger} instead!" + assert log =~ "Successfully added logger backend: {ExSyslogger, :ex_syslogger}" end end @@ -27,7 +29,7 @@ defmodule Pleroma.ApplicationTest do test "emits a warning" do assert capture_log(fn -> Pleroma.Application.configure_logger() end) =~ - ":console is no longer considered a backend and is enabled by default" + ":console is no longer considered a backend and is enabled by default" end end end diff --git a/test/pleroma/config/deprecation_warnings_test.exs b/test/pleroma/config/deprecation_warnings_test.exs index 1491f4ea3..5383e2bce 100644 --- a/test/pleroma/config/deprecation_warnings_test.exs +++ b/test/pleroma/config/deprecation_warnings_test.exs @@ -394,13 +394,15 @@ defmodule Pleroma.Config.DeprecationWarningsTest do Application.put_env(:logger, :backends, [:console, {ExSyslogger, :ex_syslogger}]) on_exit(fn -> - Application.delete_env(:logger, :backends) end) + Application.delete_env(:logger, :backends) + end) end test "warns on deprecated syntax" do - assert capture_log(fn -> Pleroma.Config.DeprecationWarnings.check_deprecated_logger_config() end) =~ - "'config :logger, backends: [...]' is deprecated syntax due to changes in Elixir. Use 'config :pleroma, :logger, backends: [...]' instead." + assert capture_log(fn -> + Pleroma.Config.DeprecationWarnings.check_deprecated_logger_config() + end) =~ + "'config :logger, backends: [...]' is deprecated syntax due to changes in Elixir. Use 'config :pleroma, :logger, backends: [...]' instead." end end - end From a4137243b62795b6299248ccf11f80ccda059d56 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 13 Jan 2026 21:07:06 +0100 Subject: [PATCH 068/391] Elixir 1.19: Reword Logger backends deprecation warning --- lib/pleroma/config/deprecation_warnings.ex | 15 +++++++++++---- test/pleroma/config/deprecation_warnings_test.exs | 12 +++++++++++- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex index 8f56a3771..8b8628862 100644 --- a/lib/pleroma/config/deprecation_warnings.ex +++ b/lib/pleroma/config/deprecation_warnings.ex @@ -420,10 +420,17 @@ defmodule Pleroma.Config.DeprecationWarnings do @spec check_deprecated_logger_config() :: :ok | :error def check_deprecated_logger_config do if Application.get_env(:logger, :backends) do - Logger.warning( - "'config :logger, backends: [...]' is deprecated syntax due to changes in Elixir. " <> - "Use 'config :pleroma, :logger, backends: [...]' instead." - ) + Logger.warning(""" + !!!DEPRECATION WARNING!!! + Your configuration is using deprecated syntax for configuring backends of Elixir's logger. + `config :logger, backends: [...]` is deprecated syntax due to changes in Elixir. + Please update your configuration at your earliest convenience to use: + `config :pleroma, :logger, backends: [...]` + Pleroma's default configuration will be updated in the next release which WILL override your settings. + + Note: :console is no longer considered a backend and is used by default, you can disable it using: + `config :logger, :default_handler: false` + """) :error else diff --git a/test/pleroma/config/deprecation_warnings_test.exs b/test/pleroma/config/deprecation_warnings_test.exs index 5383e2bce..a9a71be36 100644 --- a/test/pleroma/config/deprecation_warnings_test.exs +++ b/test/pleroma/config/deprecation_warnings_test.exs @@ -402,7 +402,17 @@ defmodule Pleroma.Config.DeprecationWarningsTest do assert capture_log(fn -> Pleroma.Config.DeprecationWarnings.check_deprecated_logger_config() end) =~ - "'config :logger, backends: [...]' is deprecated syntax due to changes in Elixir. Use 'config :pleroma, :logger, backends: [...]' instead." + """ + !!!DEPRECATION WARNING!!! + Your configuration is using deprecated syntax for configuring backends of Elixir's logger. + `config :logger, backends: [...]` is deprecated syntax due to changes in Elixir. + Please update your configuration at your earliest convenience to use: + `config :pleroma, :logger, backends: [...]` + Pleroma's default configuration will be updated in the next release which WILL override your settings. + + Note: :console is no longer considered a backend and is used by default, you can disable it using: + `config :logger, :default_handler: false` + """ end end end From 6ffd25682d4a0e6cebe9b0c8789c76bb9e8a312d Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 13 Jan 2026 21:18:08 +0100 Subject: [PATCH 069/391] Elixir 1.19: Add deprecation warning for :logger, :console configs --- lib/pleroma/config/deprecation_warnings.ex | 30 +++++++++++--- .../config/deprecation_warnings_test.exs | 41 +++++++++++++------ 2 files changed, 54 insertions(+), 17 deletions(-) diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex index 8b8628862..f38fb7f2f 100644 --- a/lib/pleroma/config/deprecation_warnings.ex +++ b/lib/pleroma/config/deprecation_warnings.ex @@ -419,22 +419,42 @@ defmodule Pleroma.Config.DeprecationWarnings do @spec check_deprecated_logger_config() :: :ok | :error def check_deprecated_logger_config do - if Application.get_env(:logger, :backends) do + backend = if Application.get_env(:logger, :backends) do Logger.warning(""" !!!DEPRECATION WARNING!!! Your configuration is using deprecated syntax for configuring backends of Elixir's logger. `config :logger, backends: [...]` is deprecated syntax due to changes in Elixir. Please update your configuration at your earliest convenience to use: - `config :pleroma, :logger, backends: [...]` + `config :logger, backends: [...]` Pleroma's default configuration will be updated in the next release which WILL override your settings. - Note: :console is no longer considered a backend and is used by default, you can disable it using: + Note: `:console` is no longer considered a backend and is used by default, you can disable it using: `config :logger, :default_handler: false` """) - :error + true else - :ok + false end + + console = if Application.get_env(:logger, :console) do + Logger.warning(""" + !!!DEPRECATION WARNING!!! + Your configuration is using deprecated syntax for configuring logging to console. + `config :logger, :console` is deprecated syntax due to changes in Elixir. + Please update your configuration at your earliest convenience to use + `config :logger, default_handler` and `config :logger, :default_formatter`. + Pleroma's default configuration will be updated in the next release which WILL override your settings. + + Note: `:default_handler` is used only for the `level` setting. All other configurations go under + `:default_formatter`. For more info visit: https://hexdocs.pm/logger/Logger.html#module-backends-and-backwards-compatibility + """) + + true + else + false + end + + if backend or console, do: :error, else: :ok end end diff --git a/test/pleroma/config/deprecation_warnings_test.exs b/test/pleroma/config/deprecation_warnings_test.exs index a9a71be36..f5b9d76d3 100644 --- a/test/pleroma/config/deprecation_warnings_test.exs +++ b/test/pleroma/config/deprecation_warnings_test.exs @@ -392,27 +392,44 @@ defmodule Pleroma.Config.DeprecationWarningsTest do describe "check_deprecated_logger_config" do setup do Application.put_env(:logger, :backends, [:console, {ExSyslogger, :ex_syslogger}]) + Application.put_env(:logger, :console, level: :warning) on_exit(fn -> Application.delete_env(:logger, :backends) + Application.delete_env(:logger, :console) end) end test "warns on deprecated syntax" do - assert capture_log(fn -> + log = capture_log(fn -> Pleroma.Config.DeprecationWarnings.check_deprecated_logger_config() - end) =~ - """ - !!!DEPRECATION WARNING!!! - Your configuration is using deprecated syntax for configuring backends of Elixir's logger. - `config :logger, backends: [...]` is deprecated syntax due to changes in Elixir. - Please update your configuration at your earliest convenience to use: - `config :pleroma, :logger, backends: [...]` - Pleroma's default configuration will be updated in the next release which WILL override your settings. + end) - Note: :console is no longer considered a backend and is used by default, you can disable it using: - `config :logger, :default_handler: false` - """ + assert log =~ + """ + !!!DEPRECATION WARNING!!! + Your configuration is using deprecated syntax for configuring backends of Elixir's logger. + `config :logger, backends: [...]` is deprecated syntax due to changes in Elixir. + Please update your configuration at your earliest convenience to use: + `config :logger, backends: [...]` + Pleroma's default configuration will be updated in the next release which WILL override your settings. + + Note: `:console` is no longer considered a backend and is used by default, you can disable it using: + `config :logger, :default_handler: false` + """ + + assert log =~ + """ + !!!DEPRECATION WARNING!!! + Your configuration is using deprecated syntax for configuring logging to console. + `config :logger, :console` is deprecated syntax due to changes in Elixir. + Please update your configuration at your earliest convenience to use + `config :logger, default_handler` and `config :logger, :default_formatter`. + Pleroma's default configuration will be updated in the next release which WILL override your settings. + + Note: `:default_handler` is used only for the `level` setting. All other configurations go under + `:default_formatter`. For more info visit: https://hexdocs.pm/logger/Logger.html#module-backends-and-backwards-compatibility + """ end end end From a5bd4e0d9230fe9aaad4722681ed12aec0cb5301 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 13 Jan 2026 21:31:07 +0100 Subject: [PATCH 070/391] Partially revert "Elixir 1.19: Refactor Logger configuration to what Elixir 1.15+ expects" This partially reverts commit fc35204cc1d718ba7815cc5e1b08746202b4f7dc. --- config/config.exs | 9 ++++----- config/dev.exs | 2 +- config/prod.exs | 3 ++- config/test.exs | 10 +++------- docs/configuration/cheatsheet.md | 2 +- 5 files changed, 11 insertions(+), 15 deletions(-) diff --git a/config/config.exs b/config/config.exs index 08311146e..5bf2c5c2e 100644 --- a/config/config.exs +++ b/config/config.exs @@ -132,12 +132,11 @@ config :pleroma, Pleroma.Web.Endpoint, "SameSite=Lax" ] -# config :logger, :console has been changed in 1.15+ and split into -# :default_handler and :default_formatter. It started emitting warnings in 1.19. -# See https://hexdocs.pm/logger/1.19.0/Logger.html#module-backends-and-backwards-compatibility -config :logger, :default_handler, level: :debug +# Configures Elixir's Logger +config :logger, backends: [:console] -config :logger, :default_formatter, +config :logger, :console, + level: :debug, format: "\n$time $metadata[$level] $message\n", metadata: [:actor, :path, :type, :user] diff --git a/config/dev.exs b/config/dev.exs index e86bdb7fd..14cf4a6dc 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -36,7 +36,7 @@ config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Local # different ports. # Do not include timestamps in development logs -config :logger, :default_formatter, format: "$metadata[$level] $message\n" +config :logger, Logger.Backends.Console, format: "$metadata[$level] $message\n" # Set a higher stacktrace during development. Avoid configuring such # in production as building large stacktraces may be expensive. diff --git a/config/prod.exs b/config/prod.exs index 1dfbbe937..2d252bf02 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -20,7 +20,8 @@ config :pleroma, Pleroma.Web.Endpoint, config :phoenix, serve_endpoints: true # Do not print debug messages in production -config :logger, :default_handler, level: :info +config :logger, Logger.Backends.Console, level: :info +config :logger, :console, level: :info config :logger, :ex_syslogger, level: :info # ## SSL Support diff --git a/config/test.exs b/config/test.exs index 5b7f3b1ed..9652b7d4b 100644 --- a/config/test.exs +++ b/config/test.exs @@ -15,13 +15,9 @@ config :pleroma, Pleroma.Captcha, method: Pleroma.Captcha.Mock # Print only warnings and errors during test -config :logger, :default_handler, level: :warning - -config :logger, :default_formatter, - format: "\n[$level] $message\n", - colors: [ - enabled: false - ] +config :logger, :console, + level: :warning, + format: "\n[$level] $message\n" config :pleroma, :auth, oauth_consumer_strategies: [] diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 01d81f6ca..b0a7087a7 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -886,7 +886,7 @@ See: [logger’s documentation](https://hexdocs.pm/logger/Logger.html) and [ex_s An example of logging info to local syslog, but debug to console: ```elixir config :pleroma, :logger, - backends: [ {ExSyslogger, :ex_syslogger}] + backends: [{ExSyslogger, :ex_syslogger}] config :logger, :ex_syslogger, level: :info, From aed0eb33331568b38c14b83d753c671b8336cb38 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 13 Jan 2026 23:28:57 +0100 Subject: [PATCH 071/391] Elixir 1.19: Warn on old config of :console Logger and conflicts --- lib/pleroma/config/deprecation_warnings.ex | 122 +++++++++++++----- .../config/deprecation_warnings_test.exs | 47 +++---- 2 files changed, 115 insertions(+), 54 deletions(-) diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex index f38fb7f2f..ee3965b64 100644 --- a/lib/pleroma/config/deprecation_warnings.ex +++ b/lib/pleroma/config/deprecation_warnings.ex @@ -419,42 +419,102 @@ defmodule Pleroma.Config.DeprecationWarnings do @spec check_deprecated_logger_config() :: :ok | :error def check_deprecated_logger_config do - backend = if Application.get_env(:logger, :backends) do - Logger.warning(""" - !!!DEPRECATION WARNING!!! - Your configuration is using deprecated syntax for configuring backends of Elixir's logger. - `config :logger, backends: [...]` is deprecated syntax due to changes in Elixir. - Please update your configuration at your earliest convenience to use: - `config :logger, backends: [...]` - Pleroma's default configuration will be updated in the next release which WILL override your settings. + mix_env = Pleroma.Config.get(:env) + config_file_name = "config/#{mix_env}.secret.exs" + config_file = File.read(config_file_name) - Note: `:console` is no longer considered a backend and is used by default, you can disable it using: - `config :logger, :default_handler: false` - """) + config_file = + case config_file do + {:ok, contents} -> + String.split(contents, "\n", trim: true) + |> Enum.reject(fn string -> String.starts_with?(string, "#") end) + |> Enum.join("\n") - true - else - false - end + _ -> + false + end - console = if Application.get_env(:logger, :console) do - Logger.warning(""" - !!!DEPRECATION WARNING!!! - Your configuration is using deprecated syntax for configuring logging to console. - `config :logger, :console` is deprecated syntax due to changes in Elixir. - Please update your configuration at your earliest convenience to use - `config :logger, default_handler` and `config :logger, :default_formatter`. - Pleroma's default configuration will be updated in the next release which WILL override your settings. + declared_deprecated_backend = + if is_binary(config_file) do + String.contains?(config_file, "config :logger, backends") or + String.contains?(config_file, "config :logger,\n backends") + else + nil + end - Note: `:default_handler` is used only for the `level` setting. All other configurations go under - `:default_formatter`. For more info visit: https://hexdocs.pm/logger/Logger.html#module-backends-and-backwards-compatibility - """) + declared_deprecated_console = + if is_binary(config_file) do + String.contains?(config_file, "config :logger, :console") or + String.contains?(config_file, "config :logger,\n console") + else + nil + end - true - else - false - end + # We can't use Application.get_env/3, because by default default_formatter is populated + declared_new_console = + if is_binary(config_file) do + String.contains?(config_file, "config :logger, :default_handler") or + String.contains?(config_file, "config :logger,\n :default_handler") or + String.contains?(config_file, "config :logger, :default_formatter") or + String.contains?(config_file, "config :logger,\n :default_formatter") + else + nil + end - if backend or console, do: :error, else: :ok + use_new_backend_config = Pleroma.Config.get([:logger, :backends]) + + backend = + if declared_deprecated_backend do + Logger.warning(""" + !!!DEPRECATION WARNING!!! + Your configuration is using deprecated syntax for configuring backends of Elixir's logger. + `config :logger, backends: [...]` is deprecated syntax due to changes in Elixir. + Please update your configuration at your earliest convenience to use: + `config :logger, backends: [...]` + Pleroma's default configuration will be updated in the next release which WILL override your settings. + + Note: `:console` is no longer considered a backend and is used by default, you can disable it using: + `config :logger, :default_handler: false` + """) + + true + else + false + end + + console = + if declared_deprecated_console do + Logger.warning(""" + !!!DEPRECATION WARNING!!! + Your configuration is using deprecated syntax for configuring logging to console. + `config :logger, :console` is deprecated syntax due to changes in Elixir. + Please update your configuration at your earliest convenience to use + `config :logger, default_handler` and `config :logger, :default_formatter`. + Pleroma's default configuration will be updated in the next release which WILL override your settings. + + Note: `:default_handler` is used only for the `level` setting. All other configurations go under + `:default_formatter`. For more info visit: https://hexdocs.pm/logger/Logger.html#module-backends-and-backwards-compatibility + """) + + true + else + false + end + + conflict = + if (!is_nil(use_new_backend_config) and declared_deprecated_backend) or + (declared_new_console and declared_deprecated_console) do + Logger.warning(""" + !!!CONFIGURATION CONFLICT!!! + Pleroma has detected that both the deprecated way of configuring Logger and the new way are defined in your configuration. + Delete the deprecated configuration syntax. + """) + + true + else + false + end + + if backend or console or conflict, do: :error, else: :ok end end diff --git a/test/pleroma/config/deprecation_warnings_test.exs b/test/pleroma/config/deprecation_warnings_test.exs index f5b9d76d3..cf2c6b4d2 100644 --- a/test/pleroma/config/deprecation_warnings_test.exs +++ b/test/pleroma/config/deprecation_warnings_test.exs @@ -401,35 +401,36 @@ defmodule Pleroma.Config.DeprecationWarningsTest do end test "warns on deprecated syntax" do - log = capture_log(fn -> - Pleroma.Config.DeprecationWarnings.check_deprecated_logger_config() - end) + log = + capture_log(fn -> + Pleroma.Config.DeprecationWarnings.check_deprecated_logger_config() + end) assert log =~ - """ - !!!DEPRECATION WARNING!!! - Your configuration is using deprecated syntax for configuring backends of Elixir's logger. - `config :logger, backends: [...]` is deprecated syntax due to changes in Elixir. - Please update your configuration at your earliest convenience to use: - `config :logger, backends: [...]` - Pleroma's default configuration will be updated in the next release which WILL override your settings. + """ + !!!DEPRECATION WARNING!!! + Your configuration is using deprecated syntax for configuring backends of Elixir's logger. + `config :logger, backends: [...]` is deprecated syntax due to changes in Elixir. + Please update your configuration at your earliest convenience to use: + `config :logger, backends: [...]` + Pleroma's default configuration will be updated in the next release which WILL override your settings. - Note: `:console` is no longer considered a backend and is used by default, you can disable it using: - `config :logger, :default_handler: false` - """ + Note: `:console` is no longer considered a backend and is used by default, you can disable it using: + `config :logger, :default_handler: false` + """ assert log =~ - """ - !!!DEPRECATION WARNING!!! - Your configuration is using deprecated syntax for configuring logging to console. - `config :logger, :console` is deprecated syntax due to changes in Elixir. - Please update your configuration at your earliest convenience to use - `config :logger, default_handler` and `config :logger, :default_formatter`. - Pleroma's default configuration will be updated in the next release which WILL override your settings. + """ + !!!DEPRECATION WARNING!!! + Your configuration is using deprecated syntax for configuring logging to console. + `config :logger, :console` is deprecated syntax due to changes in Elixir. + Please update your configuration at your earliest convenience to use + `config :logger, default_handler` and `config :logger, :default_formatter`. + Pleroma's default configuration will be updated in the next release which WILL override your settings. - Note: `:default_handler` is used only for the `level` setting. All other configurations go under - `:default_formatter`. For more info visit: https://hexdocs.pm/logger/Logger.html#module-backends-and-backwards-compatibility - """ + Note: `:default_handler` is used only for the `level` setting. All other configurations go under + `:default_formatter`. For more info visit: https://hexdocs.pm/logger/Logger.html#module-backends-and-backwards-compatibility + """ end end end From bd0c5aa748d226d109aeec7462ac9e6ba8d12f75 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 14 Jan 2026 11:09:57 +0100 Subject: [PATCH 072/391] Elixir 1.19: Move default configs to the new Logger configuration, again Trying to do a two-release migration proved to be very hacky and ugly. Instead this is preparation for moving defaults first and then merging the old configuration left in admin's config file at runtime while still emitting a warning. This hopefully will be a lot nicer and less complicated. --- config/config.exs | 5 ++--- config/dev.exs | 2 +- config/prod.exs | 3 +-- config/test.exs | 5 ++--- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/config/config.exs b/config/config.exs index 5bf2c5c2e..069c0c7c0 100644 --- a/config/config.exs +++ b/config/config.exs @@ -133,10 +133,9 @@ config :pleroma, Pleroma.Web.Endpoint, ] # Configures Elixir's Logger -config :logger, backends: [:console] +config :logger, :default_handler, level: :debug -config :logger, :console, - level: :debug, +config :logger, :default_formatter, format: "\n$time $metadata[$level] $message\n", metadata: [:actor, :path, :type, :user] diff --git a/config/dev.exs b/config/dev.exs index 14cf4a6dc..e86bdb7fd 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -36,7 +36,7 @@ config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Local # different ports. # Do not include timestamps in development logs -config :logger, Logger.Backends.Console, format: "$metadata[$level] $message\n" +config :logger, :default_formatter, format: "$metadata[$level] $message\n" # Set a higher stacktrace during development. Avoid configuring such # in production as building large stacktraces may be expensive. diff --git a/config/prod.exs b/config/prod.exs index 2d252bf02..1dfbbe937 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -20,8 +20,7 @@ config :pleroma, Pleroma.Web.Endpoint, config :phoenix, serve_endpoints: true # Do not print debug messages in production -config :logger, Logger.Backends.Console, level: :info -config :logger, :console, level: :info +config :logger, :default_handler, level: :info config :logger, :ex_syslogger, level: :info # ## SSL Support diff --git a/config/test.exs b/config/test.exs index 9652b7d4b..083759327 100644 --- a/config/test.exs +++ b/config/test.exs @@ -15,9 +15,8 @@ config :pleroma, Pleroma.Captcha, method: Pleroma.Captcha.Mock # Print only warnings and errors during test -config :logger, :console, - level: :warning, - format: "\n[$level] $message\n" +config :logger, :default_handler, level: :warning +config :logger, :default_formatter, format: "\n[$level] $message\n" config :pleroma, :auth, oauth_consumer_strategies: [] From 9450fba83d10fca74e013708bd12406520ef9788 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 14 Jan 2026 13:45:20 +0100 Subject: [PATCH 073/391] Elixir 1.19: Disable colors for logging in test env Color ANSI codes end up in the capture_log output and break existing tests. --- config/test.exs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/config/test.exs b/config/test.exs index 083759327..ed5283a26 100644 --- a/config/test.exs +++ b/config/test.exs @@ -16,7 +16,10 @@ config :pleroma, Pleroma.Captcha, # Print only warnings and errors during test config :logger, :default_handler, level: :warning -config :logger, :default_formatter, format: "\n[$level] $message\n" + +config :logger, :default_formatter, + format: "\n[$level] $message\n", + colors: [enabled: false] config :pleroma, :auth, oauth_consumer_strategies: [] From 0f262bb2f3238bf8d999d22e3a4a45bcbc7fc301 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 14 Jan 2026 13:49:31 +0100 Subject: [PATCH 074/391] Elixir 1.19: Replace ugly old logger config checks with config merging Side note: Going up in log levels doesn't work properly. - debug->info works - info->debug does not work --- lib/pleroma/application.ex | 3 + lib/pleroma/config/deprecation_warnings.ex | 88 ++++++------------- .../config/deprecation_warnings_test.exs | 20 ++++- 3 files changed, 51 insertions(+), 60 deletions(-) diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 0e8723251..78073b430 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -132,6 +132,9 @@ defmodule Pleroma.Application do {:ok, _} -> Logger.debug("Successfully added logger backend: #{inspect(backend)}") + {:error, :already_present} -> + :ok + {:error, reason} -> Logger.error("Failed to add logger backend #{inspect(backend)}: #{inspect(reason)}") end diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex index ee3965b64..4b6281e01 100644 --- a/lib/pleroma/config/deprecation_warnings.ex +++ b/lib/pleroma/config/deprecation_warnings.ex @@ -20,6 +20,9 @@ defmodule Pleroma.Config.DeprecationWarnings do "\n* `config :pleroma, :instance, mrf_transparency_exclusions` is now `config :pleroma, :mrf, transparency_exclusions`"} ] + @logger_config_knobs [:level, :translator_inspect_opts] + @logger_formatter_config_knows [:colors, :format, :metadata, :truncate, :utc_log] + def check_exiftool_filter do filters = Config.get([Pleroma.Upload]) |> Keyword.get(:filters, []) @@ -417,54 +420,33 @@ defmodule Pleroma.Config.DeprecationWarnings do end end + defp merge_deprecated_logger_config(config) do + handler_config = Enum.filter(config, fn {k, _} -> k in @logger_config_knobs end) + formatter_config = Enum.filter(config, fn {k, _} -> k in @logger_formatter_config_knows end) + formatter = Logger.default_formatter(formatter_config) + + Logger.debug(""" + Reconfiguring console Logger with deprecated configuration syntax. + Handler configuration: + #{inspect(handler_config)} + + Formatter: + #{inspect(formatter)} + """) + + Logger.configure(handler_config) + :logger.update_handler_config(:default, :formatter, formatter) + end + @spec check_deprecated_logger_config() :: :ok | :error def check_deprecated_logger_config do - mix_env = Pleroma.Config.get(:env) - config_file_name = "config/#{mix_env}.secret.exs" - config_file = File.read(config_file_name) - - config_file = - case config_file do - {:ok, contents} -> - String.split(contents, "\n", trim: true) - |> Enum.reject(fn string -> String.starts_with?(string, "#") end) - |> Enum.join("\n") - - _ -> - false - end - - declared_deprecated_backend = - if is_binary(config_file) do - String.contains?(config_file, "config :logger, backends") or - String.contains?(config_file, "config :logger,\n backends") - else - nil - end - - declared_deprecated_console = - if is_binary(config_file) do - String.contains?(config_file, "config :logger, :console") or - String.contains?(config_file, "config :logger,\n console") - else - nil - end - - # We can't use Application.get_env/3, because by default default_formatter is populated - declared_new_console = - if is_binary(config_file) do - String.contains?(config_file, "config :logger, :default_handler") or - String.contains?(config_file, "config :logger,\n :default_handler") or - String.contains?(config_file, "config :logger, :default_formatter") or - String.contains?(config_file, "config :logger,\n :default_formatter") - else - nil - end - - use_new_backend_config = Pleroma.Config.get([:logger, :backends]) + backends_config = Application.get_env(:logger, :backends) + console_config = Application.get_env(:logger, :console) + # Note: No need to merge the old backends config since it still works. + # And new configuration will just add new Logger backends. backend = - if declared_deprecated_backend do + if backends_config do Logger.warning(""" !!!DEPRECATION WARNING!!! Your configuration is using deprecated syntax for configuring backends of Elixir's logger. @@ -483,7 +465,7 @@ defmodule Pleroma.Config.DeprecationWarnings do end console = - if declared_deprecated_console do + if console_config do Logger.warning(""" !!!DEPRECATION WARNING!!! Your configuration is using deprecated syntax for configuring logging to console. @@ -496,25 +478,13 @@ defmodule Pleroma.Config.DeprecationWarnings do `:default_formatter`. For more info visit: https://hexdocs.pm/logger/Logger.html#module-backends-and-backwards-compatibility """) - true - else - false - end - - conflict = - if (!is_nil(use_new_backend_config) and declared_deprecated_backend) or - (declared_new_console and declared_deprecated_console) do - Logger.warning(""" - !!!CONFIGURATION CONFLICT!!! - Pleroma has detected that both the deprecated way of configuring Logger and the new way are defined in your configuration. - Delete the deprecated configuration syntax. - """) + merge_deprecated_logger_config(console_config) true else false end - if backend or console or conflict, do: :error, else: :ok + if backend or console, do: :error, else: :ok end end diff --git a/test/pleroma/config/deprecation_warnings_test.exs b/test/pleroma/config/deprecation_warnings_test.exs index cf2c6b4d2..787f9d2fd 100644 --- a/test/pleroma/config/deprecation_warnings_test.exs +++ b/test/pleroma/config/deprecation_warnings_test.exs @@ -392,11 +392,12 @@ defmodule Pleroma.Config.DeprecationWarningsTest do describe "check_deprecated_logger_config" do setup do Application.put_env(:logger, :backends, [:console, {ExSyslogger, :ex_syslogger}]) - Application.put_env(:logger, :console, level: :warning) + Application.put_env(:logger, :console, level: :debug) on_exit(fn -> Application.delete_env(:logger, :backends) Application.delete_env(:logger, :console) + :logger.update_handler_config(:default, :formatter, Logger.default_formatter()) end) end @@ -432,5 +433,22 @@ defmodule Pleroma.Config.DeprecationWarningsTest do `:default_formatter`. For more info visit: https://hexdocs.pm/logger/Logger.html#module-backends-and-backwards-compatibility """ end + + test "reconfigures logger" do + log = + capture_log(fn -> + Pleroma.Config.DeprecationWarnings.check_deprecated_logger_config() + end) + + # Formatter has inconsitent color formatting order + assert log =~ + """ + Reconfiguring console Logger with deprecated configuration syntax. + Handler configuration: + [level: :debug] + + Formatter: + """ + end end end From 02fc770e3638de20592fdaab115a041cda05f952 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 14 Jan 2026 15:02:51 +0100 Subject: [PATCH 075/391] Elixir 1.19: Improve DeprecationWarningsTest Logger reconfiguration test --- .../config/deprecation_warnings_test.exs | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/test/pleroma/config/deprecation_warnings_test.exs b/test/pleroma/config/deprecation_warnings_test.exs index 787f9d2fd..a6ccf063c 100644 --- a/test/pleroma/config/deprecation_warnings_test.exs +++ b/test/pleroma/config/deprecation_warnings_test.exs @@ -11,6 +11,8 @@ defmodule Pleroma.Config.DeprecationWarningsTest do alias Pleroma.Config alias Pleroma.Config.DeprecationWarnings + require Logger + describe "filter exiftool" do test "gives warning when still used" do clear_config( @@ -392,11 +394,12 @@ defmodule Pleroma.Config.DeprecationWarningsTest do describe "check_deprecated_logger_config" do setup do Application.put_env(:logger, :backends, [:console, {ExSyslogger, :ex_syslogger}]) - Application.put_env(:logger, :console, level: :debug) + Application.put_env(:logger, :console, level: :info) on_exit(fn -> Application.delete_env(:logger, :backends) Application.delete_env(:logger, :console) + Logger.configure(level: :debug) :logger.update_handler_config(:default, :formatter, Logger.default_formatter()) end) end @@ -435,20 +438,18 @@ defmodule Pleroma.Config.DeprecationWarningsTest do end test "reconfigures logger" do - log = + empty_log = capture_log(fn -> - Pleroma.Config.DeprecationWarnings.check_deprecated_logger_config() + Logger.debug("This should not be logged") end) - # Formatter has inconsitent color formatting order - assert log =~ - """ - Reconfiguring console Logger with deprecated configuration syntax. - Handler configuration: - [level: :debug] + logged = + capture_log(fn -> + Logger.info("This should be logged") + end) - Formatter: - """ + assert empty_log =~ "" + assert logged =~ "This should be logged" end end end From 829a60ce9365ddc69655cc8e88f625dfa17c5e96 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 15 Jan 2026 16:03:44 +0100 Subject: [PATCH 076/391] Elixir 1.19 MRFTest: Replace matchable_regexes with regexes_match! func --- test/pleroma/web/activity_pub/mrf_test.exs | 26 +++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/test/pleroma/web/activity_pub/mrf_test.exs b/test/pleroma/web/activity_pub/mrf_test.exs index 6656a01b8..401d4ebb3 100644 --- a/test/pleroma/web/activity_pub/mrf_test.exs +++ b/test/pleroma/web/activity_pub/mrf_test.exs @@ -10,22 +10,25 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do alias Pleroma.Web.ActivityPub.MRF + defp regexes_match!([],[]), do: true + + defp regexes_match!([authority | authority_rest], [checked | checked_rest]) do + authority.source == checked.source and regexes_match!(authority_rest, checked_rest) + end + + defp regexes_match!(_, _), do: false + test "subdomains_regex/1" do regexes = MRF.subdomains_regex(["unsafe.tld", "*.unsafe.tld"]) - matchable_regexes = Enum.map(regexes, fn r -> r.source end) - assert matchable_regexes == [ - ~r/^unsafe.tld$/i.source, - ~r/^(.*\.)*unsafe.tld$/i.source - ] + assert regexes_match!(regexes, [~r/^unsafe.tld$/i, ~r/^(.*\.)*unsafe.tld$/i]) end describe "subdomain_match/2" do test "common domains" do regexes = MRF.subdomains_regex(["unsafe.tld", "unsafe2.tld"]) - matchable_regexes = Enum.map(regexes, fn r -> r.source end) - assert matchable_regexes == [~r/^unsafe.tld$/i.source, ~r/^unsafe2.tld$/i.source] + assert regexes_match!(regexes, [~r/^unsafe.tld$/i, ~r/^unsafe2.tld$/i]) assert MRF.subdomain_match?(regexes, "unsafe.tld") assert MRF.subdomain_match?(regexes, "unsafe2.tld") @@ -35,9 +38,8 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do test "wildcard domains with one subdomain" do regexes = MRF.subdomains_regex(["*.unsafe.tld"]) - matchable_regexes = Enum.map(regexes, fn r -> r.source end) - assert matchable_regexes == [~r/^(.*\.)*unsafe.tld$/i.source] + assert regexes_match!(regexes, [~r/^(.*\.)*unsafe.tld$/i]) assert MRF.subdomain_match?(regexes, "unsafe.tld") assert MRF.subdomain_match?(regexes, "sub.unsafe.tld") @@ -47,9 +49,8 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do test "wildcard domains with two subdomains" do regexes = MRF.subdomains_regex(["*.unsafe.tld"]) - matchable_regexes = Enum.map(regexes, fn r -> r.source end) - assert matchable_regexes == [~r/^(.*\.)*unsafe.tld$/i.source] + assert regexes_match!(regexes, [~r/^(.*\.)*unsafe.tld$/i]) assert MRF.subdomain_match?(regexes, "unsafe.tld") assert MRF.subdomain_match?(regexes, "sub.sub.unsafe.tld") @@ -59,9 +60,8 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do test "matches are case-insensitive" do regexes = MRF.subdomains_regex(["UnSafe.TLD", "UnSAFE2.Tld"]) - matchable_regexes = Enum.map(regexes, fn r -> r.source end) - assert matchable_regexes == [~r/^UnSafe.TLD$/i.source, ~r/^UnSAFE2.Tld$/i.source] + assert regexes_match!(regexes, [~r/^UnSafe.TLD$/i, ~r/^UnSAFE2.Tld$/i]) assert MRF.subdomain_match?(regexes, "UNSAFE.TLD") assert MRF.subdomain_match?(regexes, "UNSAFE2.TLD") From ad7ab7741b1d426554765401c92857500f432c6c Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 15 Jan 2026 16:36:59 +0100 Subject: [PATCH 077/391] Elixir 1.19: Update dependencies in docs Elixir 1.15 support only OTP>=24 --- docs/installation/generic_dependencies.include | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation/generic_dependencies.include b/docs/installation/generic_dependencies.include index 588a6c019..382f07a6c 100644 --- a/docs/installation/generic_dependencies.include +++ b/docs/installation/generic_dependencies.include @@ -1,8 +1,8 @@ ## Required dependencies * PostgreSQL >=11.0 -* Elixir >=1.15.0 <1.19 -* Erlang OTP >=23.0.0 (supported: <28) +* Elixir >=1.15.0, <1.20 +* Erlang OTP >=24.0.0 (supported: <29) * git * file / libmagic * gcc or clang From b75f8f7c9addbd73f88ac468473e23aef86b55fa Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 15 Jan 2026 16:56:01 +0100 Subject: [PATCH 078/391] Elixir 1.19: Added changelogs --- changelog.d/elixir-1-19.fix | 1 + changelog.d/logger-backends.change | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog.d/elixir-1-19.fix create mode 100644 changelog.d/logger-backends.change diff --git a/changelog.d/elixir-1-19.fix b/changelog.d/elixir-1-19.fix new file mode 100644 index 000000000..53079b0f7 --- /dev/null +++ b/changelog.d/elixir-1-19.fix @@ -0,0 +1 @@ +Fixed Elixir 1.19 warnings diff --git a/changelog.d/logger-backends.change b/changelog.d/logger-backends.change new file mode 100644 index 000000000..5f044c2c7 --- /dev/null +++ b/changelog.d/logger-backends.change @@ -0,0 +1 @@ +Changed how Logger is configured due to changes in Elixir and added deprecation warnings for the old configuration format From 0ac1b65ba95b9bc3382494a84432bb8ecf088771 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 21 Jan 2026 18:39:15 +0100 Subject: [PATCH 079/391] Elixir 1.19: Fix improper deprecation warning wording --- lib/pleroma/config/deprecation_warnings.ex | 4 +--- test/pleroma/config/deprecation_warnings_test.exs | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex index 4b6281e01..4f68c141c 100644 --- a/lib/pleroma/config/deprecation_warnings.ex +++ b/lib/pleroma/config/deprecation_warnings.ex @@ -452,8 +452,7 @@ defmodule Pleroma.Config.DeprecationWarnings do Your configuration is using deprecated syntax for configuring backends of Elixir's logger. `config :logger, backends: [...]` is deprecated syntax due to changes in Elixir. Please update your configuration at your earliest convenience to use: - `config :logger, backends: [...]` - Pleroma's default configuration will be updated in the next release which WILL override your settings. + `config :pleroma, :logger, backends: [...]` Note: `:console` is no longer considered a backend and is used by default, you can disable it using: `config :logger, :default_handler: false` @@ -472,7 +471,6 @@ defmodule Pleroma.Config.DeprecationWarnings do `config :logger, :console` is deprecated syntax due to changes in Elixir. Please update your configuration at your earliest convenience to use `config :logger, default_handler` and `config :logger, :default_formatter`. - Pleroma's default configuration will be updated in the next release which WILL override your settings. Note: `:default_handler` is used only for the `level` setting. All other configurations go under `:default_formatter`. For more info visit: https://hexdocs.pm/logger/Logger.html#module-backends-and-backwards-compatibility diff --git a/test/pleroma/config/deprecation_warnings_test.exs b/test/pleroma/config/deprecation_warnings_test.exs index a6ccf063c..221609da0 100644 --- a/test/pleroma/config/deprecation_warnings_test.exs +++ b/test/pleroma/config/deprecation_warnings_test.exs @@ -416,8 +416,7 @@ defmodule Pleroma.Config.DeprecationWarningsTest do Your configuration is using deprecated syntax for configuring backends of Elixir's logger. `config :logger, backends: [...]` is deprecated syntax due to changes in Elixir. Please update your configuration at your earliest convenience to use: - `config :logger, backends: [...]` - Pleroma's default configuration will be updated in the next release which WILL override your settings. + `config :pleroma, :logger, backends: [...]` Note: `:console` is no longer considered a backend and is used by default, you can disable it using: `config :logger, :default_handler: false` @@ -430,7 +429,6 @@ defmodule Pleroma.Config.DeprecationWarningsTest do `config :logger, :console` is deprecated syntax due to changes in Elixir. Please update your configuration at your earliest convenience to use `config :logger, default_handler` and `config :logger, :default_formatter`. - Pleroma's default configuration will be updated in the next release which WILL override your settings. Note: `:default_handler` is used only for the `level` setting. All other configurations go under `:default_formatter`. For more info visit: https://hexdocs.pm/logger/Logger.html#module-backends-and-backwards-compatibility From 1743a837dd31e7756a7e3293a170234b2d94fc57 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 21 Jan 2026 20:02:26 +0100 Subject: [PATCH 080/391] Elixir 1.19: Set merged logger config correctly, remove log level test --- lib/pleroma/config/deprecation_warnings.ex | 29 +++++++++--------- .../config/deprecation_warnings_test.exs | 30 +++++-------------- 2 files changed, 23 insertions(+), 36 deletions(-) diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex index 4f68c141c..e77785483 100644 --- a/lib/pleroma/config/deprecation_warnings.ex +++ b/lib/pleroma/config/deprecation_warnings.ex @@ -20,8 +20,7 @@ defmodule Pleroma.Config.DeprecationWarnings do "\n* `config :pleroma, :instance, mrf_transparency_exclusions` is now `config :pleroma, :mrf, transparency_exclusions`"} ] - @logger_config_knobs [:level, :translator_inspect_opts] - @logger_formatter_config_knows [:colors, :format, :metadata, :truncate, :utc_log] + @logger_formatter_config_knobs [:colors, :format, :metadata, :truncate, :utc_log] def check_exiftool_filter do filters = Config.get([Pleroma.Upload]) |> Keyword.get(:filters, []) @@ -421,21 +420,23 @@ defmodule Pleroma.Config.DeprecationWarnings do end defp merge_deprecated_logger_config(config) do - handler_config = Enum.filter(config, fn {k, _} -> k in @logger_config_knobs end) - formatter_config = Enum.filter(config, fn {k, _} -> k in @logger_formatter_config_knows end) + formatter_config = Enum.filter(config, fn {k, _} -> k in @logger_formatter_config_knobs end) formatter = Logger.default_formatter(formatter_config) + log_level = Keyword.get(config, :level, nil) - Logger.debug(""" - Reconfiguring console Logger with deprecated configuration syntax. - Handler configuration: - #{inspect(handler_config)} + if Config.get(:env) != :test do + Logger.debug(""" + Reconfiguring console Logger with deprecated configuration syntax. + Handler configuration: + log_level: #{inspect(log_level)} - Formatter: - #{inspect(formatter)} - """) + Formatter: + #{inspect(formatter)} + """) - Logger.configure(handler_config) - :logger.update_handler_config(:default, :formatter, formatter) + if log_level, do: :logger.update_handler_config(:default, :level, log_level) + :logger.update_handler_config(:default, :formatter, formatter) + end end @spec check_deprecated_logger_config() :: :ok | :error @@ -443,7 +444,7 @@ defmodule Pleroma.Config.DeprecationWarnings do backends_config = Application.get_env(:logger, :backends) console_config = Application.get_env(:logger, :console) - # Note: No need to merge the old backends config since it still works. + # NOTE: No need to merge the old backends config since it still works. # And new configuration will just add new Logger backends. backend = if backends_config do diff --git a/test/pleroma/config/deprecation_warnings_test.exs b/test/pleroma/config/deprecation_warnings_test.exs index 221609da0..475a88c4d 100644 --- a/test/pleroma/config/deprecation_warnings_test.exs +++ b/test/pleroma/config/deprecation_warnings_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Config.DeprecationWarningsTest do - use ExUnit.Case + use ExUnit.Case, async: false use Pleroma.Tests.Helpers import ExUnit.CaptureLog @@ -393,14 +393,15 @@ defmodule Pleroma.Config.DeprecationWarningsTest do describe "check_deprecated_logger_config" do setup do - Application.put_env(:logger, :backends, [:console, {ExSyslogger, :ex_syslogger}]) - Application.put_env(:logger, :console, level: :info) + initial_console = Application.get_env(:logger, :console, nil) + initial_backends = Application.get_env(:logger, :backends, nil) + + Application.put_env(:logger, :console, level: :all) + Application.put_env(:logger, :backends, [:console]) on_exit(fn -> - Application.delete_env(:logger, :backends) - Application.delete_env(:logger, :console) - Logger.configure(level: :debug) - :logger.update_handler_config(:default, :formatter, Logger.default_formatter()) + if initial_console, do: Application.put_env(:logger, :console, initial_console), else: Application.delete_env(:logger, :console) + if initial_backends, do: Application.put_env(:logger, :backends, initial_backends), else: Application.delete_env(:logger, :backends) end) end @@ -434,20 +435,5 @@ defmodule Pleroma.Config.DeprecationWarningsTest do `:default_formatter`. For more info visit: https://hexdocs.pm/logger/Logger.html#module-backends-and-backwards-compatibility """ end - - test "reconfigures logger" do - empty_log = - capture_log(fn -> - Logger.debug("This should not be logged") - end) - - logged = - capture_log(fn -> - Logger.info("This should be logged") - end) - - assert empty_log =~ "" - assert logged =~ "This should be logged" - end end end From f44774d387c88ce6289ff691a4aa5662c0966964 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 21 Jan 2026 21:13:23 +0100 Subject: [PATCH 081/391] lint --- test/pleroma/config/deprecation_warnings_test.exs | 13 +++++++++++-- test/pleroma/web/activity_pub/mrf_test.exs | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/test/pleroma/config/deprecation_warnings_test.exs b/test/pleroma/config/deprecation_warnings_test.exs index 475a88c4d..72dd63100 100644 --- a/test/pleroma/config/deprecation_warnings_test.exs +++ b/test/pleroma/config/deprecation_warnings_test.exs @@ -400,8 +400,17 @@ defmodule Pleroma.Config.DeprecationWarningsTest do Application.put_env(:logger, :backends, [:console]) on_exit(fn -> - if initial_console, do: Application.put_env(:logger, :console, initial_console), else: Application.delete_env(:logger, :console) - if initial_backends, do: Application.put_env(:logger, :backends, initial_backends), else: Application.delete_env(:logger, :backends) + if initial_console do + Application.put_env(:logger, :console, initial_console) + else + Application.delete_env(:logger, :console) + end + + if initial_backends do + Application.put_env(:logger, :backends, initial_backends) + else + Application.delete_env(:logger, :backends) + end end) end diff --git a/test/pleroma/web/activity_pub/mrf_test.exs b/test/pleroma/web/activity_pub/mrf_test.exs index 401d4ebb3..2d0f3b317 100644 --- a/test/pleroma/web/activity_pub/mrf_test.exs +++ b/test/pleroma/web/activity_pub/mrf_test.exs @@ -10,7 +10,7 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do alias Pleroma.Web.ActivityPub.MRF - defp regexes_match!([],[]), do: true + defp regexes_match!([], []), do: true defp regexes_match!([authority | authority_rest], [checked | checked_rest]) do authority.source == checked.source and regexes_match!(authority_rest, checked_rest) From a12f9777f857002318a6d6da17473bb6a52276b7 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 12 Feb 2026 23:46:08 +0100 Subject: [PATCH 082/391] Elixir 1.19: Bump Woodpecker test pipeline to 1.19 --- .woodpecker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.woodpecker.yml b/.woodpecker.yml index 4ceb1cab5..f6b06dc61 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -4,7 +4,7 @@ when: steps: test: - image: elixir:1.15-alpine + image: elixir:1.19-alpine environment: MIX_ENV: test DB_HOST: postgres From 01b139c6331977f5d622866127be9b4291cd4732 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sun, 22 Feb 2026 21:50:28 +0100 Subject: [PATCH 083/391] Elixir 1.19: Add note to deprecation warnings test The Logger does not get reconfigured when running in the test env, the added and on_exit removed settings below should have no effect besides triggering the deprecation warnings. To make breakage even less possible, async is turned off for these tests. TransferTask test does almost the same with the Application env. --- test/pleroma/config/deprecation_warnings_test.exs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/pleroma/config/deprecation_warnings_test.exs b/test/pleroma/config/deprecation_warnings_test.exs index 72dd63100..b65b0e630 100644 --- a/test/pleroma/config/deprecation_warnings_test.exs +++ b/test/pleroma/config/deprecation_warnings_test.exs @@ -396,6 +396,9 @@ defmodule Pleroma.Config.DeprecationWarningsTest do initial_console = Application.get_env(:logger, :console, nil) initial_backends = Application.get_env(:logger, :backends, nil) + # NOTE: The Logger does not get reconfigured when running in the test env, + # the added and on_exit removed settings below should have no effect besides + # triggering the deprecation warnings Application.put_env(:logger, :console, level: :all) Application.put_env(:logger, :backends, [:console]) From 08ba0d46d77cbb19f218b464a1cbeb700227e6a8 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sun, 22 Feb 2026 22:27:48 +0100 Subject: [PATCH 084/391] ActivityDraft: Add missing __MODULE__ matches and drop unneeded ones --- lib/pleroma/web/common_api/activity_draft.ex | 48 ++++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index 6c84ef656..16489663a 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -99,34 +99,34 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do |> Map.put("cc", draft.cc) |> Map.put("actor", draft.user.ap_id) - %__MODULE__{draft | object: object} + %{draft | object: object} end defp put_params(%__MODULE__{} = draft, params) do params = Map.put_new(params, :in_reply_to_status_id, params[:in_reply_to_id]) - %__MODULE__{draft | params: params} + %{draft | params: params} end defp status(%__MODULE__{params: %{status: status}} = draft) do - %__MODULE__{draft | status: String.trim(status)} + %{draft | status: String.trim(status)} end defp summary(%__MODULE__{params: params} = draft) do - %__MODULE__{draft | summary: Map.get(params, :spoiler_text, "")} + %{draft | summary: Map.get(params, :spoiler_text, "")} end defp full_payload(%__MODULE__{status: status, summary: summary} = draft) do full_payload = String.trim(status <> summary) case Utils.validate_character_limit(full_payload, draft.attachments) do - :ok -> %__MODULE__{draft | full_payload: full_payload} + :ok -> %{draft | full_payload: full_payload} {:error, message} -> add_error(draft, message) end end defp attachments(%__MODULE__{params: params} = draft) do attachments = Utils.attachments_from_ids(params, draft.user) - draft = %__MODULE__{draft | attachments: attachments} + draft = %{draft | attachments: attachments} case Utils.validate_attachments_count(attachments) do :ok -> draft @@ -149,7 +149,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do with %Activity{} = activity <- Activity.get_by_id(id), true <- Visibility.visible_for_user?(activity, draft.user), {_, type} when type in ["Create", "Announce"] <- {:type, activity.data["type"]} do - %__MODULE__{draft | in_reply_to: activity} + %{draft | in_reply_to: activity} else nil -> add_error(draft, dgettext("errors", "Cannot reply to a deleted status")) @@ -170,7 +170,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp in_reply_to( %__MODULE__{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft ) do - %__MODULE__{draft | in_reply_to: in_reply_to} + %{draft | in_reply_to: in_reply_to} end defp in_reply_to(draft), do: draft @@ -179,14 +179,14 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do when not_empty_string(id) do case Activity.get_by_id_with_object(id) do %Activity{} = activity -> - %__MODULE__{draft | quote_post: activity} + %{draft | quote_post: activity} _ -> draft end end - defp quote_post(%{params: %{quote_id: id}} = draft) when not_empty_string(id) do + defp quote_post(%__MODULE__{params: %{quote_id: id}} = draft) when not_empty_string(id) do quote_post(%{draft | params: Map.put(draft.params, :quoted_status_id, id)}) end @@ -194,7 +194,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp in_reply_to_conversation(%__MODULE__{} = draft) do in_reply_to_conversation = Participation.get(draft.params[:in_reply_to_conversation_id]) - %__MODULE__{draft | in_reply_to_conversation: in_reply_to_conversation} + %{draft | in_reply_to_conversation: in_reply_to_conversation} end defp visibility(%__MODULE__{params: params} = draft) do @@ -203,7 +203,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do add_error(draft, dgettext("errors", "The message visibility must be direct")) {visibility, _} -> - %__MODULE__{draft | visibility: visibility} + %{draft | visibility: visibility} end end @@ -219,7 +219,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do false end - defp quoting_visibility(%{quote_post: %Activity{}} = draft) do + defp quoting_visibility(%__MODULE__{quote_post: %Activity{}} = draft) do with %Object{} = object <- Object.normalize(draft.quote_post, fetch: false), true <- can_quote?(draft, object, Visibility.get_visibility(object)) do draft @@ -232,7 +232,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp expires_at(%__MODULE__{} = draft) do case CommonAPI.check_expiry_date(draft.params[:expires_in]) do - {:ok, expires_at} -> %__MODULE__{draft | expires_at: expires_at} + {:ok, expires_at} -> %{draft | expires_at: expires_at} {:error, message} -> add_error(draft, message) end end @@ -240,7 +240,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp poll(%__MODULE__{} = draft) do case Utils.make_poll_data(draft.params) do {:ok, {poll, poll_emoji}} -> - %__MODULE__{draft | extra: poll, emoji: Map.merge(draft.emoji, poll_emoji)} + %{draft | extra: poll, emoji: Map.merge(draft.emoji, poll_emoji)} {:error, message} -> add_error(draft, message) @@ -258,22 +258,22 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do |> Kernel.++(mentioned_ap_ids) |> Utils.get_addressed_users(draft.params[:to]) - %__MODULE__{draft | content_html: content_html, mentions: mentions, tags: tags} + %{draft | content_html: content_html, mentions: mentions, tags: tags} end defp to_and_cc(%__MODULE__{} = draft) do {to, cc} = Utils.get_to_and_cc(draft) - %__MODULE__{draft | to: to, cc: cc} + %{draft | to: to, cc: cc} end defp context(%__MODULE__{} = draft) do context = Utils.make_context(draft.in_reply_to, draft.in_reply_to_conversation) - %__MODULE__{draft | context: context} + %{draft | context: context} end defp sensitive(%__MODULE__{} = draft) do sensitive = draft.params[:sensitive] - %__MODULE__{draft | sensitive: sensitive} + %{draft | sensitive: sensitive} end defp language(%__MODULE__{} = draft) do @@ -285,7 +285,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do _ -> LanguageDetector.detect(draft.content_html <> " " <> draft.summary) end - %__MODULE__{draft | language: language} + %{draft | language: language} end defp object(%__MODULE__{} = draft) do @@ -329,12 +329,12 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do |> Map.put("generator", draft.params[:generator]) |> Map.put("language", draft.language) - %__MODULE__{draft | object: object} + %{draft | object: object} end defp preview?(%__MODULE__{} = draft) do preview? = Pleroma.Web.Utils.Params.truthy_param?(draft.params[:preview]) - %__MODULE__{draft | preview?: preview?} + %{draft | preview?: preview?} end defp changes(%__MODULE__{} = draft) do @@ -357,14 +357,14 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do } |> Utils.maybe_add_list_data(draft.user, draft.visibility) - %__MODULE__{draft | changes: changes} + %{draft | changes: changes} end defp with_valid(%{valid?: true} = draft, func), do: func.(draft) defp with_valid(draft, _func), do: draft defp add_error(%__MODULE__{} = draft, message) do - %__MODULE__{draft | valid?: false, errors: [message | draft.errors]} + %{draft | valid?: false, errors: [message | draft.errors]} end defp validate(%{valid?: true} = draft), do: {:ok, draft} From 8ac967518b1729aa2530bb2e3003a31bc40cabfb Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 25 Mar 2026 15:11:45 -0700 Subject: [PATCH 085/391] Switch to the newly relocated Pleroma.Search.object_to_search_data/1 --- lib/pleroma/search/paradedb.ex | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/search/paradedb.ex b/lib/pleroma/search/paradedb.ex index a6649a894..20be5b6f9 100644 --- a/lib/pleroma/search/paradedb.ex +++ b/lib/pleroma/search/paradedb.ex @@ -18,10 +18,9 @@ defmodule Pleroma.Search.ParadeDB do maybe_restrict_local: 2 ] - import Pleroma.Search.Meilisearch, only: [object_to_search_data: 1] - alias Pleroma.Activity alias Pleroma.Config.Getting, as: Config + alias Pleroma.Search @max_limit 40 @default_table "pleroma_search_documents" @@ -81,7 +80,7 @@ defmodule Pleroma.Search.ParadeDB do @impl true def add_to_index(activity) do - maybe_search_data = object_to_search_data(activity.object) + maybe_search_data = Search.object_to_search_data(activity.object) if activity.data["type"] == "Create" and maybe_search_data do dumped_activity_id = dump_activity_id(activity.id) From 1e3456a1604ebe5818e319f47bbeb79828c3d6e5 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 25 Mar 2026 15:14:35 -0700 Subject: [PATCH 086/391] Ensure the Ecto Repo for ParadeDB is started for the data indexing Mix task --- lib/mix/pleroma.ex | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/mix/pleroma.ex b/lib/mix/pleroma.ex index e7869919c..372bf3399 100644 --- a/lib/mix/pleroma.ex +++ b/lib/mix/pleroma.ex @@ -62,7 +62,8 @@ defmodule Mix.Pleroma do {Majic.Pool, [name: Pleroma.MajicPool, pool_size: Pleroma.Config.get([:majic_pool, :size], 2)]} ] ++ - http_children(adapter) + http_children(adapter) ++ + search_children() cachex_children = Enum.map(@cachex_children, &Pleroma.Application.build_cachex(&1, [])) @@ -141,4 +142,14 @@ defmodule Mix.Pleroma do end defp http_children(_), do: [] + + defp search_children do + case Config.get([Pleroma.Search, :module]) do + Pleroma.Search.ParadeDB -> + [Pleroma.Search.ParadeDB.Repo] + + _ -> + [] + end + end end From 3776f13f20172fa7c88127ba28583fba27cb1e1c Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 1 Apr 2026 19:10:29 +0000 Subject: [PATCH 087/391] Fix warning --- lib/mix/pleroma.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/pleroma.ex b/lib/mix/pleroma.ex index 372bf3399..aebad6984 100644 --- a/lib/mix/pleroma.ex +++ b/lib/mix/pleroma.ex @@ -144,7 +144,7 @@ defmodule Mix.Pleroma do defp http_children(_), do: [] defp search_children do - case Config.get([Pleroma.Search, :module]) do + case Pleroma.Config.get([Pleroma.Search, :module]) do Pleroma.Search.ParadeDB -> [Pleroma.Search.ParadeDB.Repo] From c2fb145c5f33ea1b4c3ad7dd4ce21588bedc5e95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 10 Apr 2026 21:33:20 +0200 Subject: [PATCH 088/391] litepub-0.1.jsonld cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/context-cleanup.skip | 1 + priv/static/schemas/litepub-0.1.jsonld | 59 +++++++++++++++----------- 2 files changed, 35 insertions(+), 25 deletions(-) create mode 100644 changelog.d/context-cleanup.skip diff --git a/changelog.d/context-cleanup.skip b/changelog.d/context-cleanup.skip new file mode 100644 index 000000000..ae609602d --- /dev/null +++ b/changelog.d/context-cleanup.skip @@ -0,0 +1 @@ +litepub-0.1.jsonld cleanup diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld index 3569165a4..4e82d4b01 100644 --- a/priv/static/schemas/litepub-0.1.jsonld +++ b/priv/static/schemas/litepub-0.1.jsonld @@ -4,46 +4,55 @@ "https://w3id.org/security/v1", "https://purl.archive.org/socialweb/webfinger", { - "Emoji": "toot:Emoji", + "as": "https://www.w3.org/ns/activitystreams#", + "ostatus": "http://ostatus.org#", + "schema": "http://schema.org#", + "vcard": "http://www.w3.org/2006/vcard/ns#", + + "fedibird": "http://fedibird.com/ns#", + "litepub": "http://litepub.social/ns#", + "sm": "http://smithereen.software/ns#", + "toot": "http://joinmastodon.org/ns#", + + "alsoKnownAs": { + "@id": "as:alsoKnownAs", + "@type": "@id" + }, "Hashtag": "as:Hashtag", - "PropertyValue": "schema:PropertyValue", + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "quoteUrl": "as:quoteUrl", + "sensitive": "as:sensitive", + "atomUri": "ostatus:atomUri", "conversation": { "@id": "ostatus:conversation", "@type": "@id" }, - "discoverable": "toot:discoverable", - "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", - "capabilities": "litepub:capabilities", - "ostatus": "http://ostatus.org#", - "schema": "http://schema.org#", - "toot": "http://joinmastodon.org/ns#", - "fedibird": "http://fedibird.com/ns#", + + "PropertyValue": "schema:PropertyValue", "value": "schema:value", - "sensitive": "as:sensitive", - "litepub": "http://litepub.social/ns#", - "invisible": "litepub:invisible", + + "quoteUri": "fedibird:quoteUri", + + "capabilities": "litepub:capabilities", + "ChatMessage": "litepub:ChatMessage", "directMessage": "litepub:directMessage", + "EmojiReact": "litepub:EmojiReact", + "formerRepresentations": "litepub:formerRepresentations", + "invisible": "litepub:invisible", "listMessage": { "@id": "litepub:listMessage", "@type": "@id" }, - "quoteUrl": "as:quoteUrl", - "quoteUri": "fedibird:quoteUri", "oauthRegistrationEndpoint": { "@id": "litepub:oauthRegistrationEndpoint", "@type": "@id" }, - "EmojiReact": "litepub:EmojiReact", - "ChatMessage": "litepub:ChatMessage", - "alsoKnownAs": { - "@id": "as:alsoKnownAs", - "@type": "@id" - }, - "vcard": "http://www.w3.org/2006/vcard/ns#", - "formerRepresentations": "litepub:formerRepresentations", - "sm": "http://smithereen.software/ns#", - "nonAnonymous": "sm:nonAnonymous" + + "nonAnonymous": "sm:nonAnonymous", + + "discoverable": "toot:discoverable", + "Emoji": "toot:Emoji" } ] -} +} \ No newline at end of file From af175fbdfc589be7f80adcb9613cfa3d67a217a8 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sun, 3 May 2026 21:19:24 +0400 Subject: [PATCH 089/391] Woodpecker CI: Build armv7 Docker images --- .woodpecker/docker-combine.yaml | 2 +- .woodpecker/docker.yaml | 47 +++++++++++++++++++++++++++++++++ Dockerfile | 6 +++-- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/.woodpecker/docker-combine.yaml b/.woodpecker/docker-combine.yaml index 6c91d26e9..9b9f89fdb 100644 --- a/.woodpecker/docker-combine.yaml +++ b/.woodpecker/docker-combine.yaml @@ -27,7 +27,7 @@ steps: settings: &docker_settings registry: "git.pleroma.social" image: "pleroma/pleroma" - architectures: [amd64, arm64] + architectures: [amd64, arm64, armv7] tags: - latest - develop diff --git a/.woodpecker/docker.yaml b/.woodpecker/docker.yaml index abc6bfa3b..5f174fb99 100644 --- a/.woodpecker/docker.yaml +++ b/.woodpecker/docker.yaml @@ -11,6 +11,7 @@ when: matrix: platform: - linux/amd64 + - linux/arm - linux/arm64 # This is needed for the when clauses below. @@ -27,6 +28,13 @@ variables: password: from_secret: pleroma-ci-password kaniko_image: &kaniko_image woodpeckerci/plugin-kaniko:2.3.1 + # HexPM Elixir images are not published for arm/v7. + armv7_build_args: &armv7_build_args + - ELIXIR_IMG=arm32v7/elixir + - ELIXIR_TAG=1.17.3-alpine + - ALPINE_IMG=arm32v7/alpine + - ALPINE_VER=3.22 + armv7_extra_opts: &armv7_extra_opts --custom-platform=linux/arm/v7 steps: docker-develop-amd64: @@ -51,6 +59,19 @@ steps: - develop-arm64 - ${CI_COMMIT_SHA:0:8}-arm64 + docker-develop-armv7: + image: *kaniko_image + when: + - evaluate: 'platform == "linux/arm" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + settings: + <<: *docker_variables + build_args: *armv7_build_args + extra_opts: *armv7_extra_opts + tags: + - latest-armv7 + - develop-armv7 + - ${CI_COMMIT_SHA:0:8}-armv7 + docker-stable-amd64: image: *kaniko_image when: @@ -84,6 +105,19 @@ steps: - stable-arm64 - ${CI_COMMIT_SHA:0:8}-arm64 + docker-stable-armv7: + image: *kaniko_image + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG == ""' + settings: + <<: *docker_variables + build_args: *armv7_build_args + extra_opts: *armv7_extra_opts + tags: &armv7_tags + - latest-armv7 + - stable-armv7 + - ${CI_COMMIT_SHA:0:8}-armv7 + docker-stable-tag-arm64: image: *kaniko_image when: @@ -94,3 +128,16 @@ steps: tags: - <<: *arm64_tags - ${CI_COMMIT_TAG}-arm64 + + docker-stable-tag-armv7: + image: *kaniko_image + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"' + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG != ""' + settings: + <<: *docker_variables + build_args: *armv7_build_args + extra_opts: *armv7_extra_opts + tags: + - <<: *armv7_tags + - ${CI_COMMIT_TAG}-armv7 diff --git a/Dockerfile b/Dockerfile index 11a17b7ad..5fd1cc0a1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,8 +3,10 @@ ARG ELIXIR_IMG=hexpm/elixir ARG ELIXIR_VER=1.17.3 ARG ERLANG_VER=26.2.5.6 ARG ALPINE_VER=3.17.9 +ARG ELIXIR_TAG=${ELIXIR_VER}-erlang-${ERLANG_VER}-alpine-${ALPINE_VER} +ARG ALPINE_IMG=alpine -FROM ${ELIXIR_IMG}:${ELIXIR_VER}-erlang-${ERLANG_VER}-alpine-${ALPINE_VER} AS build +FROM ${ELIXIR_IMG}:${ELIXIR_TAG} AS build COPY . . @@ -20,7 +22,7 @@ RUN apk add git gcc g++ musl-dev make cmake file-dev vips-dev &&\ mkdir release &&\ mix release --path release -FROM alpine:${ALPINE_VER} +FROM ${ALPINE_IMG}:${ALPINE_VER} ARG BUILD_DATE ARG VCS_REF From ae763ff6cf97fdb78edc5bdad13aab917cea37ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 4 May 2026 01:41:12 +0200 Subject: [PATCH 090/391] api/v2/admin/accounts: support 'permissions' param MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../api_spec/operations/mastodon_admin/account_operation.ex | 2 +- .../web/mastodon_api/admin/controllers/account_controller.ex | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/api_spec/operations/mastodon_admin/account_operation.ex b/lib/pleroma/web/api_spec/operations/mastodon_admin/account_operation.ex index e9143364c..ad79d43d7 100644 --- a/lib/pleroma/web/api_spec/operations/mastodon_admin/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/mastodon_admin/account_operation.ex @@ -126,7 +126,7 @@ defmodule Pleroma.Web.ApiSpec.MastodonAdmin.AccountOperation do :permissions, :query, :string, - "Filter for accounts with staff permissions (users that can manage reports). (not implemented yet)" + "Filter for accounts with staff permissions (users that can manage reports)." ), Operation.parameter( :role_ids, diff --git a/lib/pleroma/web/mastodon_api/admin/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/admin/controllers/account_controller.ex index 833bf1db5..e96566774 100644 --- a/lib/pleroma/web/mastodon_api/admin/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/admin/controllers/account_controller.ex @@ -183,6 +183,7 @@ defmodule Pleroma.Web.MastodonAPI.Admin.AccountController do %{} |> maybe_filter_origin(params) |> maybe_filter_status(params) + |> maybe_filter_staff(params) |> maybe_filter_nickname(params) |> maybe_filter_name(params) |> maybe_filter_domain(params) @@ -240,6 +241,9 @@ defmodule Pleroma.Web.MastodonAPI.Admin.AccountController do defp maybe_filter_email(criteria, %{email: email} = _params), do: Map.put(criteria, :email, email) + defp maybe_filter_staff(criteria, %{permissions: "staff"} = _params), + do: Map.put(criteria, :staff, true) + defp maybe_filter_staff(criteria, %{staff: staff} = _params), do: Map.put(criteria, :staff, staff) From 684e9ef24702bf2c509d42445fc665ceb91070d4 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 4 May 2026 09:41:06 +0400 Subject: [PATCH 091/391] Woodpecker CI: Isolate armv7 Docker builds --- .woodpecker/docker-armv7.yaml | 84 +++++++++++++++++++++++++++++++++ .woodpecker/docker-combine.yaml | 2 +- .woodpecker/docker.yaml | 47 ------------------ 3 files changed, 85 insertions(+), 48 deletions(-) create mode 100644 .woodpecker/docker-armv7.yaml diff --git a/.woodpecker/docker-armv7.yaml b/.woodpecker/docker-armv7.yaml new file mode 100644 index 000000000..19f3049b3 --- /dev/null +++ b/.woodpecker/docker-armv7.yaml @@ -0,0 +1,84 @@ +when: + # Temporary PR validation while arm/v7 Docker builds are being brought up. + - event: pull_request + path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] + - event: push + branch: ${CI_REPO_DEFAULT_BRANCH} + path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] + - event: tag + - event: manual + branch: ${CI_REPO_DEFAULT_BRANCH} + - event: manual + branch: stable + +# Target the CAX21 arm64 host with native arm32 userspace support, not generic arm64 runners. +labels: + armv7: native + +variables: + docker_variables: &docker_variables + repo: pleroma/pleroma + registry: git.pleroma.social + armv7_build_settings: &armv7_build_settings + <<: *docker_variables + build_args: + - ELIXIR_IMG=arm32v7/elixir + - ELIXIR_TAG=1.17.3-otp-26-alpine + - ALPINE_IMG=arm32v7/alpine + - ALPINE_VER=3.20 + extra_opts: --custom-platform=linux/arm/v7 + armv7_push_settings: &armv7_push_settings + <<: *armv7_build_settings + username: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + +steps: + docker-armv7-pr: + image: woodpeckerci/plugin-kaniko:2.3.1 + when: + - event: pull_request + settings: + <<: *armv7_build_settings + dry_run: true + tags: + - armv7-pr-check + + docker-develop-armv7: + image: woodpeckerci/plugin-kaniko:2.3.1 + when: + - event: push + branch: ${CI_REPO_DEFAULT_BRANCH} + - event: manual + branch: ${CI_REPO_DEFAULT_BRANCH} + settings: + <<: *armv7_push_settings + tags: + - latest-armv7 + - develop-armv7 + - ${CI_COMMIT_SHA:0:8}-armv7 + + docker-stable-armv7: + image: woodpeckerci/plugin-kaniko:2.3.1 + when: + - evaluate: 'CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG == ""' + settings: + <<: *armv7_push_settings + tags: + - latest-armv7 + - stable-armv7 + - ${CI_COMMIT_SHA:0:8}-armv7 + + docker-stable-tag-armv7: + image: woodpeckerci/plugin-kaniko:2.3.1 + when: + - event: tag + - evaluate: 'CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG != ""' + settings: + <<: *armv7_push_settings + tags: + - latest-armv7 + - stable-armv7 + - ${CI_COMMIT_SHA:0:8}-armv7 + - ${CI_COMMIT_TAG}-armv7 diff --git a/.woodpecker/docker-combine.yaml b/.woodpecker/docker-combine.yaml index 9b9f89fdb..6c91d26e9 100644 --- a/.woodpecker/docker-combine.yaml +++ b/.woodpecker/docker-combine.yaml @@ -27,7 +27,7 @@ steps: settings: &docker_settings registry: "git.pleroma.social" image: "pleroma/pleroma" - architectures: [amd64, arm64, armv7] + architectures: [amd64, arm64] tags: - latest - develop diff --git a/.woodpecker/docker.yaml b/.woodpecker/docker.yaml index 5f174fb99..abc6bfa3b 100644 --- a/.woodpecker/docker.yaml +++ b/.woodpecker/docker.yaml @@ -11,7 +11,6 @@ when: matrix: platform: - linux/amd64 - - linux/arm - linux/arm64 # This is needed for the when clauses below. @@ -28,13 +27,6 @@ variables: password: from_secret: pleroma-ci-password kaniko_image: &kaniko_image woodpeckerci/plugin-kaniko:2.3.1 - # HexPM Elixir images are not published for arm/v7. - armv7_build_args: &armv7_build_args - - ELIXIR_IMG=arm32v7/elixir - - ELIXIR_TAG=1.17.3-alpine - - ALPINE_IMG=arm32v7/alpine - - ALPINE_VER=3.22 - armv7_extra_opts: &armv7_extra_opts --custom-platform=linux/arm/v7 steps: docker-develop-amd64: @@ -59,19 +51,6 @@ steps: - develop-arm64 - ${CI_COMMIT_SHA:0:8}-arm64 - docker-develop-armv7: - image: *kaniko_image - when: - - evaluate: 'platform == "linux/arm" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' - settings: - <<: *docker_variables - build_args: *armv7_build_args - extra_opts: *armv7_extra_opts - tags: - - latest-armv7 - - develop-armv7 - - ${CI_COMMIT_SHA:0:8}-armv7 - docker-stable-amd64: image: *kaniko_image when: @@ -105,19 +84,6 @@ steps: - stable-arm64 - ${CI_COMMIT_SHA:0:8}-arm64 - docker-stable-armv7: - image: *kaniko_image - when: - - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG == ""' - settings: - <<: *docker_variables - build_args: *armv7_build_args - extra_opts: *armv7_extra_opts - tags: &armv7_tags - - latest-armv7 - - stable-armv7 - - ${CI_COMMIT_SHA:0:8}-armv7 - docker-stable-tag-arm64: image: *kaniko_image when: @@ -128,16 +94,3 @@ steps: tags: - <<: *arm64_tags - ${CI_COMMIT_TAG}-arm64 - - docker-stable-tag-armv7: - image: *kaniko_image - when: - - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"' - - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG != ""' - settings: - <<: *docker_variables - build_args: *armv7_build_args - extra_opts: *armv7_extra_opts - tags: - - <<: *armv7_tags - - ${CI_COMMIT_TAG}-armv7 From 394db0dce0c6ca3e55f2039cca35b1bb8e651875 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 4 May 2026 10:09:44 +0400 Subject: [PATCH 092/391] Woodpecker CI: Target armv7 Docker runner --- .woodpecker/docker-armv7.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.woodpecker/docker-armv7.yaml b/.woodpecker/docker-armv7.yaml index 19f3049b3..c921a56f9 100644 --- a/.woodpecker/docker-armv7.yaml +++ b/.woodpecker/docker-armv7.yaml @@ -13,6 +13,7 @@ when: # Target the CAX21 arm64 host with native arm32 userspace support, not generic arm64 runners. labels: + platform: linux/arm armv7: native variables: From 11281d740a136c349b927587c1d895ea590f76af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 4 May 2026 13:48:38 +0200 Subject: [PATCH 093/391] make GET /api/v1/admin/reports match Mastodon admin API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../mastodon_admin/report_operation.ex | 1 + .../admin/controllers/report_controller.ex | 39 ++++++++++++++++++- .../controllers/report_controller_test.exs | 12 +++++- 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/mastodon_admin/report_operation.ex b/lib/pleroma/web/api_spec/operations/mastodon_admin/report_operation.ex index 3360cb843..f59db9966 100644 --- a/lib/pleroma/web/api_spec/operations/mastodon_admin/report_operation.ex +++ b/lib/pleroma/web/api_spec/operations/mastodon_admin/report_operation.ex @@ -28,6 +28,7 @@ defmodule Pleroma.Web.ApiSpec.MastodonAdmin.ReportOperation do parameters: [ Operation.parameter(:resolved, :query, :boolean, "Filter for resolved reports"), + Operation.parameter(:unresolved, :query, :boolean, "Filter for open reports"), Operation.parameter(:account_id, :query, :string, "Filter by author account id"), Operation.parameter( :target_account_id, diff --git a/lib/pleroma/web/mastodon_api/admin/controllers/report_controller.ex b/lib/pleroma/web/mastodon_api/admin/controllers/report_controller.ex index 2d89d13bc..5dbf19aba 100644 --- a/lib/pleroma/web/mastodon_api/admin/controllers/report_controller.ex +++ b/lib/pleroma/web/mastodon_api/admin/controllers/report_controller.ex @@ -89,11 +89,46 @@ defmodule Pleroma.Web.MastodonAPI.Admin.ReportController do end end + def assign_to_self(%{assigns: %{user: admin}} = conn, %{id: id}) do + with {:ok, activity} <- CommonAPI.assign_report_to_account(id, admin.id), + report <- Activity.get_by_id_with_user_actor(activity.id) do + ModerationLog.insert_log(%{ + action: "report_assigned", + actor: admin, + subject: activity, + subject_actor: report.user_actor, + assigned_account: admin.nickname + }) + + render(conn, "show.json", Report.extract_report_info(report)) + else + {:error, error} -> + json_response(conn, :bad_request, %{error: error}) + end + end + + def unassign(%{assigns: %{user: admin}} = conn, %{id: id}) do + with {:ok, activity} <- CommonAPI.assign_report_to_account(id, nil), + report <- Activity.get_by_id_with_user_actor(activity.id) do + ModerationLog.insert_log(%{ + action: "report_unassigned", + actor: admin, + subject: activity, + subject_actor: report.user_actor + }) + + render(conn, "show.json", Report.extract_report_info(report)) + else + {:error, error} -> + json_response(conn, :bad_request, %{error: error}) + end + end + defp restrict_state(opts, %{resolved: true}), do: Map.put(opts, :state, "resolved") - defp restrict_state(opts, %{resolved: false}), do: Map.put(opts, :state, "open") + defp restrict_state(opts, %{resolved: _}), do: Map.put(opts, :state, "resolved") - defp restrict_state(opts, _params), do: opts + defp restrict_state(opts, _params), do: Map.put(opts, :state, "open") defp restrict_actor(opts, %{account_id: actor}) do with %User{ap_id: ap_id} <- User.get_by_id(actor) do diff --git a/test/pleroma/web/mastodon_api/admin/controllers/report_controller_test.exs b/test/pleroma/web/mastodon_api/admin/controllers/report_controller_test.exs index a0cc5f459..702dcf1db 100644 --- a/test/pleroma/web/mastodon_api/admin/controllers/report_controller_test.exs +++ b/test/pleroma/web/mastodon_api/admin/controllers/report_controller_test.exs @@ -50,7 +50,17 @@ defmodule Pleroma.Web.MastodonAPI.Admin.ReportControllerTest do assert [%{"id" => ^report_id1}] = conn - |> get("/api/v1/admin/reports?resolved=false") + |> get("/api/v1/admin/reports?unresolved=true") + |> json_response_and_validate_schema(200) + + assert [%{"id" => ^report_id1}] = + conn + |> get("/api/v1/admin/reports") + |> json_response_and_validate_schema(200) + + assert [%{"id" => ^report_id2}, %{"id" => ^report_id1}] = + conn + |> get("/api/v1/admin/reports?resolved=true&unresolved=true") |> json_response_and_validate_schema(200) assert [%{"id" => ^report_id2}] = From aec0deef8b3018ddcced65c7bd883514922b7539 Mon Sep 17 00:00:00 2001 From: Yonle Date: Tue, 5 May 2026 13:49:29 +0700 Subject: [PATCH 094/391] poll_view: try to read votersCount first, and then manually count local voters. --- lib/pleroma/web/mastodon_api/views/poll_view.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/poll_view.ex b/lib/pleroma/web/mastodon_api/views/poll_view.ex index 3b4271227..67f874b46 100644 --- a/lib/pleroma/web/mastodon_api/views/poll_view.ex +++ b/lib/pleroma/web/mastodon_api/views/poll_view.ex @@ -71,12 +71,12 @@ defmodule Pleroma.Web.MastodonAPI.PollView do end) end + defp voters_count(%{data: %{"votersCount" => voters}}), do: voters + defp voters_count(%{data: %{"voters" => [_ | _] = voters}}) do length(voters) end - defp voters_count(%{data: %{"votersCount" => voters}}), do: voters - defp voters_count(_), do: 0 defp voted_and_own_votes(%{object: object} = params, options) do From 3c63877e618468834a4ff31374396723dc8ba313 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 5 May 2026 21:04:13 +0200 Subject: [PATCH 095/391] Add custom Docker image for FE E2E tests Woodpecker cannot do custom commands for services. --- .woodpecker/docker.yaml | 12 ++++++ Dockerfile-e2e | 61 +++++++++++++++++++++++++++ changelog.d/docker-e2e-image.skip | 0 docker-entrypoint-e2e.sh | 70 +++++++++++++++++++++++++++++++ 4 files changed, 143 insertions(+) create mode 100644 Dockerfile-e2e create mode 100644 changelog.d/docker-e2e-image.skip create mode 100755 docker-entrypoint-e2e.sh diff --git a/.woodpecker/docker.yaml b/.woodpecker/docker.yaml index abc6bfa3b..9a5be78e2 100644 --- a/.woodpecker/docker.yaml +++ b/.woodpecker/docker.yaml @@ -62,6 +62,18 @@ steps: - stable-amd64 - ${CI_COMMIT_SHA:0:8}-amd64 + # FE workflow runs only on linux/amd64 + docker-stable-e2e-amd64: + image: *kaniko_image + when: + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' + settings: + <<: *docker_variables + dockerfile: Dockerfile-e2e + tags: + - stable-e2e + docker-stable-tag-amd64: image: *kaniko_image when: diff --git a/Dockerfile-e2e b/Dockerfile-e2e new file mode 100644 index 000000000..df8bb39aa --- /dev/null +++ b/Dockerfile-e2e @@ -0,0 +1,61 @@ +# https://hub.docker.com/r/hexpm/elixir/tags +ARG ELIXIR_IMG=docker.io/hexpm/elixir +ARG ALPINE_IMG=docker.io/alpine +ARG ELIXIR_VER=1.18.4 +ARG ERLANG_VER=27.3.4.11 +ARG ALPINE_VER=3.23.4 + +FROM ${ELIXIR_IMG}:${ELIXIR_VER}-erlang-${ERLANG_VER}-alpine-${ALPINE_VER} AS build + +COPY . . + +ENV MIX_ENV=prod +ENV VIX_COMPILATION_MODE=PLATFORM_PROVIDED_LIBVIPS + +RUN apk add git gcc g++ musl-dev make cmake file-dev vips-dev &&\ + echo "import Config" > config/prod.secret.exs &&\ + mix local.hex --force &&\ + mix local.rebar --force &&\ + mix deps.clean --all &&\ + mix deps.get --only prod &&\ + mkdir release &&\ + mix release --path release + +FROM ${ALPINE_IMG}:${ALPINE_VER} + +ARG BUILD_DATE +ARG VCS_REF + +LABEL maintainer="ops@pleroma.social" \ + org.opencontainers.image.title="pleroma" \ + org.opencontainers.image.description="Pleroma FE E2E test image" \ + org.opencontainers.image.authors="ops@pleroma.social" \ + org.opencontainers.image.vendor="pleroma.social" \ + org.opencontainers.image.documentation="https://git.pleroma.social/pleroma/pleroma" \ + org.opencontainers.image.licenses="AGPL-3.0" \ + org.opencontainers.image.url="https://pleroma.social" \ + org.opencontainers.image.revision=$VCS_REF \ + org.opencontainers.image.created=$BUILD_DATE + +ARG HOME=/opt/pleroma +ARG DATA=/var/lib/pleroma + +RUN apk update &&\ + apk add exiftool ffmpeg vips libmagic ncurses postgresql-client &&\ + adduser --system --shell /bin/false --home ${HOME} pleroma &&\ + mkdir -p ${DATA}/uploads &&\ + mkdir -p ${DATA}/static &&\ + chown -R pleroma ${DATA} &&\ + mkdir -p /etc/pleroma &&\ + chown -R pleroma /etc/pleroma + +USER pleroma + +COPY --from=build --chown=pleroma:0 /release ${HOME} + +COPY --chown=pleroma --chmod=640 ./config/docker.exs /etc/pleroma/config.exs +COPY ./docker-entrypoint-e2e.sh ${HOME} + +EXPOSE 4000 + +ENTRYPOINT ["/opt/pleroma/docker-entrypoint-e2e.sh"] diff --git a/changelog.d/docker-e2e-image.skip b/changelog.d/docker-e2e-image.skip new file mode 100644 index 000000000..e69de29bb diff --git a/docker-entrypoint-e2e.sh b/docker-entrypoint-e2e.sh new file mode 100755 index 000000000..c83c44a80 --- /dev/null +++ b/docker-entrypoint-e2e.sh @@ -0,0 +1,70 @@ +#!/bin/ash +set -eu + +SEED_SENTINEL_PATH=/var/lib/pleroma/.e2e_seeded +CONFIG_OVERRIDE_PATH=/var/lib/pleroma/config.exs + +echo '-- Waiting for database...' +while ! pg_isready -U ${DB_USER:-pleroma} -d postgres://${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-pleroma} -t 1; do + sleep 1s +done + +echo '-- Writing E2E config overrides...' +cat > $CONFIG_OVERRIDE_PATH </dev/null; then + kill -TERM $PLEROMA_PID + wait $PLEROMA_PID || true + fi +} + +trap cleanup INT TERM + +echo '-- Waiting for API...' +api_ok=false +for _i in $(seq 1 120); do + if wget -qO- http://127.0.0.1:4000/api/v1/instance >/dev/null 2>&1; then + api_ok=true + break + fi + sleep 1s +done + +if [ $api_ok != true ]; then + echo 'Timed out waiting for Pleroma API to become available' + exit 1 +fi + +if [ ! -f $SEED_SENTINEL_PATH ]; then + if [ -n ${E2E_ADMIN_USERNAME:-} ] && [ -n ${E2E_ADMIN_PASSWORD:-} ] && [ -n ${E2E_ADMIN_EMAIL:-} ]; then + echo '-- Seeding admin user' $E2E_ADMIN_USERNAME '...' + if ! /opt/pleroma/bin/pleroma_ctl user new $E2E_ADMIN_USERNAME $E2E_ADMIN_EMAIL --admin --password $E2E_ADMIN_PASSWORD -y; then + echo '-- User already exists or creation failed, ensuring admin + confirmed...' + /opt/pleroma/bin/pleroma_ctl user set $E2E_ADMIN_USERNAME --admin --confirmed + fi + else + echo '-- Skipping admin seeding (missing E2E_ADMIN_* env)' + fi + + touch $SEED_SENTINEL_PATH +fi + +wait $PLEROMA_PID From 727e9e77497f20e2b61bd6267555015d2c308696 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Wed, 6 May 2026 11:33:34 +0400 Subject: [PATCH 096/391] Fix votersCount inflation in multiple-choice polls increase_vote_count/3 was incrementing votersCount on every vote activity, causing inflation when a single voter picks multiple options. Now only increments when the actor is a new unique voter, and preserves existing votersCount otherwise. Also adds is_integer guard to voters_count/1 to handle nil safely, and adds tests for the voters_count clause ordering and edge cases. --- lib/pleroma/object.ex | 18 +- .../web/mastodon_api/views/poll_view.ex | 2 +- .../web/mastodon_api/views/poll_view_test.exs | 175 ++++++++++++++++++ 3 files changed, 189 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index f1e07a257..5e9314446 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -372,13 +372,21 @@ defmodule Pleroma.Object do option end) - voters = [actor | object.data["voters"] || []] |> Enum.uniq() + existing_voters = object.data["voters"] || [] + voters = [actor | existing_voters] |> Enum.uniq() + new_voter? = actor not in existing_voters + existing_voters_count = object.data["votersCount"] voters_count = - if Map.has_key?(object.data, "votersCount") do - object.data["votersCount"] + 1 - else - length(voters) + cond do + is_integer(existing_voters_count) and new_voter? -> + existing_voters_count + 1 + + is_integer(existing_voters_count) -> + existing_voters_count + + true -> + length(voters) end data = diff --git a/lib/pleroma/web/mastodon_api/views/poll_view.ex b/lib/pleroma/web/mastodon_api/views/poll_view.ex index 67f874b46..f047804e2 100644 --- a/lib/pleroma/web/mastodon_api/views/poll_view.ex +++ b/lib/pleroma/web/mastodon_api/views/poll_view.ex @@ -71,7 +71,7 @@ defmodule Pleroma.Web.MastodonAPI.PollView do end) end - defp voters_count(%{data: %{"votersCount" => voters}}), do: voters + defp voters_count(%{data: %{"votersCount" => voters}}) when is_integer(voters), do: voters defp voters_count(%{data: %{"voters" => [_ | _] = voters}}) do length(voters) diff --git a/test/pleroma/web/mastodon_api/views/poll_view_test.exs b/test/pleroma/web/mastodon_api/views/poll_view_test.exs index 16281393d..6cb5934de 100644 --- a/test/pleroma/web/mastodon_api/views/poll_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/poll_view_test.exs @@ -180,4 +180,179 @@ defmodule Pleroma.Web.MastodonAPI.PollViewTest do assert result[:pleroma][:non_anonymous] == true end + + test "prefers votersCount over voters list when both are present" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Which flavor?", + poll: %{options: ["chocolate", "vanilla"], expires_in: 20} + }) + + object = Object.normalize(activity, fetch: false) + + voter = insert(:user) + {:ok, _, object} = CommonAPI.vote(object, voter, [0]) + + assert object.data["votersCount"] == 1 + assert length(object.data["voters"]) == 1 + + object = %{ + object + | data: Map.put(object.data, "votersCount", 42) + } + + result = PollView.render("show.json", %{object: object}) + + assert result[:voters_count] == 42 + end + + test "falls back to voters list when votersCount is absent" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Which flavor?", + poll: %{options: ["chocolate", "vanilla"], expires_in: 20} + }) + + object = Object.normalize(activity, fetch: false) + + voter = insert(:user) + {:ok, _, object} = CommonAPI.vote(object, voter, [0]) + + assert length(object.data["voters"]) == 1 + + data = Map.delete(object.data, "votersCount") + object = %{object | data: data} + + result = PollView.render("show.json", %{object: object}) + + assert result[:voters_count] == 1 + end + + test "returns 0 when both votersCount and voters are absent" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Which flavor?", + poll: %{options: ["chocolate", "vanilla"], expires_in: 20} + }) + + object = Object.normalize(activity, fetch: false) + + data = + object.data + |> Map.delete("votersCount") + |> Map.delete("voters") + + object = %{object | data: data} + + result = PollView.render("show.json", %{object: object}) + + assert result[:voters_count] == 0 + end + + test "returns 0 when voters list is empty" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Which flavor?", + poll: %{options: ["chocolate", "vanilla"], expires_in: 20} + }) + + object = Object.normalize(activity, fetch: false) + + data = + object.data + |> Map.delete("votersCount") + |> Map.put("voters", []) + + object = %{object | data: data} + + result = PollView.render("show.json", %{object: object}) + + assert result[:voters_count] == 0 + end + + test "does not inflate votersCount when same voter picks multiple options" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Pick several", + poll: %{options: ["a", "b", "c"], expires_in: 20, multiple: true} + }) + + object = Object.normalize(activity, fetch: false) + + voter = insert(:user) + {:ok, _, object} = CommonAPI.vote(object, voter, [0, 2]) + + assert object.data["votersCount"] == 1 + assert length(object.data["voters"]) == 1 + end + + test "preserves votersCount from remote source when existing voter picks another option" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Pick several", + poll: %{options: ["a", "b"], expires_in: 20, multiple: true} + }) + + object = Object.normalize(activity, fetch: false) + + voter = insert(:user) + {:ok, _, object} = CommonAPI.vote(object, voter, [0, 1]) + + object = %{object | data: Map.put(object.data, "votersCount", 14)} + + result = PollView.render("show.json", %{object: object}) + + assert result[:voters_count] == 14 + end + + test "returns 0 when votersCount is explicitly 0" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Pick one", + poll: %{options: ["a", "b"], expires_in: 20} + }) + + object = Object.normalize(activity, fetch: false) + + object = %{object | data: Map.put(object.data, "votersCount", 0)} + + result = PollView.render("show.json", %{object: object}) + + assert result[:voters_count] == 0 + end + + test "falls back to voters list when votersCount is nil" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Pick one", + poll: %{options: ["a", "b"], expires_in: 20} + }) + + object = Object.normalize(activity, fetch: false) + + voter = insert(:user) + {:ok, _, object} = CommonAPI.vote(object, voter, [0]) + + object = %{object | data: Map.put(object.data, "votersCount", nil)} + + result = PollView.render("show.json", %{object: object}) + + assert result[:voters_count] == length(object.data["voters"]) + end end From c62d1919867b287338f497d089acc81cd5b9a31b Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Wed, 6 May 2026 11:48:20 +0400 Subject: [PATCH 097/391] Add changelog for votersCount inflation fix --- changelog.d/poll-voters-count-inflation.fix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/poll-voters-count-inflation.fix diff --git a/changelog.d/poll-voters-count-inflation.fix b/changelog.d/poll-voters-count-inflation.fix new file mode 100644 index 000000000..7eae41f13 --- /dev/null +++ b/changelog.d/poll-voters-count-inflation.fix @@ -0,0 +1 @@ +Fix votersCount inflation when same voter picks multiple options From 4873991983ca396a86a7a86c2b423ff29936afa2 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 6 May 2026 15:40:47 +0200 Subject: [PATCH 098/391] Update Pleroma-FE build artifacts URL --- changelog.d/pleroma-fe-link.fix | 1 + config/config.exs | 2 +- lib/pleroma/frontend.ex | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog.d/pleroma-fe-link.fix diff --git a/changelog.d/pleroma-fe-link.fix b/changelog.d/pleroma-fe-link.fix new file mode 100644 index 000000000..de93f86dd --- /dev/null +++ b/changelog.d/pleroma-fe-link.fix @@ -0,0 +1 @@ +Updated Pleroma-FE build URL after Forgejo migration diff --git a/config/config.exs b/config/config.exs index 5bf2c5c2e..82bdd7750 100644 --- a/config/config.exs +++ b/config/config.exs @@ -775,7 +775,7 @@ config :pleroma, :frontends, "name" => "pleroma-fe", "git" => "https://git.pleroma.social/pleroma/pleroma-fe", "build_url" => - "https://git.pleroma.social/pleroma/pleroma-fe/-/jobs/artifacts/${ref}/download?job=build", + "https://git.pleroma.social/api/packages/pleroma/generic/pleroma-fe-builds/${ref}/latest.zip", "ref" => "develop" }, "fedi-fe" => %{ diff --git a/lib/pleroma/frontend.ex b/lib/pleroma/frontend.ex index e651d7d9d..e37a3fefe 100644 --- a/lib/pleroma/frontend.ex +++ b/lib/pleroma/frontend.ex @@ -75,8 +75,8 @@ defmodule Pleroma.Frontend do end defp download_build(frontend_info, dest) do - Logger.info("Downloading pre-built bundle for #{frontend_info["name"]}") url = String.replace(frontend_info["build_url"], "${ref}", frontend_info["ref"]) + Logger.info("Downloading pre-built bundle for #{frontend_info["name"]} from #{url}") with {:ok, %{status: 200, body: zip_body}} <- Pleroma.HTTP.get(url, [], pool: :media, recv_timeout: 120_000) do From 6b86e31e5d9d5d6c2175c12c3ea086de101ea33b Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 11 May 2026 14:53:06 +0400 Subject: [PATCH 099/391] Add backend MFM support --- config/config.exs | 3 +- config/description.exs | 11 +- lib/pleroma/formatter.ex | 11 ++ .../article_note_page_validator.ex | 113 ++++++++++++ .../object_validators/common_fields.ex | 1 + lib/pleroma/web/activity_pub/utils.ex | 3 +- lib/pleroma/web/common_api/activity_draft.ex | 7 +- lib/pleroma/web/common_api/utils.ex | 18 ++ mix.exs | 3 + mix.lock | 1 + priv/scrubbers/default.ex | 42 ++++- test/mix/tasks/pleroma/config_test.exs | 8 +- .../article_note_page_validator_test.exs | 165 ++++++++++++++++++ test/pleroma/web/activity_pub/utils_test.exs | 6 +- test/pleroma/web/common_api_test.exs | 41 +++++ 15 files changed, 423 insertions(+), 10 deletions(-) diff --git a/config/config.exs b/config/config.exs index 82bdd7750..2d38e3ebe 100644 --- a/config/config.exs +++ b/config/config.exs @@ -203,7 +203,8 @@ config :pleroma, :instance, "text/plain", "text/html", "text/markdown", - "text/bbcode" + "text/bbcode", + "text/x.misskeymarkdown" ], autofollowed_nicknames: [], autofollowing_nicknames: [], diff --git a/config/description.exs b/config/description.exs index c388d17c3..6e4348907 100644 --- a/config/description.exs +++ b/config/description.exs @@ -815,7 +815,8 @@ config :pleroma, :config_description, [ "text/plain", "text/html", "text/markdown", - "text/bbcode" + "text/bbcode", + "text/x.misskeymarkdown" ] }, %{ @@ -1394,7 +1395,13 @@ config :pleroma, :config_description, [ label: "Post Content Type", type: {:dropdown, :atom}, description: "Default post formatting option", - suggestions: ["text/plain", "text/html", "text/markdown", "text/bbcode"] + suggestions: [ + "text/plain", + "text/html", + "text/markdown", + "text/bbcode", + "text/x.misskeymarkdown" + ] }, %{ key: :redirectRootNoLogin, diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex index 11d5af2fb..4bf2f6b95 100644 --- a/lib/pleroma/formatter.ex +++ b/lib/pleroma/formatter.ex @@ -127,6 +127,13 @@ defmodule Pleroma.Formatter do Earmark.as_html!(text, %Earmark.Options{compact_output: true, smartypants: false}) end + def markdown_to_html(text, opts) do + Earmark.as_html!( + text, + %Earmark.Options{compact_output: true, smartypants: false} |> Map.merge(opts) + ) + end + def html_escape({text, mentions, hashtags}, type) do {html_escape(text, type), mentions, hashtags} end @@ -135,6 +142,10 @@ defmodule Pleroma.Formatter do HTML.filter_tags(text) end + def html_escape(text, "text/x.misskeymarkdown") do + HTML.filter_tags(text) + end + def html_escape(text, "text/plain") do Regex.split(@link_regex, text, include_captures: true) |> Enum.map_every(2, fn chunk -> diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex index c0626ce4d..ec9259f95 100644 --- a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex @@ -6,6 +6,9 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do use Ecto.Schema alias Pleroma.EctoType.ActivityPub.ObjectValidators + alias Pleroma.HTML + alias Pleroma.User + alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations alias Pleroma.Web.ActivityPub.Transmogrifier @@ -26,6 +29,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do end field(:replies, {:array, ObjectValidators.ObjectID}, default: []) + field(:source, :map) end def cast_and_apply(data) do @@ -80,6 +84,113 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do def fix_attachments(data), do: data + defp remote_mention_resolver( + %{"id" => ap_id, "tag" => tags}, + "@" <> nickname = mention, + buffer, + opts, + acc + ) + when is_binary(ap_id) and is_list(tags) do + initial_host = + ap_id + |> URI.parse() + |> Map.get(:host) + + with mention_tag when not is_nil(mention_tag) <- + Enum.find(tags, &mention_tag?(&1, mention, initial_host)), + href when is_binary(href) <- mention_tag["href"], + %User{} = user <- User.get_cached_by_ap_id(href) do + link = Pleroma.Formatter.mention_from_user(user, opts) + {link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, user})}} + else + _ -> {buffer, acc} + end + end + + defp remote_mention_resolver(_object, _mention, buffer, _opts, acc), do: {buffer, acc} + + defp mention_tag?(%{"type" => "Mention", "name" => name}, mention, initial_host) + when is_binary(name) do + name == mention || mention == "#{name}@#{initial_host}" + end + + defp mention_tag?(_tag, _mention, _initial_host), do: false + + defp scrub_content(%{"content" => content} = object) when is_binary(content) do + Map.put(object, "content", HTML.filter_tags(content)) + end + + defp scrub_content(object), do: object + + defp mfm_parse_limit do + min(Pleroma.Config.get([:instance, :limit]), Pleroma.Config.get([:instance, :remote_limit])) + end + + defp normalize_source(%{"source" => source} = object) when is_binary(source) do + object + |> Map.put("source", %{"content" => source}) + |> normalize_source() + end + + defp normalize_source(%{"source" => source} = object) when is_map(source) do + source = + case source["content"] do + content when is_binary(content) -> + if String.length(content) <= mfm_parse_limit() do + source + else + Map.delete(source, "content") + end + + nil -> + source + + _ -> + Map.delete(source, "content") + end + + Map.put(object, "source", source) + end + + defp normalize_source(object), do: object + + defp fix_misskey_content(%{"htmlMfm" => true, "content" => content} = object) + when is_binary(content) do + Map.put(object, "content", HTML.filter_tags(content)) + end + + defp fix_misskey_content(%{"htmlMfm" => true} = object), do: object + + defp fix_misskey_content( + %{"source" => %{"mediaType" => "text/x.misskeymarkdown", "content" => content}} = object + ) + when is_binary(content) do + mention_handler = fn nick, buffer, opts, acc -> + remote_mention_resolver(object, nick, buffer, opts, acc) + end + + {linked, _mentions, _tags} = + Utils.format_input(content, "text/x.misskeymarkdown", mention_handler: mention_handler) + + Map.put(object, "content", linked) + end + + defp fix_misskey_content(%{"source" => %{"mediaType" => "text/x.misskeymarkdown"}} = object), + do: scrub_content(object) + + defp fix_misskey_content(%{"_misskey_content" => content} = object) when is_binary(content) do + object + |> Map.put("source", %{ + "content" => content, + "mediaType" => "text/x.misskeymarkdown" + }) + |> Map.delete("_misskey_content") + |> fix_misskey_content() + end + + defp fix_misskey_content(object), do: object + defp fix(data) do data |> CommonFixes.fix_actor() @@ -88,6 +199,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do |> fix_tag() |> fix_replies() |> fix_attachments() + |> normalize_source() + |> fix_misskey_content() |> CommonFixes.fix_quote_url() |> CommonFixes.fix_likes() |> Transmogrifier.fix_emoji() diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex index 22cf0cc05..9b8580200 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex @@ -32,6 +32,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do quote bind_quoted: binding() do field(:content, :string) field(:contentMap, ObjectValidators.ContentLanguageMap) + field(:htmlMfm, :boolean) field(:published, ObjectValidators.DateTime) field(:updated, ObjectValidators.DateTime) diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 43c0f456d..0af4ceaf5 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -120,7 +120,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do "https://www.w3.org/ns/activitystreams", "#{Endpoint.url()}/schemas/litepub-0.1.jsonld", %{ - "@language" => get_language(data) + "@language" => get_language(data), + "htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm" } ] } diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index 16489663a..6072fff6b 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -317,6 +317,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do emoji = Map.merge(emoji, summary_emoji) + media_type = Utils.get_content_type(draft.params[:content_type]) {:ok, note_data, _meta} = Builder.note(draft) object = @@ -324,14 +325,18 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do |> Map.put("emoji", emoji) |> Map.put("source", %{ "content" => draft.status, - "mediaType" => Utils.get_content_type(draft.params[:content_type]) + "mediaType" => media_type }) + |> maybe_put("htmlMfm", true, media_type == "text/x.misskeymarkdown") |> Map.put("generator", draft.params[:generator]) |> Map.put("language", draft.language) %{draft | object: object} end + defp maybe_put(map, key, value, true), do: Map.put(map, key, value) + defp maybe_put(map, _key, _value, _condition), do: map + defp preview?(%__MODULE__{} = draft) do preview? = Pleroma.Web.Utils.Params.truthy_param?(draft.params[:preview]) %{draft | preview?: preview?} diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 32572a721..26034d685 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -322,6 +322,14 @@ defmodule Pleroma.Web.CommonAPI.Utils do |> Formatter.linkify(options) end + def format_input(text, "text/x.misskeymarkdown", options) do + text + |> Formatter.markdown_to_html(%{breaks: true}) + |> safe_mfm_to_html() + |> Formatter.linkify(options) + |> Formatter.html_escape("text/x.misskeymarkdown") + end + def format_input(text, "text/markdown", options) do text |> Formatter.mentions_escape(options) @@ -330,6 +338,16 @@ defmodule Pleroma.Web.CommonAPI.Utils do |> Formatter.html_escape("text/html") end + defp safe_mfm_to_html(html) do + html + |> MfmParser.Parser.parse() + |> MfmParser.Encoder.to_html() + rescue + _ -> html + catch + _, _ -> html + end + def format_naive_asctime(date) do date |> DateTime.from_naive!("Etc/UTC") |> format_asctime end diff --git a/mix.exs b/mix.exs index bb7bfb9da..88f98d54e 100644 --- a/mix.exs +++ b/mix.exs @@ -160,6 +160,9 @@ defmodule Pleroma.Mixfile do {:sweet_xml, "~> 0.7.5"}, {:earmark, "1.4.46"}, {:bbcode_pleroma, "~> 0.2.0"}, + {:mfm_parser, + git: "https://akkoma.dev/AkkomaGang/mfm-parser.git", + ref: "360a30267a847810a63ab48f606ba227b2ca05f0"}, {:cors_plug, "~> 2.0"}, {:web_push_encryption, "~> 0.3.1"}, {:swoosh, "~> 1.16.12"}, diff --git a/mix.lock b/mix.lock index dd5f65f11..976a856b0 100644 --- a/mix.lock +++ b/mix.lock @@ -79,6 +79,7 @@ "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, + "mfm_parser": {:git, "https://akkoma.dev/AkkomaGang/mfm-parser.git", "360a30267a847810a63ab48f606ba227b2ca05f0", [ref: "360a30267a847810a63ab48f606ba227b2ca05f0"]}, "mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"}, "mimerl": {:hex, :mimerl, "1.5.0", "f35aca6f23242339b3666e0ac0702379e362b469d0aea167f6cc713547e777ed", [:rebar3], [], "hexpm", "db648ce065bae14ea84ca8b5dd123f42f49417cef693541110bf6f9e9be9ecc4"}, "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, diff --git a/priv/scrubbers/default.ex b/priv/scrubbers/default.ex index 0defdc74e..342ef9944 100644 --- a/priv/scrubbers/default.ex +++ b/priv/scrubbers/default.ex @@ -82,12 +82,50 @@ defmodule Pleroma.HTML.Scrubber.Default do "recipients-inline", "quote-inline", "invisible", - "ellipsis" + "ellipsis", + "mfm-center", + "mfm-flip", + "mfm-font", + "mfm-blur", + "mfm-rotate", + "mfm-x2", + "mfm-x3", + "mfm-x4", + "mfm-position", + "mfm-scale", + "mfm-fg", + "mfm-bg", + "mfm-jelly", + "mfm-twitch", + "mfm-shake", + "mfm-spin", + "mfm-jump", + "mfm-bounce", + "mfm-rainbow", + "mfm-tada", + "mfm-sparkle" ]) Meta.allow_tag_with_this_attribute_values(:p, "class", ["quote-inline"]) - Meta.allow_tag_with_these_attributes(:span, ["lang"]) + Meta.allow_tag_with_these_attributes(:span, [ + "lang", + "data-mfm-h", + "data-mfm-v", + "data-mfm-x", + "data-mfm-y", + "data-mfm-alternate", + "data-mfm-speed", + "data-mfm-deg", + "data-mfm-left", + "data-mfm-serif", + "data-mfm-monospace", + "data-mfm-cursive", + "data-mfm-fantasy", + "data-mfm-emoji", + "data-mfm-math", + "data-mfm-color" + ]) Meta.allow_tag_with_this_attribute_values(:code, "class", ["inline"]) diff --git a/test/mix/tasks/pleroma/config_test.exs b/test/mix/tasks/pleroma/config_test.exs index ef1adc235..f672d8c13 100644 --- a/test/mix/tasks/pleroma/config_test.exs +++ b/test/mix/tasks/pleroma/config_test.exs @@ -144,7 +144,13 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do quarantined_instances: [], managed_config: true, static_dir: "instance/static/", - allowed_post_formats: ["text/plain", "text/html", "text/markdown", "text/bbcode"], + allowed_post_formats: [ + "text/plain", + "text/html", + "text/markdown", + "text/bbcode", + "text/x.misskeymarkdown" + ], autofollowed_nicknames: [], max_pinned_statuses: 1, attachment_links: false, diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs index c32811c5b..bf9c70fb6 100644 --- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs @@ -149,6 +149,171 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest %{valid?: true} = ArticleNotePageValidator.cast_and_validate(note) end + test "a Misskey MFM note is rendered from source content" do + user = insert(:user, ap_id: "https://misskey.example/users/alice") + + note = %{ + "id" => "https://misskey.example/notes/1", + "type" => "Note", + "actor" => user.ap_id, + "attributedTo" => user.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "content" => "original content", + "context" => Utils.generate_context_id(), + "source" => %{ + "content" => "$[spin.speed=1s mfm goes here] ", + "mediaType" => "text/x.misskeymarkdown" + } + } + + %{valid?: true, changes: %{content: content, source: source}} = + ArticleNotePageValidator.cast_and_validate(note) + + assert source["mediaType"] == "text/x.misskeymarkdown" + assert content =~ ~s(class="mfm-spin") + assert content =~ ~s(data-mfm-speed="1s") + assert content =~ "mfm goes here" + refute content =~ "original content" + refute content =~ " "https://misskey.example/notes/3", + "type" => "Note", + "actor" => remote_user.ap_id, + "attributedTo" => remote_user.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "content" => "original content", + "context" => Utils.generate_context_id(), + "tag" => [ + %{ + "type" => "Mention", + "name" => "@local_user", + "href" => local_user.ap_id + }, + %{ + "type" => "Mention", + "name" => "@uncached", + "href" => "https://misskey.example/users/uncached" + } + ], + "source" => %{ + "content" => "@local_user @uncached $[spin hello]", + "mediaType" => "text/x.misskeymarkdown" + } + } + + %{valid?: true, changes: %{content: content}} = + ArticleNotePageValidator.cast_and_validate(note) + + assert content =~ local_user.ap_id + assert content =~ "@uncached" + end + + test "a Misskey MFM note drops oversized source content instead of parsing it" do + user = insert(:user, ap_id: "https://misskey.example/users/oversized") + + note = %{ + "id" => "https://misskey.example/notes/4", + "type" => "Note", + "actor" => user.ap_id, + "attributedTo" => user.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "content" => "safe fallback", + "context" => Utils.generate_context_id(), + "source" => %{ + "content" => String.duplicate("x", 5_001), + "mediaType" => "text/x.misskeymarkdown" + } + } + + %{valid?: true, changes: %{content: content, source: source}} = + ArticleNotePageValidator.cast_and_validate(note) + + assert content == "safe fallback" + refute Map.has_key?(source, "content") + end + + test "a note drops oversized non-MFM source content" do + user = insert(:user, ap_id: "https://example.com/users/source") + + note = %{ + "id" => "https://example.com/notes/1", + "type" => "Note", + "actor" => user.ap_id, + "attributedTo" => user.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "content" => "regular content", + "context" => Utils.generate_context_id(), + "source" => %{ + "content" => String.duplicate("x", 5_001), + "mediaType" => "text/markdown" + } + } + + %{valid?: true, changes: %{source: source}} = ArticleNotePageValidator.cast_and_validate(note) + + assert source == %{"mediaType" => "text/markdown"} + end + + test "a Misskey MFM note with legacy _misskey_content is rendered" do + user = insert(:user, ap_id: "https://misskey.example/users/legacy") + + note = %{ + "id" => "https://misskey.example/notes/5", + "type" => "Note", + "actor" => user.ap_id, + "attributedTo" => user.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "content" => "original content", + "context" => Utils.generate_context_id(), + "_misskey_content" => "$[spin legacy]" + } + + %{valid?: true, changes: %{content: content, source: source}} = + ArticleNotePageValidator.cast_and_validate(note) + + assert source == %{"content" => "$[spin legacy]", "mediaType" => "text/x.misskeymarkdown"} + assert content =~ ~s(class="mfm-spin") + assert content =~ "legacy" + end + + test "a Misskey MFM note with htmlMfm is scrubbed but not rendered from source content" do + user = insert(:user, ap_id: "https://misskey.example/users/bob") + + note = %{ + "id" => "https://misskey.example/notes/2", + "type" => "Note", + "actor" => user.ap_id, + "attributedTo" => user.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "content" => + "already rendered", + "htmlMfm" => true, + "context" => Utils.generate_context_id(), + "source" => %{ + "content" => String.duplicate("x", 5_001), + "mediaType" => "text/x.misskeymarkdown" + } + } + + %{valid?: true, changes: %{content: content, htmlMfm: true, source: source}} = + ArticleNotePageValidator.cast_and_validate(note) + + assert content == "already renderedalert('xss')" + refute Map.has_key?(source, "content") + end + test "a Note with validated likes collection validates" do insert(:user, ap_id: "https://pol.social/users/mkljczk") diff --git a/test/pleroma/web/activity_pub/utils_test.exs b/test/pleroma/web/activity_pub/utils_test.exs index 3b77f0867..93234a015 100644 --- a/test/pleroma/web/activity_pub/utils_test.exs +++ b/test/pleroma/web/activity_pub/utils_test.exs @@ -180,7 +180,8 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do "https://www.w3.org/ns/activitystreams", "http://localhost:4001/schemas/litepub-0.1.jsonld", %{ - "@language" => "und" + "@language" => "und", + "htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm" } ] } @@ -192,7 +193,8 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do "https://www.w3.org/ns/activitystreams", "http://localhost:4001/schemas/litepub-0.1.jsonld", %{ - "@language" => "pl" + "@language" => "pl", + "htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm" } ] } diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index 017fac696..ea1795c0b 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -709,6 +709,47 @@ defmodule Pleroma.Web.CommonAPITest do assert object.data["source"]["content"] == post end + test "it renders MFM posts and marks their ActivityPub representation" do + user = insert(:user) + + post = "

$[spin.speed=1s 13:37]

" + + {:ok, activity} = + CommonAPI.post(user, %{ + status: post, + content_type: "text/x.misskeymarkdown" + }) + + object = Object.normalize(activity, fetch: false) + + assert object.data["htmlMfm"] == true + + assert object.data["source"] == %{ + "content" => post, + "mediaType" => "text/x.misskeymarkdown" + } + + assert object.data["content"] =~ ~s(class="mfm-spin") + assert object.data["content"] =~ ~s(data-mfm-speed="1s") + assert object.data["content"] =~ "13:37" + refute object.data["content"] =~ "scrub-this" + end + + test "it falls back safely for malformed MFM" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "$[spin.speed=1s=boom malformed]", + content_type: "text/x.misskeymarkdown" + }) + + object = Object.normalize(activity, fetch: false) + + refute object.data["content"] =~ ~s(class="mfm-spin") + assert object.data["content"] =~ "malformed" + end + test "it does not allow replies to direct messages that are not direct messages themselves" do user = insert(:user) From c780298ce71577a1bffb755d6f183f049e6edf6e Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 11 May 2026 15:05:00 +0400 Subject: [PATCH 100/391] Add changelog for MFM support --- changelog.d/mfm-backend.add | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/mfm-backend.add diff --git a/changelog.d/mfm-backend.add b/changelog.d/mfm-backend.add new file mode 100644 index 000000000..f815c6828 --- /dev/null +++ b/changelog.d/mfm-backend.add @@ -0,0 +1 @@ +Add backend support for Misskey Markdown (MFM) posts From 47021b5aba4151284aefeeddbc8663ac80d5e19b Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 11 May 2026 16:37:59 +0400 Subject: [PATCH 101/391] Fix MFM validator alias ordering --- .../object_validators/article_note_page_validator.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex index ec9259f95..844ad2c8f 100644 --- a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex @@ -8,10 +8,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.HTML alias Pleroma.User - alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.CommonAPI.Utils import Ecto.Changeset From 592be493c825e324b9a161e984ad39d412575184 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 11 May 2026 19:12:03 +0400 Subject: [PATCH 102/391] Use published Pleroma MFM parser package --- mix.exs | 4 +--- mix.lock | 2 +- test/pleroma/web/common_api_test.exs | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/mix.exs b/mix.exs index 88f98d54e..db9a9b5ae 100644 --- a/mix.exs +++ b/mix.exs @@ -160,9 +160,7 @@ defmodule Pleroma.Mixfile do {:sweet_xml, "~> 0.7.5"}, {:earmark, "1.4.46"}, {:bbcode_pleroma, "~> 0.2.0"}, - {:mfm_parser, - git: "https://akkoma.dev/AkkomaGang/mfm-parser.git", - ref: "360a30267a847810a63ab48f606ba227b2ca05f0"}, + {:pleroma_mfm_parser, "~> 0.2.1"}, {:cors_plug, "~> 2.0"}, {:web_push_encryption, "~> 0.3.1"}, {:swoosh, "~> 1.16.12"}, diff --git a/mix.lock b/mix.lock index 976a856b0..af566967c 100644 --- a/mix.lock +++ b/mix.lock @@ -79,7 +79,6 @@ "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, - "mfm_parser": {:git, "https://akkoma.dev/AkkomaGang/mfm-parser.git", "360a30267a847810a63ab48f606ba227b2ca05f0", [ref: "360a30267a847810a63ab48f606ba227b2ca05f0"]}, "mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"}, "mimerl": {:hex, :mimerl, "1.5.0", "f35aca6f23242339b3666e0ac0702379e362b469d0aea167f6cc713547e777ed", [:rebar3], [], "hexpm", "db648ce065bae14ea84ca8b5dd123f42f49417cef693541110bf6f9e9be9ecc4"}, "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, @@ -113,6 +112,7 @@ "phoenix_swoosh": {:hex, :phoenix_swoosh, "1.2.1", "b74ccaa8046fbc388a62134360ee7d9742d5a8ae74063f34eb050279de7a99e1", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "4000eeba3f9d7d1a6bf56d2bd56733d5cadf41a7f0d8ffe5bb67e7d667e204a2"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, + "pleroma_mfm_parser": {:hex, :pleroma_mfm_parser, "0.2.1", "87a40fec4e2e035447df34e1c98e1b816b6a448d49baea73eb2273531ac5ce66", [:mix], [], "hexpm", "ec4845461da95d63ab3d592086d65114c45057a1f2d48d768f3def45b0d7696f"}, "plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"}, "plug_cowboy": {:hex, :plug_cowboy, "2.7.4", "729c752d17cf364e2b8da5bdb34fb5804f56251e88bb602aff48ae0bd8673d11", [: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", "9b85632bd7012615bae0a5d70084deb1b25d2bcbb32cab82d1e9a1e023168aa3"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index ea1795c0b..5d0ad4572 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -740,7 +740,7 @@ defmodule Pleroma.Web.CommonAPITest do {:ok, activity} = CommonAPI.post(user, %{ - status: "$[spin.speed=1s=boom malformed]", + status: "$[spin malformed", content_type: "text/x.misskeymarkdown" }) From 61feb3dfcdebf29186e214a92bd62c6cb1ac9583 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 11 May 2026 21:01:59 +0400 Subject: [PATCH 103/391] Update http_signatures to 0.1.3 --- mix.exs | 2 +- mix.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index db9a9b5ae..9c10cacc0 100644 --- a/mix.exs +++ b/mix.exs @@ -173,7 +173,7 @@ defmodule Pleroma.Mixfile do {:timex, "~> 3.7"}, {:ueberauth, "~> 0.10"}, {:linkify, "~> 0.5.3"}, - {:http_signatures, "~> 0.1.2"}, + {:http_signatures, "~> 0.1.3"}, {:telemetry, "~> 1.0.0", override: true}, {:poolboy, "~> 1.5"}, {:prom_ex, "~> 1.9"}, diff --git a/mix.lock b/mix.lock index af566967c..da0c1c60c 100644 --- a/mix.lock +++ b/mix.lock @@ -62,7 +62,7 @@ "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, - "http_signatures": {:hex, :http_signatures, "0.1.2", "ed1cc7043abcf5bb4f30d68fb7bad9d618ec1a45c4ff6c023664e78b67d9c406", [:mix], [], "hexpm", "f08aa9ac121829dae109d608d83c84b940ef2f183ae50f2dd1e9a8bc619d8be7"}, + "http_signatures": {:hex, :http_signatures, "0.1.3", "19f26aee35b4684e37efdce3ac4638605e6e41a04368186bd39d2b6138a60ea9", [:mix], [{:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "20313a65516db88006f85b090f6f76cc5b04e9609b45943657e6781eb91174f4"}, "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "inet_cidr": {:hex, :inet_cidr, "1.0.8", "d26bb7bdbdf21ae401ead2092bf2bb4bf57fe44a62f5eaa5025280720ace8a40", [:mix], [], "hexpm", "d5b26da66603bb56c933c65214c72152f0de9a6ea53618b56d63302a68f6a90e"}, From 7f4890b6a9beb60c8daeb59a54d19113386ebdd8 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 11 May 2026 21:12:34 +0400 Subject: [PATCH 104/391] Add changelog for http_signatures update --- changelog.d/http-signatures-0.1.3.fix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/http-signatures-0.1.3.fix diff --git a/changelog.d/http-signatures-0.1.3.fix b/changelog.d/http-signatures-0.1.3.fix new file mode 100644 index 000000000..3be1b9e9f --- /dev/null +++ b/changelog.d/http-signatures-0.1.3.fix @@ -0,0 +1 @@ +Fix compatibility with timestamped HTTP Signatures used by GoToSocial From ab9fd33762cb8a0cada9e526296d1e96f039b10b Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 11 May 2026 22:10:08 +0400 Subject: [PATCH 105/391] Fix Phoenix upstream migration regressions --- lib/pleroma/web.ex | 9 ++++++++ .../web/mastodon_api/websocket_handler.ex | 22 ++++++++++++------- .../integration/mastodon_websocket_test.exs | 20 +++++++++++++++++ .../static_fe/static_fe_controller_test.exs | 9 ++++++++ 4 files changed, 52 insertions(+), 8 deletions(-) diff --git a/lib/pleroma/web.ex b/lib/pleroma/web.ex index bf7fa86a3..70e7f6458 100644 --- a/lib/pleroma/web.ex +++ b/lib/pleroma/web.ex @@ -39,6 +39,15 @@ defmodule Pleroma.Web do alias Pleroma.Web.Router.Helpers, as: Routes + plug(:set_put_layout) + + defp set_put_layout(conn, _) do + case Pleroma.Config.get(:app_layout, "app.html") do + false -> put_layout(conn, false) + layout -> put_layout(conn, {Pleroma.Web.LayoutView, layout}) + end + end + # Marks plugs intentionally skipped and blocks their execution if present in plugs chain defp skip_plug(conn, plug_modules) do plug_modules diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex index 616f35912..2b698bd5d 100644 --- a/lib/pleroma/web/mastodon_api/websocket_handler.ex +++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex @@ -245,14 +245,20 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do Plug.Conn.send_resp(conn, 404, "Not Found") end - defp find_access_token(%{params: %{"access_token" => token}}), do: token + defp find_access_token(%{connect_info: %{sec_websocket_headers: sec_headers}} = transport_info) do + find_sec_websocket_protocol(sec_headers) || find_access_token_from_params(transport_info) + end - defp find_access_token(%{connect_info: %{sec_websocket_headers: sec_headers}}), - do: - Enum.find_value(sec_headers, fn - {"sec-websocket-protocol", v} -> v - _ -> nil - end) + defp find_access_token(transport_info), do: find_access_token_from_params(transport_info) - defp find_access_token(_), do: nil + defp find_sec_websocket_protocol(sec_headers) do + Enum.find_value(sec_headers, fn + {"sec-websocket-protocol", token} -> token + _ -> nil + end) + end + + defp find_access_token_from_params(%{params: %{"access_token" => token}}), do: token + + defp find_access_token_from_params(_), do: nil end diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs index 47f6f5f76..604382be2 100644 --- a/test/pleroma/integration/mastodon_websocket_test.exs +++ b/test/pleroma/integration/mastodon_websocket_test.exs @@ -279,6 +279,26 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do end) end + test "accepts Phoenix 1.8 sec-websocket headers connect info before query params", %{ + token: token, + user: user + } do + assert {:ok, state} = + Pleroma.Web.MastodonAPI.WebsocketHandler.connect(%{ + params: %{"stream" => "user", "access_token" => "invalid"}, + connect_info: %{ + sec_websocket_headers: [ + {"sec-websocket-version", "13"}, + {"sec-websocket-protocol", token.token} + ] + } + }) + + assert state.user.id == user.id + assert state.oauth_token.id == token.id + assert state.topics != [] + end + test "accepts valid token on client-sent event", %{token: token} do assert {:ok, pid} = start_socket() diff --git a/test/pleroma/web/static_fe/static_fe_controller_test.exs b/test/pleroma/web/static_fe/static_fe_controller_test.exs index 2fae83305..68ded6906 100644 --- a/test/pleroma/web/static_fe/static_fe_controller_test.exs +++ b/test/pleroma/web/static_fe/static_fe_controller_test.exs @@ -28,6 +28,15 @@ defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do assert html_response(conn, 200) =~ user.nickname end + test "renders profile HTML inside the default app layout", %{conn: conn, user: user} do + conn = get(conn, "/users/#{user.nickname}") + + html = html_response(conn, 200) + assert html =~ "" + assert html =~ ~s(class="instance-header") + assert html =~ ~s() + end + test "404 when user not found", %{conn: conn} do conn = get(conn, "/users/limpopo") From 8a56cf5c0fe58904ad2e6ae03ee7cddf7d2adc09 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 11 May 2026 22:19:06 +0400 Subject: [PATCH 106/391] Clarify websocket token precedence test --- test/pleroma/integration/mastodon_websocket_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs index 604382be2..078bb643c 100644 --- a/test/pleroma/integration/mastodon_websocket_test.exs +++ b/test/pleroma/integration/mastodon_websocket_test.exs @@ -279,7 +279,7 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do end) end - test "accepts Phoenix 1.8 sec-websocket headers connect info before query params", %{ + test "prefers sec-websocket-protocol token over query access_token", %{ token: token, user: user } do From 71afba4825eaa009e506a74e11dfece99dc08e39 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Tue, 12 May 2026 08:52:42 +0400 Subject: [PATCH 107/391] Signature: Treat HTTP signature errors as invalid --- lib/pleroma/signature.ex | 2 +- test/pleroma/signature_test.exs | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex index fca61799b..382cf9db3 100644 --- a/lib/pleroma/signature.ex +++ b/lib/pleroma/signature.ex @@ -104,7 +104,7 @@ defmodule Pleroma.Signature do |> put_req_header("(request-target)", request_target) |> put_req_header("@request-target", request_target) - @http_signatures_impl.validate_conn(conn) + @http_signatures_impl.validate_conn(conn) == true end @spec validate_signature(Plug.Conn.t()) :: boolean() diff --git a/test/pleroma/signature_test.exs b/test/pleroma/signature_test.exs index 572d7acc3..0c7c4c840 100644 --- a/test/pleroma/signature_test.exs +++ b/test/pleroma/signature_test.exs @@ -11,6 +11,7 @@ defmodule Pleroma.SignatureTest do import Mock alias Pleroma.Signature + alias Pleroma.StubbedHTTPSignaturesMock, as: HTTPSignaturesMock setup do mock(fn env -> apply(HttpRequestMock, :request, [env]) end) @@ -103,6 +104,18 @@ defmodule Pleroma.SignatureTest do end end + describe "validate_signature/1" do + test "treats HTTP signature errors as failed validation" do + conn = %Plug.Conn{method: "GET", request_path: "/inbox", req_headers: []} + + Mox.expect(HTTPSignaturesMock, :validate_conn, fn _conn -> + {:error, :request_target_header} + end) + + assert Signature.validate_signature(conn) == false + end + end + describe "key_id_to_actor_id/1" do test "it properly deduces the actor id for misskey" do assert Signature.key_id_to_actor_id("https://example.com/users/1234/publickey") == From ea886dc36b3727d7bd22ede1bdb90de3dfb8668e Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 12 May 2026 11:37:01 +0200 Subject: [PATCH 108/391] EnsureHostMatchesPlug: Ensure Host header matches instance URI --- changelog.d/host-header-verification.security | 1 + .../web/plugs/ensure_host_matches_plug.ex | 68 +++++++++++++++++++ lib/pleroma/web/router.ex | 1 + 3 files changed, 70 insertions(+) create mode 100644 changelog.d/host-header-verification.security create mode 100644 lib/pleroma/web/plugs/ensure_host_matches_plug.ex diff --git a/changelog.d/host-header-verification.security b/changelog.d/host-header-verification.security new file mode 100644 index 000000000..11a0784b2 --- /dev/null +++ b/changelog.d/host-header-verification.security @@ -0,0 +1 @@ +Ensure Host header is present and matches instance URI diff --git a/lib/pleroma/web/plugs/ensure_host_matches_plug.ex b/lib/pleroma/web/plugs/ensure_host_matches_plug.ex new file mode 100644 index 000000000..87c4c0531 --- /dev/null +++ b/lib/pleroma/web/plugs/ensure_host_matches_plug.ex @@ -0,0 +1,68 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2026 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.EnsureHostMatchesPlug do + @moduledoc "Ensures Host header matches instance" + + alias Pleroma.Web.Endpoint + + import Plug.Conn + + def init(options), do: options + + @spec call(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t() + def call(%Plug.Conn{assigns: %{valid_signature: true}} = conn, _opts) do + # Host header is scheme-less, URI.parse needs the // + host_header = get_req_header(conn, "host") + host_uri = URI.parse("//#{host_header}") + instance_uri = URI.parse(Endpoint.url()) + instance_scheme_port = URI.default_port(instance_uri.scheme) + + case host_header do + [host] -> + cond do + host == "" -> + resp(conn, 400, "Host header not provided") |> halt() + + true -> + if host_matches?(host_uri, instance_uri, instance_scheme_port), + do: assign(conn, :valid_host_header, true), + else: resp(conn, 400, "Host header does not match this instance") |> halt() + end + + [_head | _rest] -> + conn + |> resp(400, "More than one Host header provided") + |> halt() + + [] -> + conn + |> resp(400, "Host header not provided") + |> halt() + end + end + + # Host header may not be provided, but signature verification failed anyway + def call(conn, _opts), do: conn + + defp case_insensitive_compare(checked, authority) do + String.downcase(checked) == String.downcase(authority) + end + + # Host header did not provide port + # Host header is scheme-less, URI.parse does not provide default port + defp host_matches?(%URI{host: req_host, port: nil}, %URI{host: instance_host}, _), + do: case_insensitive_compare(req_host, instance_host) + + # Host header provided a port, reverse proxy configuration (port cannot match Endpoint port) + # Both port 80 and 443 are valid based on Endpoint configuration + defp host_matches?(%URI{host: req_host, port: port}, %URI{host: instance_host}, port), + do: case_insensitive_compare(req_host, instance_host) + + # Host header provided port, configuration without reverse proxy (port matches Endpoint port) + defp host_matches?(%URI{host: req_host, port: port}, %URI{host: instance_host, port: port}, _), + do: case_insensitive_compare(req_host, instance_host) + + defp host_matches?(_, _, _), do: false +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index c91ca8c97..813c5c482 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -216,6 +216,7 @@ defmodule Pleroma.Web.Router do pipeline :http_signature do plug(Pleroma.Web.Plugs.HTTPSignaturePlug) plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug) + plug(Pleroma.Web.Plugs.EnsureHostMatchesPlug) end pipeline :inbox_guard do From d6d0ce72604ee450560916bb29297188579faa0e Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 12 May 2026 14:00:28 +0200 Subject: [PATCH 109/391] EnsureHostMatchesPlug: Add tests --- .../plugs/ensure_host_matches_plug_test.exs | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 test/pleroma/web/plugs/ensure_host_matches_plug_test.exs diff --git a/test/pleroma/web/plugs/ensure_host_matches_plug_test.exs b/test/pleroma/web/plugs/ensure_host_matches_plug_test.exs new file mode 100644 index 000000000..fbebc21d9 --- /dev/null +++ b/test/pleroma/web/plugs/ensure_host_matches_plug_test.exs @@ -0,0 +1,123 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2026 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.EnsureHostMatchesPlugTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Web.Endpoint + alias Pleroma.Web.Plugs.EnsureHostMatchesPlug + + import Mock + import Plug.Conn + import Tesla.Mock + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + defp set_host(conn, host), do: %{conn | req_headers: conn.req_headers ++ [{"host", host}]} + + describe "EnsureHostMatchesPlug" do + setup do + conn = build_conn(:post, "/cofe") |> assign(:valid_signature, true) + [conn: conn] + end + + test "gracefully handles no Host header", %{conn: conn} do + conn = EnsureHostMatchesPlug.call(conn, %{}) + + assert conn.status == 400 + assert conn.halted == true + assert conn.resp_body == "Host header not provided" + end + + test "gracefully handles empty Host header", %{conn: conn} do + conn = + conn + |> set_host("") + |> EnsureHostMatchesPlug.call(%{}) + + assert conn.status == 400 + assert conn.halted == true + assert conn.resp_body == "Host header not provided" + end + + test "it rejects Host header not matching Endpoint URL", %{conn: conn} do + conn = + conn + |> set_host("invalid.example.com") + |> EnsureHostMatchesPlug.call(%{}) + + assert conn.status == 400 + assert conn.halted == true + assert conn.resp_body == "Host header does not match this instance" + end + + test "it rejects Host header not matching Endpoint port", %{conn: conn} do + endpoint = URI.parse(Endpoint.url()) + + conn = + conn + |> set_host("#{endpoint.host}:25") + |> EnsureHostMatchesPlug.call(%{}) + + assert conn.status == 400 + assert conn.halted == true + assert conn.resp_body == "Host header does not match this instance" + end + + test "it rejects multiple Host headers", %{conn: conn} do + conn = + conn + |> set_host("host1.example.com") + |> set_host("host2.example.com") + |> EnsureHostMatchesPlug.call(%{}) + + assert conn.status == 400 + assert conn.halted == true + assert conn.resp_body == "More than one Host header provided" + end + + test "it works for Host header with port as 80", %{conn: conn} do + endpoint = URI.parse(Endpoint.url()) + + conn = + conn + |> set_host("#{endpoint.host}:80") + |> EnsureHostMatchesPlug.call(%{}) + + assert conn.halted == false + assert Map.get(conn.assigns, :valid_host_header, nil) + end + + test "it works for Host header with port as 443", %{conn: conn} do + with_mock Pleroma.Web.Endpoint, url: fn -> "https://localhost:4001" end do + endpoint = URI.parse(Endpoint.url()) + + conn = + conn + |> set_host("#{endpoint.host}:443") + |> EnsureHostMatchesPlug.call(%{}) + + assert conn.halted == false + assert Map.get(conn.assigns, :valid_host_header, nil) + end + end + + test "it works for Host header with port as same as Endpoint (no reverse proxy config)", %{ + conn: conn + } do + endpoint = URI.parse(Endpoint.url()) + + conn = + conn + |> set_host("#{endpoint.host}:#{endpoint.port}") + |> EnsureHostMatchesPlug.call(%{}) + + assert conn.halted == false + assert Map.get(conn.assigns, :valid_host_header, nil) + end + end +end From 90e390e45b1c1af4bbc8e2e68affb41fd621ea3e Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 12 May 2026 16:48:49 +0200 Subject: [PATCH 110/391] fix tests --- test/support/conn_case.ex | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index f010fec33..c01516169 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -119,7 +119,10 @@ defmodule Pleroma.Web.ConnCase do DataCase.stub_pipeline() Mox.verify_on_exit!() + endpoint = URI.parse(Pleroma.Web.Endpoint.url()) + conn = Phoenix.ConnTest.build_conn() + conn = %{conn | req_headers: [{"host", "#{endpoint.host}:#{endpoint.port}"}]} - {:ok, conn: Phoenix.ConnTest.build_conn()} + {:ok, conn: conn} end end From 35b5447f3f21a0a30370c86efffee531f56f1ea7 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 12 May 2026 17:02:28 +0200 Subject: [PATCH 111/391] EnsureHostMatchesPlug: Add more tests --- .../plugs/ensure_host_matches_plug_test.exs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/pleroma/web/plugs/ensure_host_matches_plug_test.exs b/test/pleroma/web/plugs/ensure_host_matches_plug_test.exs index fbebc21d9..67252e7a7 100644 --- a/test/pleroma/web/plugs/ensure_host_matches_plug_test.exs +++ b/test/pleroma/web/plugs/ensure_host_matches_plug_test.exs @@ -55,6 +55,19 @@ defmodule Pleroma.Web.Plugs.EnsureHostMatchesPlugTest do assert conn.resp_body == "Host header does not match this instance" end + test "it rejects Host header not matching Endpoint with port", %{conn: conn} do + endpoint = URI.parse(Endpoint.url()) + + conn = + conn + |> set_host("invalid.example.com:#{endpoint.port}") + |> EnsureHostMatchesPlug.call(%{}) + + assert conn.status == 400 + assert conn.halted == true + assert conn.resp_body == "Host header does not match this instance" + end + test "it rejects Host header not matching Endpoint port", %{conn: conn} do endpoint = URI.parse(Endpoint.url()) @@ -80,6 +93,18 @@ defmodule Pleroma.Web.Plugs.EnsureHostMatchesPlugTest do assert conn.resp_body == "More than one Host header provided" end + test "it works for Host header without port", %{conn: conn} do + endpoint = URI.parse(Endpoint.url()) + + conn = + conn + |> set_host("#{endpoint.host}") + |> EnsureHostMatchesPlug.call(%{}) + + assert conn.halted == false + assert Map.get(conn.assigns, :valid_host_header, nil) + end + test "it works for Host header with port as 80", %{conn: conn} do endpoint = URI.parse(Endpoint.url()) From 0cf865f0254f33fba312a9342c635b7300ea0291 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Tue, 12 May 2026 23:50:30 +0400 Subject: [PATCH 112/391] Reject third-party remote reports --- changelog.d/reject-third-party-reports.fix | 1 + lib/pleroma/web/activity_pub/transmogrifier.ex | 7 +++++++ .../web/activity_pub/transmogrifier_test.exs | 17 +++++++++++++++++ 3 files changed, 25 insertions(+) create mode 100644 changelog.d/reject-third-party-reports.fix diff --git a/changelog.d/reject-third-party-reports.fix b/changelog.d/reject-third-party-reports.fix new file mode 100644 index 000000000..7d4e87b94 --- /dev/null +++ b/changelog.d/reject-third-party-reports.fix @@ -0,0 +1 @@ +Reject incoming reports when both the reporter and reported account are remote diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 4421da26c..261272d08 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -430,6 +430,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end) end + defp reject_third_party_report(%User{local: false}, %User{local: false} = account) do + {:reject, "[Transmogrifier] third-party report: #{account.ap_id}"} + end + + defp reject_third_party_report(_, _), do: :ok + def handle_incoming(data, options \\ []) do data |> fix_recursive(&strip_internal_fields/1) @@ -447,6 +453,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do %User{} = actor <- User.get_cached_by_ap_id(actor), # Reduce the object list to find the reported user. %User{} = account <- get_reported(objects), + :ok <- reject_third_party_report(actor, account), # Remove the reported user from the object list. statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do %{ diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs index c1e01557d..b20f15cbd 100644 --- a/test/pleroma/web/activity_pub/transmogrifier_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs @@ -86,6 +86,23 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do assert activity.data["cc"] == [user.ap_id] end + test "it rejects Flag activities when both reporter and reported account are remote" do + reporter = insert(:user, local: false, domain: "mastodon.cat") + reported = insert(:user, local: false, domain: "nicecrew.digital") + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "actor" => reporter.ap_id, + "content" => "blocked AND reported!!!", + "object" => [reported.ap_id, "https://nicecrew.digital/objects/report-status"], + "type" => "Flag" + } + + assert {:reject, reason} = Transmogrifier.handle_incoming(message) + assert reason =~ "third-party report" + refute "Flag" |> Pleroma.Activity.Queries.by_type() |> Pleroma.Repo.one() + end + test "it accepts Move activities" do old_user = insert(:user) new_user = insert(:user) From 6f415cf3fcdad8fc8d440a00c75e8388d99677e5 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 12 May 2026 23:31:55 +0200 Subject: [PATCH 113/391] EnsureHostMatchesPlug: Remove match against default scheme port Checking against the default port of the Endpoint URL scheme is redundant as normal instances will have the combination https/443 by default created by pleroma.instance gen, Tor-only instances should have combination http/80 and local testing instances httt/XXXX. The default scheme port doesn't add anything usefull in these configs. --- .../web/plugs/ensure_host_matches_plug.ex | 17 ++++------- .../plugs/ensure_host_matches_plug_test.exs | 29 +------------------ 2 files changed, 7 insertions(+), 39 deletions(-) diff --git a/lib/pleroma/web/plugs/ensure_host_matches_plug.ex b/lib/pleroma/web/plugs/ensure_host_matches_plug.ex index 87c4c0531..ab2129f9e 100644 --- a/lib/pleroma/web/plugs/ensure_host_matches_plug.ex +++ b/lib/pleroma/web/plugs/ensure_host_matches_plug.ex @@ -17,7 +17,6 @@ defmodule Pleroma.Web.Plugs.EnsureHostMatchesPlug do host_header = get_req_header(conn, "host") host_uri = URI.parse("//#{host_header}") instance_uri = URI.parse(Endpoint.url()) - instance_scheme_port = URI.default_port(instance_uri.scheme) case host_header do [host] -> @@ -26,7 +25,7 @@ defmodule Pleroma.Web.Plugs.EnsureHostMatchesPlug do resp(conn, 400, "Host header not provided") |> halt() true -> - if host_matches?(host_uri, instance_uri, instance_scheme_port), + if host_matches?(host_uri, instance_uri), do: assign(conn, :valid_host_header, true), else: resp(conn, 400, "Host header does not match this instance") |> halt() end @@ -52,17 +51,13 @@ defmodule Pleroma.Web.Plugs.EnsureHostMatchesPlug do # Host header did not provide port # Host header is scheme-less, URI.parse does not provide default port - defp host_matches?(%URI{host: req_host, port: nil}, %URI{host: instance_host}, _), + defp host_matches?(%URI{host: req_host, port: nil}, %URI{host: instance_host}), do: case_insensitive_compare(req_host, instance_host) - # Host header provided a port, reverse proxy configuration (port cannot match Endpoint port) - # Both port 80 and 443 are valid based on Endpoint configuration - defp host_matches?(%URI{host: req_host, port: port}, %URI{host: instance_host}, port), + # Host header provided a port + # Any port specified in the Endpoint url configuration is valid here + defp host_matches?(%URI{host: req_host, port: port}, %URI{host: instance_host, port: port}), do: case_insensitive_compare(req_host, instance_host) - # Host header provided port, configuration without reverse proxy (port matches Endpoint port) - defp host_matches?(%URI{host: req_host, port: port}, %URI{host: instance_host, port: port}, _), - do: case_insensitive_compare(req_host, instance_host) - - defp host_matches?(_, _, _), do: false + defp host_matches?(_, _), do: false end diff --git a/test/pleroma/web/plugs/ensure_host_matches_plug_test.exs b/test/pleroma/web/plugs/ensure_host_matches_plug_test.exs index 67252e7a7..8ace74dfb 100644 --- a/test/pleroma/web/plugs/ensure_host_matches_plug_test.exs +++ b/test/pleroma/web/plugs/ensure_host_matches_plug_test.exs @@ -8,7 +8,6 @@ defmodule Pleroma.Web.Plugs.EnsureHostMatchesPlugTest do alias Pleroma.Web.Endpoint alias Pleroma.Web.Plugs.EnsureHostMatchesPlug - import Mock import Plug.Conn import Tesla.Mock @@ -105,33 +104,7 @@ defmodule Pleroma.Web.Plugs.EnsureHostMatchesPlugTest do assert Map.get(conn.assigns, :valid_host_header, nil) end - test "it works for Host header with port as 80", %{conn: conn} do - endpoint = URI.parse(Endpoint.url()) - - conn = - conn - |> set_host("#{endpoint.host}:80") - |> EnsureHostMatchesPlug.call(%{}) - - assert conn.halted == false - assert Map.get(conn.assigns, :valid_host_header, nil) - end - - test "it works for Host header with port as 443", %{conn: conn} do - with_mock Pleroma.Web.Endpoint, url: fn -> "https://localhost:4001" end do - endpoint = URI.parse(Endpoint.url()) - - conn = - conn - |> set_host("#{endpoint.host}:443") - |> EnsureHostMatchesPlug.call(%{}) - - assert conn.halted == false - assert Map.get(conn.assigns, :valid_host_header, nil) - end - end - - test "it works for Host header with port as same as Endpoint (no reverse proxy config)", %{ + test "it works for Host header with port same as Endpoint", %{ conn: conn } do endpoint = URI.parse(Endpoint.url()) From 6c2d8209c9b3f86db6b89d48421b7a0d70a33d00 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 13 May 2026 00:31:50 +0200 Subject: [PATCH 114/391] SignatureRetryWorker: require validated host header --- lib/pleroma/workers/signature_retry_worker.ex | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/pleroma/workers/signature_retry_worker.ex b/lib/pleroma/workers/signature_retry_worker.ex index 2c4c097dd..28958faff 100644 --- a/lib/pleroma/workers/signature_retry_worker.ex +++ b/lib/pleroma/workers/signature_retry_worker.ex @@ -8,6 +8,7 @@ defmodule Pleroma.Workers.SignatureRetryWorker do alias Pleroma.User alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.Federator + alias Pleroma.Web.Plugs.EnsureHostMatchesPlug alias Pleroma.Web.Plugs.MappedSignatureToIdentityPlug require Logger @@ -48,6 +49,7 @@ defmodule Pleroma.Workers.SignatureRetryWorker do {:ok, _public_key} <- Signature.refetch_public_key(conn_data), {:signature, true} <- {:signature, validate_signature(conn_data)}, {:same_actor, true} <- {:same_actor, validate_same_actor(conn_data)}, + {:host_header, true} <- {:host_header, validate_host_header(conn_data)}, {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do unless Instances.reachable?(params["actor"]) do domain = URI.parse(params["actor"]).host @@ -103,6 +105,16 @@ defmodule Pleroma.Workers.SignatureRetryWorker do end end + defp validate_host_header(conn_data) do + case EnsureHostMatchesPlug.call(conn_data, []) do + %Plug.Conn{assigns: %{valid_signature: true, valid_host_header: true}} -> + true + + _ -> + false + end + end + defp validate_same_actor(conn_data) do case MappedSignatureToIdentityPlug.call(conn_data, []) do %Plug.Conn{assigns: %{valid_signature: true}} -> @@ -170,6 +182,10 @@ defmodule Pleroma.Workers.SignatureRetryWorker do {:same_actor, false} -> {:cancel, :actor_signature_mismatch} + # Host header from request not for us + {:host_header, false} -> + {:cancel, :host_header_mismatch} + # Origin / URL validation failed somewhere possibly due to spoofing {:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed} @@ -234,6 +250,7 @@ defmodule Pleroma.Workers.SignatureRetryWorker do defp log_signature_retry_rejection({:cancel, reason}, context) when reason in [ :actor_signature_mismatch, + :host_header_mismatch, :invalid_signature, :invalid_signature_retry_metadata, :missing_signature_retry_metadata, From 95b15190dedab255c102db023af2d90eacf3a259 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 13 May 2026 00:32:16 +0200 Subject: [PATCH 115/391] ActivityPubController: require validated host header --- lib/pleroma/web/activity_pub/activity_pub_controller.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 2bfff6968..415aa4f68 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -303,7 +303,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do end end - def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do + def inbox(%{assigns: %{valid_signature: true, valid_host_header: true}} = conn, %{"nickname" => nickname} = params) do with {:recipient_exists, %User{} = recipient} <- {:recipient_exists, User.get_cached_by_nickname(nickname)}, {:sender_exists, {:ok, %User{} = actor}} <- @@ -342,7 +342,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do end end - def inbox(%{assigns: %{valid_signature: true}} = conn, params) do + def inbox(%{assigns: %{valid_signature: true, valid_host_header: true}} = conn, params) do Federator.incoming_ap_doc(params) json(conn, "ok") end From c19bdf38143fcbe3a91a3b6053acefb8925773ac Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 13 May 2026 00:33:09 +0200 Subject: [PATCH 116/391] SignatureRetryWorker: add mismatched host test, fix tests --- .../workers/signature_retry_worker_test.exs | 66 ++++++++++++++++++- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/test/pleroma/workers/signature_retry_worker_test.exs b/test/pleroma/workers/signature_retry_worker_test.exs index 94dd5f6c1..7f1351f4a 100644 --- a/test/pleroma/workers/signature_retry_worker_test.exs +++ b/test/pleroma/workers/signature_retry_worker_test.exs @@ -16,12 +16,13 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do alias Pleroma.Signature alias Pleroma.User alias Pleroma.Web.ActivityPub.UserView + alias Pleroma.Web.Endpoint alias Pleroma.Web.Federator alias Pleroma.Workers.SignatureRetryWorker defp signature_headers_for(%User{} = signer) do [ - {"host", "local.test"}, + {"host", "#{URI.parse(Endpoint.url()).host}"}, {"date", "Thu, 25 Jul 2024 13:33:31 GMT"}, {"digest", "SHA-256=fake-digest"}, {"content-type", "application/activity+json"}, @@ -245,6 +246,65 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do refute Activity.get_by_ap_id(create["id"]) end + test "cancels when the Host header does not match Endpoint" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + + create = %{ + "type" => "Create", + "actor" => alice.ap_id, + "id" => "https://one.com/activities/invalid-signature-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => "https://one.com/objects/invalid-signature-note", + "actor" => alice.ap_id, + "attributedTo" => alice.ap_id, + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + expect_signature_from(alice) + + headers = + [ + {"host", "invalid.example.com"}, + {"date", "Thu, 25 Jul 2024 13:33:31 GMT"}, + {"digest", "SHA-256=fake-digest"}, + {"content-type", "application/activity+json"}, + { + "signature", + "keyId=\"#{alice.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" + } + ] + + assert {:ok, oban_job} = Federator.incoming_failed_signature_ap_doc(%{ + method: "POST", + req_headers: headers, + request_path: "/inbox", + params: create, + query_string: "" + }) + + log = + capture_log([level: :warning], fn -> + assert {:cancel, :host_header_mismatch} = SignatureRetryWorker.perform(oban_job) + end) + + assert log =~ "Failed-signature inbox retry rejected" + assert log =~ "reason=:host_header_mismatch" + assert log =~ "payload_actor=\"https://one.com/users/alice\"" + assert log =~ "signature_actor=\"https://one.com/users/alice\"" + assert log =~ "activity_id=\"https://one.com/activities/invalid-signature-create\"" + assert log =~ "type=\"Create\"" + assert log =~ "request_path=\"/inbox\"" + + refute Activity.get_by_ap_id(create["id"]) + end + test "processes the activity after refetching a valid matching signature" do alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") @@ -309,11 +369,11 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do "content-type" => "application/activity+json", date: date, digest: digest, - host: "local.test" + host: "#{URI.parse(Endpoint.url()).host}" }) req_headers = [ - ["host", "local.test"], + ["host", "#{URI.parse(Endpoint.url()).host}"], ["date", date], ["digest", digest], ["content-type", "application/activity+json"], From 95eef879d76df69e4d113f79afa9fbd2188aa5b0 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 13 May 2026 00:40:53 +0200 Subject: [PATCH 117/391] ActivityPubController: add mismatched host test --- .../activity_pub_controller_test.exs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index 3988c3912..64f79dab5 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -950,6 +950,49 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do refute Activity.get_by_ap_id(data["id"]) end + test "does not process post with Host header not for us", %{conn: conn} do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + object_id = "https://one.com/objects/inbox-forged-note" + + data = %{ + "type" => "Create", + "actor" => alice.ap_id, + "id" => "https://one.com/activities/inbox-forged-create", + "context" => "https://one.com/contexts/inbox-forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => object_id, + "actor" => alice.ap_id, + "attributedTo" => alice.ap_id, + "context" => "https://one.com/contexts/inbox-forged-create", + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + expect_signature_retry_from(alice) + + conn = %{conn | req_headers: [{"host", "invalid.example.com"}]} + + conn = + conn + |> assign(:valid_signature, false) + |> put_req_header("content-type", "application/activity+json") + |> put_req_header("signature", "keyId=\"https://one.com/users/alice#main-key\"") + |> post("/inbox", data) + + assert "Host header does not match this instance" == conn.resp_body + assert 400 == conn.status + assert true == conn.halted + + refute Activity.get_by_ap_id(data["id"]) + refute Object.get_by_ap_id(object_id) + end + test "accept follow activity", %{conn: conn} do clear_config([:instance, :federating], true) relay = Relay.get_actor() From 2b3ac2d7fee91cdcbe53ca53760559def250b7d7 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 13 May 2026 00:44:33 +0200 Subject: [PATCH 118/391] lint --- .../web/activity_pub/activity_pub_controller.ex | 7 ++++++- .../workers/signature_retry_worker_test.exs | 15 ++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 415aa4f68..3d1875599 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -303,7 +303,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do end end - def inbox(%{assigns: %{valid_signature: true, valid_host_header: true}} = conn, %{"nickname" => nickname} = params) do + def inbox( + %{ + assigns: %{valid_signature: true, valid_host_header: true} + } = conn, + %{"nickname" => nickname} = params + ) do with {:recipient_exists, %User{} = recipient} <- {:recipient_exists, User.get_cached_by_nickname(nickname)}, {:sender_exists, {:ok, %User{} = actor}} <- diff --git a/test/pleroma/workers/signature_retry_worker_test.exs b/test/pleroma/workers/signature_retry_worker_test.exs index 7f1351f4a..3806ecac9 100644 --- a/test/pleroma/workers/signature_retry_worker_test.exs +++ b/test/pleroma/workers/signature_retry_worker_test.exs @@ -281,13 +281,14 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do } ] - assert {:ok, oban_job} = Federator.incoming_failed_signature_ap_doc(%{ - method: "POST", - req_headers: headers, - request_path: "/inbox", - params: create, - query_string: "" - }) + assert {:ok, oban_job} = + Federator.incoming_failed_signature_ap_doc(%{ + method: "POST", + req_headers: headers, + request_path: "/inbox", + params: create, + query_string: "" + }) log = capture_log([level: :warning], fn -> From 4d3aea1fce1a3d9b268aad2de8cae75b625ff5d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 13 May 2026 00:55:47 +0200 Subject: [PATCH 119/391] Handle reports with just actor ap id as the object MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/iceshrimpnet-reports-fix.fix | 1 + .../web/activity_pub/transmogrifier.ex | 1 + .../web/activity_pub/transmogrifier_test.exs | 20 +++++++++++++++++++ 3 files changed, 22 insertions(+) create mode 100644 changelog.d/iceshrimpnet-reports-fix.fix diff --git a/changelog.d/iceshrimpnet-reports-fix.fix b/changelog.d/iceshrimpnet-reports-fix.fix new file mode 100644 index 000000000..7487ae47e --- /dev/null +++ b/changelog.d/iceshrimpnet-reports-fix.fix @@ -0,0 +1 @@ +Handle reports with just actor ap id as the object diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 4421da26c..eba588cf3 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -444,6 +444,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do ) do with context <- data["context"] || Utils.generate_context_id(), content <- data["content"] || "", + objects <- List.wrap(objects), %User{} = actor <- User.get_cached_by_ap_id(actor), # Reduce the object list to find the reported user. %User{} = account <- get_reported(objects), diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs index c1e01557d..2e3da8598 100644 --- a/test/pleroma/web/activity_pub/transmogrifier_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs @@ -86,6 +86,26 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do assert activity.data["cc"] == [user.ap_id] end + test "it accepts Flag activities with just actor id as object" do + user = insert(:user) + other_user = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "cc" => [user.ap_id], + "object" => user.ap_id, + "type" => "Flag", + "content" => "blocked AND reported!!!", + "actor" => other_user.ap_id + } + + assert {:ok, activity} = Transmogrifier.handle_incoming(message) + + assert activity.data["content"] == "blocked AND reported!!!" + assert activity.data["actor"] == other_user.ap_id + assert activity.data["cc"] == [user.ap_id] + end + test "it accepts Move activities" do old_user = insert(:user) new_user = insert(:user) From 4810d2536e2fd366364c1ec39d88b21265166f88 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 13 May 2026 12:10:10 +0200 Subject: [PATCH 120/391] ActivityPubController: Use valid signatures in Host header test --- .../activity_pub/activity_pub_controller_test.exs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index 64f79dab5..68e39b3a0 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -974,16 +974,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do } } - expect_signature_retry_from(alice) - - conn = %{conn | req_headers: [{"host", "invalid.example.com"}]} - + # Plug will complain when replacing raw host header with put_req_header. + # The Plug way is updating conn.host, but that isn't the raw header + # and that isn't used in the EnsureHostMatchesPlug, because it doesn't include the port. conn = conn - |> assign(:valid_signature, false) + |> assign_valid_signature_for_actor(alice) + |> delete_req_header("host") |> put_req_header("content-type", "application/activity+json") - |> put_req_header("signature", "keyId=\"https://one.com/users/alice#main-key\"") - |> post("/inbox", data) + + conn = %{conn | req_headers: conn.req_headers ++ [{"host", "invalid.example.com"}]} + conn = post(conn, "/inbox", data) assert "Host header does not match this instance" == conn.resp_body assert 400 == conn.status From 9e16332d9d208abc4d3992fec27b317c932f7e07 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Wed, 13 May 2026 18:39:40 +0400 Subject: [PATCH 121/391] Update majic to 1.2.0 --- changelog.d/majic-1.2.0.change | 1 + mix.exs | 4 +++- mix.lock | 2 +- test/pleroma/dependency_version_test.exs | 16 ++++++++++++++++ 4 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 changelog.d/majic-1.2.0.change create mode 100644 test/pleroma/dependency_version_test.exs diff --git a/changelog.d/majic-1.2.0.change b/changelog.d/majic-1.2.0.change new file mode 100644 index 000000000..bc8d5eb3c --- /dev/null +++ b/changelog.d/majic-1.2.0.change @@ -0,0 +1 @@ +Update majic to 1.2.0. diff --git a/mix.exs b/mix.exs index 9c10cacc0..872929c2c 100644 --- a/mix.exs +++ b/mix.exs @@ -191,7 +191,9 @@ defmodule Pleroma.Mixfile do git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", ref: "e7b7cc34cc16b383461b966484c297e4ec9aeef6"}, {:restarter, path: "./restarter"}, - {:majic, "~> 1.1"}, + {:majic, "1.2.0", + git: "https://git.pleroma.social/pleroma-elixir-libraries/majic.git", + ref: "1f2df25cccdd9548fd59dbfa402c996268ae99ec"}, {:open_api_spex, "~> 3.22"}, {:ecto_psql_extras, "~> 0.8"}, {:vix, "~> 0.36"}, diff --git a/mix.lock b/mix.lock index da0c1c60c..780dea4c0 100644 --- a/mix.lock +++ b/mix.lock @@ -73,7 +73,7 @@ "linkify": {:hex, :linkify, "0.5.3", "5f8143d8f61f5ff08d3aeeff47ef6509492b4948d8f08007fbf66e4d2246a7f2", [:mix], [], "hexpm", "3ef35a1377d47c25506e07c1c005ea9d38d700699d92ee92825f024434258177"}, "logger_backends": {:hex, :logger_backends, "1.0.0", "09c4fad6202e08cb0fbd37f328282f16539aca380f512523ce9472b28edc6bdf", [:mix], [], "hexpm", "1faceb3e7ec3ef66a8f5746c5afd020e63996df6fd4eb8cdb789e5665ae6c9ce"}, "mail": {:hex, :mail, "0.3.1", "cb0a14e4ed8904e4e5a08214e686ccf6f9099346885db17d8c309381f865cc5c", [:mix], [], "hexpm", "1db701e89865c1d5fa296b2b57b1cd587587cca8d8a1a22892b35ef5a8e352a6"}, - "majic": {:hex, :majic, "1.1.1", "16092a3a3376cc5e13d207e82ec06e05a5561170465e50cc11cc4df8a5747938", [:make, :mix], [{:elixir_make, "~> 0.8.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "7fbb0372f0447b3f777056177d6ab3f009742e68474f850521ff56b84bd85b96"}, + "majic": {:git, "https://git.pleroma.social/pleroma-elixir-libraries/majic.git", "1f2df25cccdd9548fd59dbfa402c996268ae99ec", [ref: "1f2df25cccdd9548fd59dbfa402c996268ae99ec"]}, "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, "makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, diff --git a/test/pleroma/dependency_version_test.exs b/test/pleroma/dependency_version_test.exs new file mode 100644 index 000000000..0c9c1e939 --- /dev/null +++ b/test/pleroma/dependency_version_test.exs @@ -0,0 +1,16 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.DependencyVersionTest do + use ExUnit.Case, async: true + + test "uses majic 1.2" do + majic_version = + :majic + |> Application.spec(:vsn) + |> to_string() + + assert Version.match?(majic_version, "~> 1.2") + end +end From d8e3ea69b17189aad1a3d3e60587ccf16bd3154f Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Wed, 13 May 2026 19:00:16 +0400 Subject: [PATCH 122/391] Use Hex release for majic 1.2.0 --- mix.exs | 4 +--- mix.lock | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/mix.exs b/mix.exs index 872929c2c..2cf5137a5 100644 --- a/mix.exs +++ b/mix.exs @@ -191,9 +191,7 @@ defmodule Pleroma.Mixfile do git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", ref: "e7b7cc34cc16b383461b966484c297e4ec9aeef6"}, {:restarter, path: "./restarter"}, - {:majic, "1.2.0", - git: "https://git.pleroma.social/pleroma-elixir-libraries/majic.git", - ref: "1f2df25cccdd9548fd59dbfa402c996268ae99ec"}, + {:majic, "~> 1.2"}, {:open_api_spex, "~> 3.22"}, {:ecto_psql_extras, "~> 0.8"}, {:vix, "~> 0.36"}, diff --git a/mix.lock b/mix.lock index 780dea4c0..9214478c7 100644 --- a/mix.lock +++ b/mix.lock @@ -73,7 +73,7 @@ "linkify": {:hex, :linkify, "0.5.3", "5f8143d8f61f5ff08d3aeeff47ef6509492b4948d8f08007fbf66e4d2246a7f2", [:mix], [], "hexpm", "3ef35a1377d47c25506e07c1c005ea9d38d700699d92ee92825f024434258177"}, "logger_backends": {:hex, :logger_backends, "1.0.0", "09c4fad6202e08cb0fbd37f328282f16539aca380f512523ce9472b28edc6bdf", [:mix], [], "hexpm", "1faceb3e7ec3ef66a8f5746c5afd020e63996df6fd4eb8cdb789e5665ae6c9ce"}, "mail": {:hex, :mail, "0.3.1", "cb0a14e4ed8904e4e5a08214e686ccf6f9099346885db17d8c309381f865cc5c", [:mix], [], "hexpm", "1db701e89865c1d5fa296b2b57b1cd587587cca8d8a1a22892b35ef5a8e352a6"}, - "majic": {:git, "https://git.pleroma.social/pleroma-elixir-libraries/majic.git", "1f2df25cccdd9548fd59dbfa402c996268ae99ec", [ref: "1f2df25cccdd9548fd59dbfa402c996268ae99ec"]}, + "majic": {:hex, :majic, "1.2.0", "414b69c1460ece692fa892a0ed6669b8db6a44c42bb3071cb723854f22e7bd78", [:make, :mix], [{:elixir_make, "~> 0.8.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0bd0c65f8e4dcc595757d957577f6151b59246da9614ba87debfdc641ccc0514"}, "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, "makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, From d0c2d04356bbed3b2be2b9873a6c207f54021285 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Wed, 13 May 2026 19:10:15 +0400 Subject: [PATCH 123/391] Use Hex release for oban_plugins_lazarus --- changelog.d/oban-plugins-lazarus-hex.change | 1 + mix.exs | 4 +--- mix.lock | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) create mode 100644 changelog.d/oban-plugins-lazarus-hex.change diff --git a/changelog.d/oban-plugins-lazarus-hex.change b/changelog.d/oban-plugins-lazarus-hex.change new file mode 100644 index 000000000..9259df0f5 --- /dev/null +++ b/changelog.d/oban-plugins-lazarus-hex.change @@ -0,0 +1 @@ +Use the Hex package for oban_plugins_lazarus. diff --git a/mix.exs b/mix.exs index 2cf5137a5..946bf0ab0 100644 --- a/mix.exs +++ b/mix.exs @@ -137,9 +137,7 @@ defmodule Pleroma.Mixfile do {:tzdata, "~> 1.0.5"}, {:plug_cowboy, "~> 2.7"}, {:oban, "~> 2.19.4"}, - {:oban_plugins_lazarus, - git: "https://git.pleroma.social/pleroma/elixir-libraries/oban_plugins_lazarus.git", - ref: "e49fc355baaf0e435208bf5f534d31e26e897711"}, + {:oban_plugins_lazarus, "~> 0.1.1"}, {:oban_web, "~> 2.11"}, {:gettext, "~> 0.24"}, {:bcrypt_elixir, "~> 2.3"}, diff --git a/mix.lock b/mix.lock index 9214478c7..f33c22482 100644 --- a/mix.lock +++ b/mix.lock @@ -96,7 +96,7 @@ "oban": {:hex, :oban, "2.19.4", "045adb10db1161dceb75c254782f97cdc6596e7044af456a59decb6d06da73c1", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5fcc6219e6464525b808d97add17896e724131f498444a292071bf8991c99f97"}, "oban_live_dashboard": {:hex, :oban_live_dashboard, "0.1.1", "8aa4ceaf381c818f7d5c8185cc59942b8ac82ef0cf559881aacf8d3f8ac7bdd3", [:mix], [{:oban, "~> 2.15", [hex: :oban, repo: "hexpm", optional: false]}, {:phoenix_live_dashboard, "~> 0.7", [hex: :phoenix_live_dashboard, repo: "hexpm", optional: false]}], "hexpm", "16dc4ce9c9a95aa2e655e35ed4e675652994a8def61731a18af85e230e1caa63"}, "oban_met": {:hex, :oban_met, "1.0.5", "bb633ab06448dab2ef9194f6688d33b3d07fc3f2ad793a1a08f4dfbb2cc9fe50", [:mix], [{:oban, "~> 2.19", [hex: :oban, repo: "hexpm", optional: false]}], "hexpm", "64664d50805bbfd3903aeada1f3c39634652a87844797ee400b0bcc95a28f5ea"}, - "oban_plugins_lazarus": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/oban_plugins_lazarus.git", "e49fc355baaf0e435208bf5f534d31e26e897711", [ref: "e49fc355baaf0e435208bf5f534d31e26e897711"]}, + "oban_plugins_lazarus": {:hex, :oban_plugins_lazarus, "0.1.1", "a5141d569e5b9f3bec8f4a958231d2c538af097d3c1ad06274f096fc06956821", [:mix], [{:oban, "< 3.0.0", [hex: :oban, repo: "hexpm", optional: false]}], "hexpm", "7e9c51bc44d33f71c0a52cf0cd5c8d4c70380ede065c1042013f5123f2fc9729"}, "oban_web": {:hex, :oban_web, "2.11.6", "53933cb4253c4d9f1098ee311c06f07935259f0e564dcf2d66bae4cc98e317fe", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, "~> 2.19", [hex: :oban, repo: "hexpm", optional: false]}, {:oban_met, "~> 1.0", [hex: :oban_met, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}], "hexpm", "576d94b705688c313694c2c114ca21aa0f8f2ad1b9ca45c052c5ba316d3e8d10"}, "octo_fetch": {:hex, :octo_fetch, "0.4.0", "074b5ecbc08be10b05b27e9db08bc20a3060142769436242702931c418695b19", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "cf8be6f40cd519d7000bb4e84adcf661c32e59369ca2827c4e20042eda7a7fc6"}, "open_api_spex": {:hex, :open_api_spex, "3.22.0", "fbf90dc82681dc042a4ee79853c8e989efbba73d9e87439085daf849bbf8bc20", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "dd751ddbdd709bb4a5313e9a24530da6e66594773c7242a0c2592cbd9f589063"}, From c92d23323346e0538fc04e79e9a110fe6bb9198e Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Wed, 13 May 2026 20:04:12 +0400 Subject: [PATCH 124/391] Use upstream remote_ip package --- changelog.d/remote-ip-hex.change | 1 + config/config.exs | 1 + config/description.exs | 8 ++++- lib/pleroma/web/plugs/remote_ip.ex | 42 ++++++++++++++++++++--- mix.exs | 5 ++- mix.lock | 4 +-- test/pleroma/web/plugs/remote_ip_test.exs | 34 ++++++++++++++++++ 7 files changed, 85 insertions(+), 10 deletions(-) create mode 100644 changelog.d/remote-ip-hex.change diff --git a/changelog.d/remote-ip-hex.change b/changelog.d/remote-ip-hex.change new file mode 100644 index 000000000..44f6bc1bf --- /dev/null +++ b/changelog.d/remote-ip-hex.change @@ -0,0 +1 @@ +Use upstream Hex releases for the `remote_ip` dependency and expose client IP ranges for remote IP resolution. diff --git a/config/config.exs b/config/config.exs index 2d38e3ebe..3f880af53 100644 --- a/config/config.exs +++ b/config/config.exs @@ -738,6 +738,7 @@ config :pleroma, Pleroma.Workers.PurgeExpiredActivity, enabled: true, min_lifeti config :pleroma, Pleroma.Web.Plugs.RemoteIp, enabled: true, headers: ["x-forwarded-for"], + clients: [], proxies: [], reserved: [ "127.0.0.0/8", diff --git a/config/description.exs b/config/description.exs index 6e4348907..7c377588c 100644 --- a/config/description.exs +++ b/config/description.exs @@ -2910,7 +2910,7 @@ config :pleroma, :config_description, [ key: Pleroma.Web.Plugs.RemoteIp, type: :group, description: """ - `Pleroma.Web.Plugs.RemoteIp` is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration. + `Pleroma.Web.Plugs.RemoteIp` is a shim to call [`RemoteIp`](https://hex.pm/packages/remote_ip) but with runtime configuration. **If your instance is not behind at least one reverse proxy, you should not enable this plug.** """, children: [ @@ -2926,6 +2926,12 @@ config :pleroma, :config_description, [ A list of strings naming the HTTP headers to use when deriving the true client IP. Default: `["x-forwarded-for"]`. """ }, + %{ + key: :clients, + type: {:list, :string}, + description: + "A list of client IPs or subnets in CIDR notation. These will not be treated as proxies or reserved ranges. Defaults to `[]`. IPv4 entries without a bitmask will be assumed to be /32 and IPv6 /128." + }, %{ key: :proxies, type: {:list, :string}, diff --git a/lib/pleroma/web/plugs/remote_ip.ex b/lib/pleroma/web/plugs/remote_ip.ex index 3a4bffb50..4c2ea099d 100644 --- a/lib/pleroma/web/plugs/remote_ip.ex +++ b/lib/pleroma/web/plugs/remote_ip.ex @@ -4,7 +4,7 @@ defmodule Pleroma.Web.Plugs.RemoteIp do @moduledoc """ - This is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration. + This is a shim to call [`RemoteIp`](https://hex.pm/packages/remote_ip) but with runtime configuration. """ alias Pleroma.Config @@ -17,15 +17,29 @@ defmodule Pleroma.Web.Plugs.RemoteIp do def call(%{remote_ip: original_remote_ip} = conn, _) do if Config.get([__MODULE__, :enabled]) do - %{remote_ip: new_remote_ip} = conn = RemoteIp.call(conn, remote_ip_opts()) + new_remote_ip = remote_ip(conn) || original_remote_ip + + conn = %{conn | remote_ip: new_remote_ip} assign(conn, :remote_ip_found, original_remote_ip != new_remote_ip) else conn end end + defp remote_ip(conn) do + opts = remote_ip_opts() + + # Do not use RemoteIp.from/2 here: upstream remote_ip always applies its + # built-in reserved ranges. Pleroma keeps :reserved configurable, so reuse + # only the header parsing and apply Pleroma's own block classification. + conn.req_headers + |> RemoteIp.Headers.take(opts[:headers]) + |> RemoteIp.Headers.parse() + |> Enum.reverse() + |> Enum.find(&client?(&1, opts)) + end + defp remote_ip_opts do - headers = Config.get([__MODULE__, :headers], []) |> MapSet.new() reserved = Config.get([__MODULE__, :reserved], []) proxies = @@ -33,6 +47,26 @@ defmodule Pleroma.Web.Plugs.RemoteIp do |> Enum.concat(reserved) |> Enum.map(&InetHelper.parse_cidr/1) - {headers, proxies} + clients = + Config.get([__MODULE__, :clients], []) + |> Enum.map(&InetHelper.parse_cidr/1) + + [ + headers: Config.get([__MODULE__, :headers], []), + clients: clients, + proxies: proxies + ] + end + + defp client?(ip, opts) do + client_ip?(ip, opts[:clients]) || !proxy_ip?(ip, opts[:proxies]) + end + + defp client_ip?(ip, clients) do + Enum.any?(clients, &InetCidr.contains?(&1, ip)) + end + + defp proxy_ip?(ip, proxies) do + Enum.any?(proxies, &InetCidr.contains?(&1, ip)) end end diff --git a/mix.exs b/mix.exs index 946bf0ab0..90586b2d3 100644 --- a/mix.exs +++ b/mix.exs @@ -182,9 +182,8 @@ defmodule Pleroma.Mixfile do {:plug_static_index_html, "~> 1.0.0"}, {:flake_id, "~> 0.1.0"}, {:concurrent_limiter, "~> 0.1.1"}, - {:remote_ip, - git: "https://git.pleroma.social/pleroma/remote_ip.git", - ref: "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8"}, + {:remote_ip, "~> 1.2.0"}, + {:inet_cidr, "~> 1.0"}, {:captcha, git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", ref: "e7b7cc34cc16b383461b966484c297e4ec9aeef6"}, diff --git a/mix.lock b/mix.lock index f33c22482..04de2825e 100644 --- a/mix.lock +++ b/mix.lock @@ -65,7 +65,7 @@ "http_signatures": {:hex, :http_signatures, "0.1.3", "19f26aee35b4684e37efdce3ac4638605e6e41a04368186bd39d2b6138a60ea9", [:mix], [{:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "20313a65516db88006f85b090f6f76cc5b04e9609b45943657e6781eb91174f4"}, "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, - "inet_cidr": {:hex, :inet_cidr, "1.0.8", "d26bb7bdbdf21ae401ead2092bf2bb4bf57fe44a62f5eaa5025280720ace8a40", [:mix], [], "hexpm", "d5b26da66603bb56c933c65214c72152f0de9a6ea53618b56d63302a68f6a90e"}, + "inet_cidr": {:hex, :inet_cidr, "1.0.9", "e0ef72a2942529da78c8e4147d53f2ef5f6f5293335c3637b0fdf83c012cc816", [:mix], [], "hexpm", "172da15ff7cf635b1feaf14f5818be28c811b37cc5fb7c5f7c01058c1c1066cc"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"}, "jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"}, @@ -132,7 +132,7 @@ "quic": {:hex, :quic, "0.10.2", "4b390507a85f65ce47808f3df1a864e0baf9adb7a1b4ea9f4dcd66fe9d0cb166", [:rebar3], [], "hexpm", "7c196a66973c877a59768a5687f0a0610ff11817254d0a4e45cc4e3a16b1d00b"}, "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"}, "recon": {:hex, :recon, "2.5.6", "9052588e83bfedfd9b72e1034532aee2a5369d9d9343b61aeb7fbce761010741", [:mix, :rebar3], [], "hexpm", "96c6799792d735cc0f0fd0f86267e9d351e63339cbe03df9d162010cefc26bb0"}, - "remote_ip": {:git, "https://git.pleroma.social/pleroma/remote_ip.git", "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8", [ref: "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8"]}, + "remote_ip": {:hex, :remote_ip, "1.2.0", "fb078e12a44414f4cef5a75963c33008fe169b806572ccd17257c208a7bc760f", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "2ff91de19c48149ce19ed230a81d377186e4412552a597d6a5137373e5877cb7"}, "rustler": {:hex, :rustler, "0.30.0", "cefc49922132b072853fa9b0ca4dc2ffcb452f68fb73b779042b02d545e097fb", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "9ef1abb6a7dda35c47cfc649e6a5a61663af6cf842a55814a554a84607dee389"}, "sleeplocks": {:hex, :sleeplocks, "1.1.3", "96a86460cc33b435c7310dbd27ec82ca2c1f24ae38e34f8edde97f756503441a", [:rebar3], [], "hexpm", "d3b3958552e6eb16f463921e70ae7c767519ef8f5be46d7696cc1ed649421321"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, diff --git a/test/pleroma/web/plugs/remote_ip_test.exs b/test/pleroma/web/plugs/remote_ip_test.exs index 37b751370..19e786f8a 100644 --- a/test/pleroma/web/plugs/remote_ip_test.exs +++ b/test/pleroma/web/plugs/remote_ip_test.exs @@ -106,4 +106,38 @@ defmodule Pleroma.Web.Plugs.RemoteIpTest do assert conn.remote_ip == {1, 1, 1, 1} end + + test "reserved ranges are configurable" do + clear_config([RemoteIp, :reserved], []) + + conn = + conn(:get, "/") + |> put_req_header("x-forwarded-for", "1.1.1.1, 10.0.0.3") + |> RemoteIp.call(nil) + + assert conn.remote_ip == {10, 0, 0, 3} + end + + test "clients override reserved ranges" do + clear_config([RemoteIp, :clients], ["10.0.0.0/8"]) + + conn = + conn(:get, "/") + |> put_req_header("x-forwarded-for", "1.1.1.1, 10.0.0.3") + |> RemoteIp.call(nil) + + assert conn.remote_ip == {10, 0, 0, 3} + end + + test "clients override proxies" do + clear_config([RemoteIp, :clients], ["10.0.0.3"]) + clear_config([RemoteIp, :proxies], ["10.0.0.0/8"]) + + conn = + conn(:get, "/") + |> put_req_header("x-forwarded-for", "1.1.1.1, 10.0.0.3") + |> RemoteIp.call(nil) + + assert conn.remote_ip == {10, 0, 0, 3} + end end From 7ab9e2c7cecf5f53fb5d46037dd52260fe42d762 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Wed, 13 May 2026 23:05:25 +0400 Subject: [PATCH 125/391] Use pleroma_captcha Hex package --- changelog.d/captcha-package.fix | 1 + mix.exs | 4 +--- mix.lock | 2 +- test/pleroma/captcha_test.exs | 8 ++++++++ 4 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 changelog.d/captcha-package.fix diff --git a/changelog.d/captcha-package.fix b/changelog.d/captcha-package.fix new file mode 100644 index 000000000..98c2f6525 --- /dev/null +++ b/changelog.d/captcha-package.fix @@ -0,0 +1 @@ +Switch native captcha to the published pleroma_captcha Hex package. diff --git a/mix.exs b/mix.exs index 946bf0ab0..17ca278be 100644 --- a/mix.exs +++ b/mix.exs @@ -185,9 +185,7 @@ defmodule Pleroma.Mixfile do {:remote_ip, git: "https://git.pleroma.social/pleroma/remote_ip.git", ref: "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8"}, - {:captcha, - git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", - ref: "e7b7cc34cc16b383461b966484c297e4ec9aeef6"}, + {:captcha, "~> 1.0.3", hex: :pleroma_captcha}, {:restarter, path: "./restarter"}, {:majic, "~> 1.2"}, {:open_api_spex, "~> 3.22"}, diff --git a/mix.lock b/mix.lock index f33c22482..d20cc408d 100644 --- a/mix.lock +++ b/mix.lock @@ -10,7 +10,7 @@ "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "cachex": {:hex, :cachex, "3.6.0", "14a1bfbeee060dd9bec25a5b6f4e4691e3670ebda28c8ba2884b12fe30b36bf8", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "ebf24e373883bc8e0c8d894a63bbe102ae13d918f790121f5cfe6e485cc8e2e2"}, "calendar": {:hex, :calendar, "1.0.0", "f52073a708528482ec33d0a171954ca610fe2bd28f1e871f247dc7f1565fa807", [:mix], [{:tzdata, "~> 0.1.201603 or ~> 0.5.20 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "990e9581920c82912a5ee50e62ff5ef96da6b15949a2ee4734f935fdef0f0a6f"}, - "captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "e7b7cc34cc16b383461b966484c297e4ec9aeef6", [ref: "e7b7cc34cc16b383461b966484c297e4ec9aeef6"]}, + "captcha": {:hex, :pleroma_captcha, "1.0.3", "6cc1440e91a092653fe909f028cc4c5d83ea858b1439e3b9a85e446382e2b9a3", [:make, :mix], [], "hexpm", "477f62c1a845a9458c546658223295f958f98136acef89c05beb278b1b6f4a14"}, "castore": {:hex, :castore, "1.0.15", "8aa930c890fe18b6fe0a0cff27b27d0d4d231867897bd23ea772dee561f032a3", [:mix], [], "hexpm", "96ce4c69d7d5d7a0761420ef743e2f4096253931a3ba69e5ff8ef1844fe446d3"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, diff --git a/test/pleroma/captcha_test.exs b/test/pleroma/captcha_test.exs index ec4c713f8..0f0a9bc87 100644 --- a/test/pleroma/captcha_test.exs +++ b/test/pleroma/captcha_test.exs @@ -50,6 +50,14 @@ defmodule Pleroma.CaptchaTest do end describe "Native" do + test "uses the published pleroma_captcha package" do + deps = Mix.Project.config() |> Keyword.fetch!(:deps) + assert {:captcha, "~> 1.0.3", opts} = Enum.find(deps, &(elem(&1, 0) == :captcha)) + assert opts[:hex] == :pleroma_captcha + refute Keyword.has_key?(opts, :compile) + refute Keyword.has_key?(opts, :git) + end + test "new and validate" do new = Native.new() From 9ae1249ccb821607a1babf23cf12eba3d633e6e9 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Wed, 13 May 2026 23:09:29 +0400 Subject: [PATCH 126/391] Remove captcha dependency shape test --- test/pleroma/captcha_test.exs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/test/pleroma/captcha_test.exs b/test/pleroma/captcha_test.exs index 0f0a9bc87..ec4c713f8 100644 --- a/test/pleroma/captcha_test.exs +++ b/test/pleroma/captcha_test.exs @@ -50,14 +50,6 @@ defmodule Pleroma.CaptchaTest do end describe "Native" do - test "uses the published pleroma_captcha package" do - deps = Mix.Project.config() |> Keyword.fetch!(:deps) - assert {:captcha, "~> 1.0.3", opts} = Enum.find(deps, &(elem(&1, 0) == :captcha)) - assert opts[:hex] == :pleroma_captcha - refute Keyword.has_key?(opts, :compile) - refute Keyword.has_key?(opts, :git) - end - test "new and validate" do new = Native.new() From 143f426e840de86e9a90a436f8446cc8534e165b Mon Sep 17 00:00:00 2001 From: Irrlicht Date: Wed, 9 Apr 2025 23:59:32 +0000 Subject: [PATCH 127/391] Translated using Weblate (French) Currently translated at 10.8% (106 of 974 strings) Translation: Pleroma/Pleroma Backend (domain config_descriptions) Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-backend-domain-config_descriptions/fr/ --- .../fr/LC_MESSAGES/config_descriptions.po | 116 ++++++++++-------- 1 file changed, 66 insertions(+), 50 deletions(-) diff --git a/priv/gettext/fr/LC_MESSAGES/config_descriptions.po b/priv/gettext/fr/LC_MESSAGES/config_descriptions.po index c24ab6751..56ea1bdf3 100644 --- a/priv/gettext/fr/LC_MESSAGES/config_descriptions.po +++ b/priv/gettext/fr/LC_MESSAGES/config_descriptions.po @@ -3,8 +3,8 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-07-22 02:09+0300\n" -"PO-Revision-Date: 2024-10-13 21:03+0000\n" -"Last-Translator: Codimp \n" +"PO-Revision-Date: 2025-04-11 01:04+0000\n" +"Last-Translator: Irrlicht \n" "Language-Team: French \n" "Language: fr\n" @@ -402,7 +402,7 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:shout" msgid "Pleroma shout settings" -msgstr "" +msgstr "Paramètre de Pleroma shout" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -416,7 +416,7 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:streamer" msgid "Settings for notifications streamer" -msgstr "" +msgstr "Paramètres des streamers de notifications" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -429,12 +429,14 @@ msgstr "Paramètres liés au schémas d'URI" msgctxt "config description at :pleroma-:web_cache_ttl" msgid "The expiration time for the web responses cache. Values should be in milliseconds or `nil` to disable expiration." msgstr "" +"Temps d'expiration pour le cache des réponses web. Les valeurs doivent être " +"en millisecondes ou `nil`pour désactiver l'expiration." #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:welcome" msgid "Welcome messages settings" -msgstr "" +msgstr "Paramètres de messages de bienvenue" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -458,133 +460,147 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Captcha" msgid "Captcha-related settings" -msgstr "" +msgstr "Paramètres liés au Captcha" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Captcha.Kocaptcha" msgid "Kocaptcha is a very simple captcha service with a single API endpoint, the source code is here: https://github.com/koto-bank/kocaptcha. The default endpoint (https://captcha.kotobank.ch) is hosted by the developer." msgstr "" +"Kocaptcha est un service de captcha très simple avec une API n'utilisant " +"qu'une seule ressource, le code source est ici: https://github.com/koto-bank/" +"kocaptcha.\n" +"La ressource par défaut (https://captcha.kotobank.ch) est hébergée par son " +"développeur." #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Emails.Mailer" msgid "Mailer-related settings" -msgstr "" +msgstr "Paramètres liés à l'envoyeur d'email" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Emails.NewUsersDigestEmail" msgid "New users admin email digest" -msgstr "" +msgstr "Email à l'administrateur de résumé des nouveaux utilisateurs" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Emails.UserEmail" msgid "Email template settings" -msgstr "" +msgstr "Paramètres de template d'email" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Formatter" msgid "Configuration for Pleroma's link formatter which parses mentions, hashtags, and URLs." msgstr "" +"Configuration du formateur de lien Pleroma qui interprète les mentions, " +"hashtags et URLs." #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.ScheduledActivity" msgid "Scheduled activities settings" -msgstr "" +msgstr "Paramètres des activités planifiées" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Upload" msgid "Upload general settings" -msgstr "" +msgstr "Paramètres généraux d'upload" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Upload.Filter.AnonymizeFilename" msgid "Filter replaces the filename of the upload" -msgstr "" +msgstr "Filtre de remplacement du nom de fichier de l'upload" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Upload.Filter.Mogrify" msgid "Uploads mogrify filter settings" -msgstr "" +msgstr "Paramètres de filtre mogrify pour les uploads" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Uploaders.Local" msgid "Local uploader-related settings" -msgstr "" +msgstr "Paramètres liés aux uploads locaux" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Uploaders.S3" msgid "S3 uploader-related settings" -msgstr "" +msgstr "Paramètres liés à l'uploader S3" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.User.Backup" msgid "Account Backup" -msgstr "" +msgstr "Sauvegarde de Comptes" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Http" msgid "HTTP invalidate settings" -msgstr "" +msgstr "Paramètres d'invalidation HTTP" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Script" msgid "Invalidation script settings" -msgstr "" +msgstr "Paramètres du script d'invalidation" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Web.Metadata" msgid "Metadata-related settings" -msgstr "" +msgstr "Paramètres liés aux métadonnées" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Web.Plugs.RemoteIp" msgid "`Pleroma.Web.Plugs.RemoteIp` is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.\n**If your instance is not behind at least one reverse proxy, you should not enable this plug.**\n" msgstr "" +"`Pleroma.Web.Plugs.RemoteIp` est un shim pour invoquer " +"[`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) mais avec une " +"configuration au runtime.\n" +"**Si votre instance n'est pas derrière au moins un proxy inverse, vous ne " +"devriez pas activer ce plug.**\n" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Web.Preload" msgid "Preload-related settings" -msgstr "" +msgstr "Paramètres liés au préchargement" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Workers.PurgeExpiredActivity" msgid "Expired activities settings" -msgstr "" +msgstr "Paramètres des activités expirées" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :prometheus-Pleroma.Web.Endpoint.MetricsExporter" msgid "Prometheus app metrics endpoint configuration" -msgstr "" +msgstr "Configuration des endpoints de métriques Prometheus" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :web_push_encryption-:vapid_details" msgid "Web Push Notifications configuration. You can use the mix task mix web_push.gen.keypair to generate it." msgstr "" +"Configuration des notifications Web Push. Vous pouvez utiliser la tâche mix " +"web_push.gen.keypair pour le générer." #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config label at :ex_aws-:s3" msgid "S3" -msgstr "" +msgstr "S3" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -596,13 +612,13 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config label at :logger-:ex_syslogger" msgid "ExSyslogger" -msgstr "" +msgstr "ExSyslogger" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:activitypub" msgid "ActivityPub" -msgstr "" +msgstr "ActivityPub" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -620,151 +636,151 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:connections_pool" msgid "Connections pool" -msgstr "" +msgstr "Pool de connexions" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:email_notifications" msgid "Email notifications" -msgstr "" +msgstr "Notifications email" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:emoji" msgid "Emoji" -msgstr "" +msgstr "Emoji" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:features" msgid "Features" -msgstr "" +msgstr "Fonctionnalités" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:feed" msgid "Feed" -msgstr "" +msgstr "Flux" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontend_configurations" msgid "Frontend configurations" -msgstr "" +msgstr "Configurations des frontends" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontends" msgid "Frontends" -msgstr "" +msgstr "Frontends" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:gopher" msgid "Gopher" -msgstr "" +msgstr "Gopher" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:hackney_pools" msgid "Hackney pools" -msgstr "" +msgstr "Bacs (pools) d'Hackney" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:http" msgid "HTTP" -msgstr "" +msgstr "HTTP" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:http_security" msgid "HTTP security" -msgstr "" +msgstr "Sécurité HTTP" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance" msgid "Instance" -msgstr "" +msgstr "Instance" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instances_favicons" msgid "Instances favicons" -msgstr "" +msgstr "Favicons des instances" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:ldap" msgid "LDAP" -msgstr "" +msgstr "LDAP" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:majic_pool" msgid "Majic pool" -msgstr "" +msgstr "Bac (pool) de Majic" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:manifest" msgid "Manifest" -msgstr "" +msgstr "Manifeste" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:markup" msgid "Markup Settings" -msgstr "" +msgstr "Paramètres des Balises" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:media_preview_proxy" msgid "Media preview proxy" -msgstr "" +msgstr "Proxy de prévisualisation média" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:media_proxy" msgid "Media proxy" -msgstr "" +msgstr "Proxy média" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:modules" msgid "Modules" -msgstr "" +msgstr "Modules" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf" msgid "MRF" -msgstr "" +msgstr "MRF" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_activity_expiration" msgid "MRF Activity Expiration Policy" -msgstr "" +msgstr "Politique MRF d'Expiration des Activités" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_follow_bot" msgid "MRF FollowBot Policy" -msgstr "" +msgstr "Politique MRF FollowBot" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_hashtag" msgid "MRF Hashtag" -msgstr "" +msgstr "Politique MRF hashtag" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_hellthread" msgid "MRF Hellthread" -msgstr "" +msgstr "MRF Hellthread" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format From 7fff19cbe4f337deb2e4b58bdb0223d4b553c120 Mon Sep 17 00:00:00 2001 From: Neko Nekowazarashi Date: Wed, 30 Jul 2025 13:30:45 +0000 Subject: [PATCH 128/391] Translated using Weblate (Indonesian) Currently translated at 77.8% (74 of 95 strings) Translation: Pleroma/Pleroma Backend (domain errors) Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-backend-domain-errors/id/ --- priv/gettext/id/LC_MESSAGES/errors.po | 58 +++++++++++++-------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/priv/gettext/id/LC_MESSAGES/errors.po b/priv/gettext/id/LC_MESSAGES/errors.po index bc98273be..5bb9d56c6 100644 --- a/priv/gettext/id/LC_MESSAGES/errors.po +++ b/priv/gettext/id/LC_MESSAGES/errors.po @@ -3,16 +3,16 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-08-15 15:40+0000\n" -"PO-Revision-Date: 2021-09-03 06:45+0000\n" -"Last-Translator: @liimee \n" +"PO-Revision-Date: 2025-07-31 05:58+0000\n" +"Last-Translator: Neko Nekowazarashi \n" "Language-Team: Indonesian \n" +"pleroma-backend-domain-errors/id/>\n" "Language: id\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" -"X-Generator: Weblate 4.6.2\n" +"X-Generator: Weblate 4.13.1\n" ## This file is a PO Template file. ## @@ -41,7 +41,7 @@ msgstr "memiliki format yang tidak valid" ## From Ecto.Changeset.validate_subset/3 msgid "has an invalid entry" -msgstr "" +msgstr "ada yang tidak valid" ## From Ecto.Changeset.validate_exclusion/3 msgid "is reserved" @@ -49,7 +49,7 @@ msgstr "" ## From Ecto.Changeset.validate_confirmation/3 msgid "does not match confirmation" -msgstr "" +msgstr "tidak sama dengan konfirmasi" ## From Ecto.Changeset.no_assoc_constraint/3 msgid "is still associated with this entry" @@ -61,19 +61,19 @@ msgstr "" ## From Ecto.Changeset.validate_length/3 msgid "should be %{count} character(s)" msgid_plural "should be %{count} character(s)" -msgstr[0] "harus memiliki %{count} karakter" +msgstr[0] "harus ada %{count} karakter" msgid "should have %{count} item(s)" msgid_plural "should have %{count} item(s)" -msgstr[0] "harus memiliki %{count} item" +msgstr[0] "harus ada %{count} item" msgid "should be at least %{count} character(s)" msgid_plural "should be at least %{count} character(s)" -msgstr[0] "harus memiliki sekurang-kurangnya %{count} karakter" +msgstr[0] "harus ada sekurang-kurangnya %{count} karakter" msgid "should have at least %{count} item(s)" msgid_plural "should have at least %{count} item(s)" -msgstr[0] "harus memiliki sekurang-kurangnya %{count} item" +msgstr[0] "harus ada sekurang-kurangnya %{count} item" msgid "should be at most %{count} character(s)" msgid_plural "should be at most %{count} character(s)" @@ -112,7 +112,7 @@ msgstr "Sudah memilih" #: lib/pleroma/web/oauth/oauth_controller.ex:359 #, elixir-format msgid "Bad request" -msgstr "Permintaan buruk (bad request)" +msgstr "Permintaan buruk" #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:426 #, elixir-format @@ -133,7 +133,7 @@ msgstr "Tidak dapat mencari pengguna" #: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:61 #, elixir-format msgid "Can't get favorites" -msgstr "Tidak dapat mendapatkan favorit" +msgstr "Tidak dapat mengambil favorit" #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:438 #, elixir-format @@ -143,7 +143,7 @@ msgstr "Tidak dapat menyukai objek" #: lib/pleroma/web/common_api/utils.ex:563 #, elixir-format msgid "Cannot post an empty status without attachments" -msgstr "Tidak dapat memposting status kosong tanpa lampiran" +msgstr "Tidak dapat mempos status kosong tanpa lampiran" #: lib/pleroma/web/common_api/utils.ex:511 #, elixir-format @@ -206,7 +206,7 @@ msgstr "CAPTCHA tidak valid" #: lib/pleroma/web/oauth/oauth_controller.ex:568 #, elixir-format msgid "Invalid credentials" -msgstr "Kredensian tidak valid" +msgstr "Kredensial tidak valid" #: lib/pleroma/plugs/ensure_authenticated_plug.ex:38 #, elixir-format @@ -279,12 +279,12 @@ msgstr "" #: lib/pleroma/web/ostatus/ostatus_controller.ex:149 #, elixir-format msgid "Something went wrong" -msgstr "Sesuatu yang salah terjadi" +msgstr "Ada sesuatu yang salah" #: lib/pleroma/web/common_api/activity_draft.ex:107 #, elixir-format msgid "The message visibility must be direct" -msgstr "Visibilitas pesan harus langsung" +msgstr "Ketampakan pesan harus langsung" #: lib/pleroma/web/common_api/utils.ex:573 #, elixir-format @@ -294,7 +294,7 @@ msgstr "Status lebih dari batas karakter" #: lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex:31 #, elixir-format msgid "This resource requires authentication." -msgstr "" +msgstr "Autentikasi diperlukan untuk hal ini." #: lib/pleroma/plugs/rate_limiter/rate_limiter.ex:206 #, elixir-format @@ -314,7 +314,7 @@ msgstr "" #: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:485 #, elixir-format msgid "You can't revoke your own admin status." -msgstr "" +msgstr "Anda tidak dapat mencabut status admin Anda." #: lib/pleroma/web/oauth/oauth_controller.ex:221 #: lib/pleroma/web/oauth/oauth_controller.ex:308 @@ -382,7 +382,7 @@ msgstr "Gagal" #: lib/pleroma/web/oauth/oauth_controller.ex:410 #, elixir-format msgid "Failed to authenticate: %{message}." -msgstr "Gagal mengotentikasi: %{message}." +msgstr "Gagal mengautentikasi: %{message}." #: lib/pleroma/web/oauth/oauth_controller.ex:441 #, elixir-format @@ -392,7 +392,7 @@ msgstr "Gagal menyiapkan akun pengguna." #: lib/pleroma/plugs/oauth_scopes_plug.ex:38 #, elixir-format msgid "Insufficient permissions: %{permissions}." -msgstr "" +msgstr "Izin tidak cukup: %{permissions}." #: lib/pleroma/plugs/uploaded_media.ex:104 #, elixir-format @@ -418,12 +418,12 @@ msgstr "" #: lib/pleroma/web/oauth/oauth_controller.ex:172 #, elixir-format msgid "This action is outside the authorized scopes" -msgstr "" +msgstr "Tindakan ini diluar jangkauan yang terotorisasi" #: lib/pleroma/web/oauth/fallback_controller.ex:14 #, elixir-format msgid "Unknown error, please check the details and try again." -msgstr "Kesalahan tidak dikenal, harap periksa keterangannya dan coba lagi." +msgstr "Kesalahan tidak dikenal, harap periksa detailnya dan coba lagi." #: lib/pleroma/web/oauth/oauth_controller.ex:119 #: lib/pleroma/web/oauth/oauth_controller.ex:158 @@ -444,7 +444,7 @@ msgstr "" #: lib/pleroma/web/uploader_controller.ex:23 #, elixir-format msgid "bad request" -msgstr "permintaan buruk (bad request)" +msgstr "permintaan buruk" #: lib/pleroma/web/twitter_api/twitter_api.ex:103 #, elixir-format @@ -469,7 +469,7 @@ msgstr "CAPTCHA Tidak Valid (Parameter kurang: %{name})" #: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:92 #, elixir-format msgid "List not found" -msgstr "" +msgstr "Daftar tidak ditemukan" #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:123 #, elixir-format @@ -480,7 +480,7 @@ msgstr "Parameter kurang: %{name}" #: lib/pleroma/web/oauth/oauth_controller.ex:321 #, elixir-format msgid "Password reset is required" -msgstr "" +msgstr "Diperlukan atur ulang kata sandi" #: lib/pleroma/tests/auth_test_controller.ex:9 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:6 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:6 @@ -522,7 +522,7 @@ msgstr "" #: lib/pleroma/plugs/ensure_authenticated_plug.ex:28 #, elixir-format msgid "Two-factor authentication enabled, you must use a access token." -msgstr "Otentikasi dua-faktor diaktifkan, Anda harus menggunakan token akses." +msgstr "Autentikasi dua langkah diaktifkan, Anda harus menggunakan token akses." #: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:210 #, elixir-format @@ -552,7 +552,7 @@ msgstr "" #: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61 #, elixir-format msgid "Web push subscription is disabled on this Pleroma instance" -msgstr "" +msgstr "Langganan push web dinonaktifkan untuk peladen Pleroma ini" #: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:451 #, elixir-format @@ -562,7 +562,7 @@ msgstr "Anda tidak bisa mencabut status admin/moderator Anda sendiri." #: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:126 #, elixir-format msgid "authorization required for timeline view" -msgstr "" +msgstr "Otorisasi diperlukan untuk tampilan linimasa" #: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:24 #, elixir-format @@ -572,7 +572,7 @@ msgstr "Akses ditolak" #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:282 #, elixir-format msgid "This API requires an authenticated user" -msgstr "" +msgstr "API ini memerlukan pengguna terautentikasi" #: lib/pleroma/plugs/user_is_admin_plug.ex:21 #, elixir-format From 526365364bf98da6172f60732396e5a87178b1f7 Mon Sep 17 00:00:00 2001 From: Neko Nekowazarashi Date: Mon, 11 Aug 2025 15:40:00 +0000 Subject: [PATCH 129/391] Added translation using Weblate (Indonesian) --- priv/gettext/id/LC_MESSAGES/posix_errors.po | 163 ++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 priv/gettext/id/LC_MESSAGES/posix_errors.po diff --git a/priv/gettext/id/LC_MESSAGES/posix_errors.po b/priv/gettext/id/LC_MESSAGES/posix_errors.po new file mode 100644 index 000000000..f5ea93285 --- /dev/null +++ b/priv/gettext/id/LC_MESSAGES/posix_errors.po @@ -0,0 +1,163 @@ +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-08-11 18:40+0300\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: id\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Translate Toolkit 3.7.2\n" + +## This file is a PO Template file. +## +## `msgid`s here are often extracted from source code. +## Add new translations manually only if they're dynamic +## translations that can't be statically extracted. +## +## Run `mix gettext.extract` to bring this file up to +## date. Leave `msgstr`s empty as changing them here as no +## effect: edit them in PO (`.po`) files instead. +msgid "eperm" +msgstr "" + +msgid "eacces" +msgstr "" + +msgid "eagain" +msgstr "" + +msgid "ebadf" +msgstr "" + +msgid "ebadmsg" +msgstr "" + +msgid "ebusy" +msgstr "" + +msgid "edeadlk" +msgstr "" + +msgid "edeadlock" +msgstr "" + +msgid "edquot" +msgstr "" + +msgid "eexist" +msgstr "" + +msgid "efault" +msgstr "" + +msgid "efbig" +msgstr "" + +msgid "eftype" +msgstr "" + +msgid "eintr" +msgstr "" + +msgid "einval" +msgstr "" + +msgid "eio" +msgstr "" + +msgid "eisdir" +msgstr "" + +msgid "eloop" +msgstr "" + +msgid "emfile" +msgstr "" + +msgid "emlink" +msgstr "" + +msgid "emultihop" +msgstr "" + +msgid "enametoolong" +msgstr "" + +msgid "enfile" +msgstr "" + +msgid "enobufs" +msgstr "" + +msgid "enodev" +msgstr "" + +msgid "enolck" +msgstr "" + +msgid "enolink" +msgstr "" + +msgid "enoent" +msgstr "" + +msgid "enomem" +msgstr "" + +msgid "enospc" +msgstr "" + +msgid "enosr" +msgstr "" + +msgid "enostr" +msgstr "" + +msgid "enosys" +msgstr "" + +msgid "enotblk" +msgstr "" + +msgid "enotdir" +msgstr "" + +msgid "enotsup" +msgstr "" + +msgid "enxio" +msgstr "" + +msgid "eopnotsupp" +msgstr "" + +msgid "eoverflow" +msgstr "" + +msgid "epipe" +msgstr "" + +msgid "erange" +msgstr "" + +msgid "erofs" +msgstr "" + +msgid "espipe" +msgstr "" + +msgid "esrch" +msgstr "" + +msgid "estale" +msgstr "" + +msgid "etxtbsy" +msgstr "" + +msgid "exdev" +msgstr "" From 4fef91030308b0d73d085cb10b2e49790f322ca1 Mon Sep 17 00:00:00 2001 From: Neko Nekowazarashi Date: Mon, 11 Aug 2025 15:40:23 +0000 Subject: [PATCH 130/391] Added translation using Weblate (Indonesian) --- priv/gettext/id/LC_MESSAGES/static_pages.po | 574 ++++++++++++++++++++ 1 file changed, 574 insertions(+) create mode 100644 priv/gettext/id/LC_MESSAGES/static_pages.po diff --git a/priv/gettext/id/LC_MESSAGES/static_pages.po b/priv/gettext/id/LC_MESSAGES/static_pages.po new file mode 100644 index 000000000..e2565888a --- /dev/null +++ b/priv/gettext/id/LC_MESSAGES/static_pages.po @@ -0,0 +1,574 @@ +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-08-11 18:40+0300\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: id\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Translate Toolkit 3.7.2\n" + +## This file is a PO Template file. +## +## "msgid"s here are often extracted from source code. +## Add new translations manually only if they're dynamic +## translations that can't be statically extracted. +## +## Run "mix gettext.extract" to bring this file up to +## date. Leave "msgstr"s empty as changing them here as no +## effect: edit them in PO (.po) files instead. + +#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:9 +#, elixir-autogen, elixir-format +msgctxt "remote follow authorization button" +msgid "Authorize" +msgstr "" + +#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:2 +#, elixir-autogen, elixir-format +msgctxt "remote follow error" +msgid "Error fetching user" +msgstr "" + +#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:4 +#, elixir-autogen, elixir-format +msgctxt "remote follow header" +msgid "Remote follow" +msgstr "" + +#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:8 +#, elixir-autogen, elixir-format +msgctxt "placeholder text for auth code entry" +msgid "Authentication code" +msgstr "" + +#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:10 +#, elixir-autogen, elixir-format +msgctxt "placeholder text for password entry" +msgid "Password" +msgstr "" + +#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:8 +#, elixir-autogen, elixir-format +msgctxt "placeholder text for username entry" +msgid "Username" +msgstr "" + +#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:13 +#, elixir-autogen, elixir-format +msgctxt "remote follow authorization button for login" +msgid "Authorize" +msgstr "" + +#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:12 +#, elixir-autogen, elixir-format +msgctxt "remote follow authorization button for mfa" +msgid "Authorize" +msgstr "" + +#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:2 +#, elixir-autogen, elixir-format +msgctxt "remote follow error" +msgid "Error following account" +msgstr "" + +#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:4 +#, elixir-autogen, elixir-format +msgctxt "remote follow header, need login" +msgid "Log in to follow" +msgstr "" + +#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:4 +#, elixir-autogen, elixir-format +msgctxt "remote follow mfa header" +msgid "Two-factor authentication" +msgstr "" + +#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:4 +#, elixir-autogen, elixir-format +msgctxt "remote follow success" +msgid "Account followed!" +msgstr "" + +#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:7 +#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:7 +#, elixir-autogen, elixir-format +msgctxt "placeholder text for account id" +msgid "Your account ID, e.g. lain@quitter.se" +msgstr "" + +#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:8 +#, elixir-autogen, elixir-format +msgctxt "remote follow authorization button for following with a remote account" +msgid "Follow" +msgstr "" + +#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:2 +#, elixir-autogen, elixir-format +msgctxt "remote follow error" +msgid "Error: %{error}" +msgstr "" + +#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:4 +#, elixir-autogen, elixir-format +msgctxt "remote follow header" +msgid "Remotely follow %{nickname}" +msgstr "" + +#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:12 +#, elixir-autogen, elixir-format +msgctxt "password reset button" +msgid "Reset" +msgstr "" + +#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:4 +#, elixir-autogen, elixir-format +msgctxt "password reset failed homepage link" +msgid "Homepage" +msgstr "" + +#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:1 +#, elixir-autogen, elixir-format +msgctxt "password reset failed message" +msgid "Password reset failed" +msgstr "" + +#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:8 +#, elixir-autogen, elixir-format +msgctxt "password reset form confirm password prompt" +msgid "Confirmation" +msgstr "" + +#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:4 +#, elixir-autogen, elixir-format +msgctxt "password reset form password prompt" +msgid "Password" +msgstr "" + +#: lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex:1 +#, elixir-autogen, elixir-format +msgctxt "password reset invalid token message" +msgid "Invalid Token" +msgstr "" + +#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:2 +#, elixir-autogen, elixir-format +msgctxt "password reset successful homepage link" +msgid "Homepage" +msgstr "" + +#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:1 +#, elixir-autogen, elixir-format +msgctxt "password reset successful message" +msgid "Password changed!" +msgstr "" + +#: lib/pleroma/web/templates/feed/feed/tag.atom.eex:12 +#: lib/pleroma/web/templates/feed/feed/tag.rss.eex:8 +#, elixir-autogen, elixir-format +msgctxt "tag feed description" +msgid "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse." +msgstr "" + +#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:1 +#, elixir-autogen, elixir-format +msgctxt "oauth authorization exists page title" +msgid "Authorization exists" +msgstr "" + +#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:32 +#, elixir-autogen, elixir-format +msgctxt "oauth authorize approve button" +msgid "Approve" +msgstr "" + +#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:30 +#, elixir-autogen, elixir-format +msgctxt "oauth authorize cancel button" +msgid "Cancel" +msgstr "" + +#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:23 +#, elixir-autogen, elixir-format +msgctxt "oauth authorize message" +msgid "Application %{client_name} is requesting access to your account." +msgstr "" + +#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:1 +#, elixir-autogen, elixir-format +msgctxt "oauth authorized page title" +msgid "Successfully authorized" +msgstr "" + +#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:1 +#, elixir-autogen, elixir-format +msgctxt "oauth external provider page title" +msgid "Sign in with external provider" +msgstr "" + +#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:13 +#, elixir-autogen, elixir-format +msgctxt "oauth external provider sign in button" +msgid "Sign in with %{strategy}" +msgstr "" + +#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:54 +#, elixir-autogen, elixir-format +msgctxt "oauth login button" +msgid "Log In" +msgstr "" + +#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:51 +#, elixir-autogen, elixir-format +msgctxt "oauth login password prompt" +msgid "Password" +msgstr "" + +#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:47 +#, elixir-autogen, elixir-format +msgctxt "oauth login username prompt" +msgid "Username" +msgstr "" + +#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:39 +#, elixir-autogen, elixir-format +msgctxt "oauth register nickname prompt" +msgid "Pleroma Handle" +msgstr "" + +#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:37 +#, elixir-autogen, elixir-format +msgctxt "oauth register nickname unchangeable warning" +msgid "Choose carefully! You won't be able to change this later. You will be able to change your display name, though." +msgstr "" + +#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:18 +#, elixir-autogen, elixir-format +msgctxt "oauth register page email prompt" +msgid "Email" +msgstr "" + +#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:10 +#, elixir-autogen, elixir-format +msgctxt "oauth register page fill form prompt" +msgid "If you'd like to register a new account, please provide the details below." +msgstr "" + +#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:35 +#, elixir-autogen, elixir-format +msgctxt "oauth register page login button" +msgid "Proceed as existing user" +msgstr "" + +#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:31 +#, elixir-autogen, elixir-format +msgctxt "oauth register page login password prompt" +msgid "Password" +msgstr "" + +#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:24 +#, elixir-autogen, elixir-format +msgctxt "oauth register page login prompt" +msgid "Alternatively, sign in to connect to existing account." +msgstr "" + +#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:27 +#, elixir-autogen, elixir-format +msgctxt "oauth register page login username prompt" +msgid "Name or email" +msgstr "" + +#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:14 +#, elixir-autogen, elixir-format +msgctxt "oauth register page nickname prompt" +msgid "Nickname" +msgstr "" + +#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:22 +#, elixir-autogen, elixir-format +msgctxt "oauth register page register button" +msgid "Proceed as new user" +msgstr "" + +#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:8 +#, elixir-autogen, elixir-format +msgctxt "oauth register page title" +msgid "Registration Details" +msgstr "" + +#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:36 +#, elixir-autogen, elixir-format +msgctxt "oauth register page title" +msgid "This is the first time you visit! Please enter your Pleroma handle." +msgstr "" + +#: lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex:2 +#, elixir-autogen, elixir-format +msgctxt "oauth scopes message" +msgid "The following permissions will be granted" +msgstr "" + +#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:2 +#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:2 +#, elixir-autogen, elixir-format +msgctxt "oauth token code message" +msgid "Token code is
%{token}" +msgstr "" + +#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:12 +#, elixir-autogen, elixir-format +msgctxt "mfa auth code prompt" +msgid "Authentication code" +msgstr "" + +#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:8 +#, elixir-autogen, elixir-format +msgctxt "mfa auth page title" +msgid "Two-factor authentication" +msgstr "" + +#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:23 +#, elixir-autogen, elixir-format +msgctxt "mfa auth page use recovery code link" +msgid "Enter a two-factor recovery code" +msgstr "" + +#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:20 +#, elixir-autogen, elixir-format +msgctxt "mfa auth verify code button" +msgid "Verify" +msgstr "" + +#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:8 +#, elixir-autogen, elixir-format +msgctxt "mfa recover page title" +msgid "Two-factor recovery" +msgstr "" + +#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:12 +#, elixir-autogen, elixir-format +msgctxt "mfa recover recovery code prompt" +msgid "Recovery code" +msgstr "" + +#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:23 +#, elixir-autogen, elixir-format +msgctxt "mfa recover use 2fa code link" +msgid "Enter a two-factor code" +msgstr "" + +#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:20 +#, elixir-autogen, elixir-format +msgctxt "mfa recover verify recovery code button" +msgid "Verify" +msgstr "" + +#: lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex:8 +#, elixir-autogen, elixir-format +msgctxt "static fe profile page remote follow button" +msgid "Remote follow" +msgstr "" + +#: lib/pleroma/web/templates/email/digest.html.eex:163 +#, elixir-autogen, elixir-format +msgctxt "digest email header line" +msgid "Hey %{nickname}, here is what you've missed!" +msgstr "" + +#: lib/pleroma/web/templates/email/digest.html.eex:544 +#, elixir-autogen, elixir-format +msgctxt "digest email receiver address" +msgid "The email address you are subscribed as is %{email}. " +msgstr "" + +#: lib/pleroma/web/templates/email/digest.html.eex:538 +#, elixir-autogen, elixir-format +msgctxt "digest email sending reason" +msgid "You have received this email because you have signed up to receive digest emails from %{instance} Pleroma instance." +msgstr "" + +#: lib/pleroma/web/templates/email/digest.html.eex:547 +#, elixir-autogen, elixir-format +msgctxt "digest email unsubscribe action" +msgid "To unsubscribe, please go %{here}." +msgstr "" + +#: lib/pleroma/web/templates/email/digest.html.eex:547 +#, elixir-autogen, elixir-format +msgctxt "digest email unsubscribe action link text" +msgid "here" +msgstr "" + +#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_failure.html.eex:1 +#, elixir-autogen, elixir-format +msgctxt "mailer unsubscribe failed message" +msgid "UNSUBSCRIBE FAILURE" +msgstr "" + +#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_success.html.eex:1 +#, elixir-autogen, elixir-format +msgctxt "mailer unsubscribe successful message" +msgid "UNSUBSCRIBE SUCCESSFUL" +msgstr "" + +#: lib/pleroma/web/templates/email/digest.html.eex:385 +#, elixir-format +msgctxt "new followers count header" +msgid "%{count} New Follower" +msgid_plural "%{count} New Followers" +msgstr[0] "" +msgstr[1] "" + +#: lib/pleroma/emails/user_email.ex:356 +#, elixir-autogen, elixir-format +msgctxt "account archive email body - self-requested" +msgid "

You requested a full backup of your Pleroma account. It's ready for download:

\n

%{download_url}

\n" +msgstr "" + +#: lib/pleroma/emails/user_email.ex:384 +#, elixir-autogen, elixir-format +msgctxt "account archive email subject" +msgid "Your account archive is ready" +msgstr "" + +#: lib/pleroma/emails/user_email.ex:188 +#, elixir-autogen, elixir-format +msgctxt "approval pending email body" +msgid "

Awaiting Approval

\n

Your account at %{instance_name} is being reviewed by staff. You will receive another email once your account is approved.

\n" +msgstr "" + +#: lib/pleroma/emails/user_email.ex:202 +#, elixir-autogen, elixir-format +msgctxt "approval pending email subject" +msgid "Your account is awaiting approval" +msgstr "" + +#: lib/pleroma/emails/user_email.ex:158 +#, elixir-autogen, elixir-format +msgctxt "confirmation email body" +msgid "

Thank you for registering on %{instance_name}

\n

Email confirmation is required to activate the account.

\n

Please click the following link to activate your account.

\n" +msgstr "" + +#: lib/pleroma/emails/user_email.ex:174 +#, elixir-autogen, elixir-format +msgctxt "confirmation email subject" +msgid "%{instance_name} account confirmation" +msgstr "" + +#: lib/pleroma/emails/user_email.ex:310 +#, elixir-autogen, elixir-format +msgctxt "digest email subject" +msgid "Your digest from %{instance_name}" +msgstr "" + +#: lib/pleroma/emails/user_email.ex:81 +#, elixir-autogen, elixir-format +msgctxt "password reset email body" +msgid "

Reset your password at %{instance_name}

\n

Someone has requested password change for your account at %{instance_name}.

\n

If it was you, visit the following link to proceed: reset password.

\n

If it was someone else, nothing to worry about: your data is secure and your password has not been changed.

\n" +msgstr "" + +#: lib/pleroma/emails/user_email.ex:98 +#, elixir-autogen, elixir-format +msgctxt "password reset email subject" +msgid "Password reset" +msgstr "" + +#: lib/pleroma/emails/user_email.ex:215 +#, elixir-autogen, elixir-format +msgctxt "successful registration email body" +msgid "

Hello @%{nickname},

\n

Your account at %{instance_name} has been registered successfully.

\n

No further action is required to activate your account.

\n" +msgstr "" + +#: lib/pleroma/emails/user_email.ex:231 +#, elixir-autogen, elixir-format +msgctxt "successful registration email subject" +msgid "Account registered on %{instance_name}" +msgstr "" + +#: lib/pleroma/emails/user_email.ex:119 +#, elixir-autogen, elixir-format +msgctxt "user invitation email body" +msgid "

You are invited to %{instance_name}

\n

%{inviter_name} invites you to join %{instance_name}, an instance of Pleroma federated social networking platform.

\n

Click the following link to register: accept invitation.

\n" +msgstr "" + +#: lib/pleroma/emails/user_email.ex:136 +#, elixir-autogen, elixir-format +msgctxt "user invitation email subject" +msgid "Invitation to %{instance_name}" +msgstr "" + +#: lib/pleroma/emails/user_email.ex:53 +#, elixir-autogen, elixir-format +msgctxt "welcome email html body" +msgid "Welcome to %{instance_name}!" +msgstr "" + +#: lib/pleroma/emails/user_email.ex:41 +#, elixir-autogen, elixir-format +msgctxt "welcome email subject" +msgid "Welcome to %{instance_name}!" +msgstr "" + +#: lib/pleroma/emails/user_email.ex:65 +#, elixir-autogen, elixir-format +msgctxt "welcome email text body" +msgid "Welcome to %{instance_name}!" +msgstr "" + +#: lib/pleroma/emails/user_email.ex:368 +#, elixir-autogen, elixir-format +msgctxt "account archive email body - admin requested" +msgid "

Admin @%{admin_nickname} requested a full backup of your Pleroma account. It's ready for download:

\n

%{download_url}

\n" +msgstr "" + +#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:123 +#, elixir-autogen, elixir-format +msgctxt "remote follow error message - unknown error" +msgid "Something went wrong." +msgstr "" + +#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:67 +#, elixir-autogen, elixir-format +msgctxt "remote follow error message - user not found" +msgid "Could not find user" +msgstr "" + +#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:8 +#, elixir-autogen, elixir-format +msgctxt "status interact authorization button" +msgid "Interact" +msgstr "" + +#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:2 +#, elixir-autogen, elixir-format +msgctxt "status interact error" +msgid "Error: %{error}" +msgstr "" + +#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:95 +#, elixir-autogen, elixir-format +msgctxt "status interact error message - status not found" +msgid "Could not find status" +msgstr "" + +#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:144 +#, elixir-autogen, elixir-format +msgctxt "status interact error message - unknown error" +msgid "Something went wrong." +msgstr "" + +#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:4 +#, elixir-autogen, elixir-format +msgctxt "status interact header" +msgid "Interacting with %{nickname}'s %{status_link}" +msgstr "" + +#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:4 +#, elixir-autogen, elixir-format +msgctxt "status interact header - status link text" +msgid "status" +msgstr "" From 086c15b5cd4d431bb54974d470de5b81321ebe38 Mon Sep 17 00:00:00 2001 From: Codimp Date: Fri, 26 Dec 2025 18:18:33 +0000 Subject: [PATCH 131/391] Translated using Weblate (French) Currently translated at 100.0% (80 of 80 strings) Translation: Pleroma/Pleroma Backend (domain errors) Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-backend-domain-errors/fr/ --- priv/gettext/fr/LC_MESSAGES/errors.po | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/priv/gettext/fr/LC_MESSAGES/errors.po b/priv/gettext/fr/LC_MESSAGES/errors.po index 406f98de9..20636cee3 100644 --- a/priv/gettext/fr/LC_MESSAGES/errors.po +++ b/priv/gettext/fr/LC_MESSAGES/errors.po @@ -8,16 +8,15 @@ ## to merge POT files into PO files. msgid "" msgstr "" -"PO-Revision-Date: 2020-05-12 15:52+0000\n" -"Last-Translator: Haelwenn (lanodan) Monnier " -"\n" +"PO-Revision-Date: 2025-12-27 07:08+0000\n" +"Last-Translator: Codimp \n" "Language-Team: French \n" +"pleroma-backend-domain-errors/fr/>\n" "Language: fr\n" "Content-Type: text/plain; charset=UTF-8\n" -"Plural-Forms: nplurals=2; plural=n > 1;\n" -"X-Generator: Weblate 4.0.4\n" "Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 4.13.1\n" msgid "can't be blank" msgstr "ne peut être vide" @@ -357,7 +356,7 @@ msgstr "Ne peut poster dans la boite d'émission de %{nickname} en tant que %{as #: lib/pleroma/web/common_api/common_api.ex:335 #, elixir-format msgid "conversation is already muted" -msgstr "la conversation est déjà baillonée" +msgstr "la conversation est déjà silenciée" #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:192 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:317 @@ -421,7 +420,7 @@ msgstr "Erreur interne" #: lib/pleroma/web/oauth/fallback_controller.ex:29 #, elixir-format msgid "Invalid Username/Password" -msgstr "Nom d'utilisateur/mot de passe invalide" +msgstr "Nom d'utilisateur·ice/mot de passe invalide" #: lib/pleroma/captcha/captcha.ex:107 #, elixir-format From d7a0d97c3687d3a8bac2597a223bb686693aaa8a Mon Sep 17 00:00:00 2001 From: Codimp Date: Fri, 26 Dec 2025 18:20:14 +0000 Subject: [PATCH 132/391] Translated using Weblate (French) Currently translated at 10.8% (106 of 974 strings) Translation: Pleroma/Pleroma Backend (domain config_descriptions) Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-backend-domain-config_descriptions/fr/ --- priv/gettext/fr/LC_MESSAGES/config_descriptions.po | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/priv/gettext/fr/LC_MESSAGES/config_descriptions.po b/priv/gettext/fr/LC_MESSAGES/config_descriptions.po index 56ea1bdf3..2e12baa43 100644 --- a/priv/gettext/fr/LC_MESSAGES/config_descriptions.po +++ b/priv/gettext/fr/LC_MESSAGES/config_descriptions.po @@ -3,8 +3,8 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-07-22 02:09+0300\n" -"PO-Revision-Date: 2025-04-11 01:04+0000\n" -"Last-Translator: Irrlicht \n" +"PO-Revision-Date: 2025-12-27 07:08+0000\n" +"Last-Translator: Codimp \n" "Language-Team: French \n" "Language: fr\n" @@ -280,7 +280,7 @@ msgstr "" "Rejeter, Enlever de TWKN ou marquer comme contenu sensible les messages avec " "des mots-croisillons (sans mettre le # du début)\n" "\n" -"Note: cette politique MRF est toujours activée. Si vous voulez la " +"Note : cette politique MRF est toujours activée. Si vous voulez la " "désactiver, vous devez configurer des listes vides.\n" #: lib/pleroma/docs/translator.ex:5 @@ -468,10 +468,9 @@ msgctxt "config description at :pleroma-Pleroma.Captcha.Kocaptcha" msgid "Kocaptcha is a very simple captcha service with a single API endpoint, the source code is here: https://github.com/koto-bank/kocaptcha. The default endpoint (https://captcha.kotobank.ch) is hosted by the developer." msgstr "" "Kocaptcha est un service de captcha très simple avec une API n'utilisant " -"qu'une seule ressource, le code source est ici: https://github.com/koto-bank/" -"kocaptcha.\n" -"La ressource par défaut (https://captcha.kotobank.ch) est hébergée par son " -"développeur." +"qu'une seule ressource, le code source est ici : https://github.com/" +"koto-bank/kocaptcha. La ressource par défaut (https://captcha.kotobank.ch) " +"est hébergée par son développeur." #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format From 784a319f2eb9b98e6e034c38b462dd1e11390359 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 14 May 2026 00:22:24 +0200 Subject: [PATCH 133/391] Woodpecker CI: Bump unit testing pipeline to Elixir 1.19 --- ...t-testing-elixir-1.18.yaml => unit-testing-elixir-1.19.yaml} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename .woodpecker/{unit-testing-elixir-1.18.yaml => unit-testing-elixir-1.19.yaml} (97%) diff --git a/.woodpecker/unit-testing-elixir-1.18.yaml b/.woodpecker/unit-testing-elixir-1.19.yaml similarity index 97% rename from .woodpecker/unit-testing-elixir-1.18.yaml rename to .woodpecker/unit-testing-elixir-1.19.yaml index 9ad9eebc9..462e2ee5d 100644 --- a/.woodpecker/unit-testing-elixir-1.18.yaml +++ b/.woodpecker/unit-testing-elixir-1.19.yaml @@ -19,7 +19,7 @@ variables: steps: unit-testing-elixir-1.18: - image: elixir:1.18-otp-27-alpine + image: elixir:1.19-otp-28-alpine entrypoint: *script_file_entrypoint environment: MIX_ENV: test From 7575b7abc2b1559dbb2a780ce55e572969d8184f Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 14 May 2026 12:20:27 +0200 Subject: [PATCH 134/391] Woodpecker CI: Do not trigger on Weblate MRs --- .woodpecker/changelog.yaml | 1 + .woodpecker/lint.yaml | 1 + .woodpecker/unit-testing-elixir-1.15.yaml | 1 + .woodpecker/unit-testing-elixir-1.18.yaml | 1 + changelog.d/weblate-ci.skip | 0 5 files changed, 4 insertions(+) create mode 100644 changelog.d/weblate-ci.skip diff --git a/.woodpecker/changelog.yaml b/.woodpecker/changelog.yaml index 64062f17e..a3732c240 100644 --- a/.woodpecker/changelog.yaml +++ b/.woodpecker/changelog.yaml @@ -1,5 +1,6 @@ when: - event: pull_request + evaluate: 'CI_COMMIT_SOURCE_BRANCH != "weblate"' labels: platform: linux/amd64 diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index 0ab7441a8..8a5718c5d 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -1,6 +1,7 @@ when: - event: pull_request path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] + evaluate: 'CI_COMMIT_SOURCE_BRANCH != "weblate"' - event: push branch: ${CI_REPO_DEFAULT_BRANCH} path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] diff --git a/.woodpecker/unit-testing-elixir-1.15.yaml b/.woodpecker/unit-testing-elixir-1.15.yaml index a4a8fc266..a5bcc0ef3 100644 --- a/.woodpecker/unit-testing-elixir-1.15.yaml +++ b/.woodpecker/unit-testing-elixir-1.15.yaml @@ -1,6 +1,7 @@ when: - event: pull_request path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] + evaluate: 'CI_COMMIT_SOURCE_BRANCH != "weblate"' - event: push branch: ${CI_REPO_DEFAULT_BRANCH} path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] diff --git a/.woodpecker/unit-testing-elixir-1.18.yaml b/.woodpecker/unit-testing-elixir-1.18.yaml index 9ad9eebc9..6e576fad9 100644 --- a/.woodpecker/unit-testing-elixir-1.18.yaml +++ b/.woodpecker/unit-testing-elixir-1.18.yaml @@ -1,6 +1,7 @@ when: - event: pull_request path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] + evaluate: 'CI_COMMIT_SOURCE_BRANCH != "weblate"' - event: push branch: ${CI_REPO_DEFAULT_BRANCH} path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] diff --git a/changelog.d/weblate-ci.skip b/changelog.d/weblate-ci.skip new file mode 100644 index 000000000..e69de29bb From 1a13ec539baca66d23db899d92b42c181aaf3149 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 18 May 2026 21:05:46 +0300 Subject: [PATCH 135/391] reorganize mfm attributes in order they're listed in misskey repo, add missing ones --- priv/scrubbers/default.ex | 49 ++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/priv/scrubbers/default.ex b/priv/scrubbers/default.ex index 342ef9944..d5c7340d6 100644 --- a/priv/scrubbers/default.ex +++ b/priv/scrubbers/default.ex @@ -83,48 +83,55 @@ defmodule Pleroma.HTML.Scrubber.Default do "quote-inline", "invisible", "ellipsis", - "mfm-center", - "mfm-flip", - "mfm-font", - "mfm-blur", - "mfm-rotate", - "mfm-x2", - "mfm-x3", - "mfm-x4", - "mfm-position", - "mfm-scale", - "mfm-fg", - "mfm-bg", + "mfm-tada", "mfm-jelly", "mfm-twitch", "mfm-shake", "mfm-spin", "mfm-jump", "mfm-bounce", + "mfm-flip", + "mfm-x2", + "mfm-x3", + "mfm-x4", + "mfm-scale", + "mfm-position", + "mfm-fg", + "mfm-bg", + "mfm-border", + "mfm-font", + "mfm-blur", "mfm-rainbow", - "mfm-tada", - "mfm-sparkle" + "mfm-sparkle", + "mfm-rotate", + "mfm-ruby", + "mfm-unixtime", ]) Meta.allow_tag_with_this_attribute_values(:p, "class", ["quote-inline"]) Meta.allow_tag_with_these_attributes(:span, [ "lang", - "data-mfm-h", - "data-mfm-v", + "data-mfm-speed", + "data-mfm-delay", + "data-mfm-left", + "data-mfm-alternate", "data-mfm-x", "data-mfm-y", - "data-mfm-alternate", - "data-mfm-speed", - "data-mfm-deg", - "data-mfm-left", + "data-mfm-h", + "data-mfm-v", + "data-mfm-color", + "data-mfm-width", + "data-mfm-style", + "data-mfm-radius", + "data-mfm-noclip", "data-mfm-serif", "data-mfm-monospace", "data-mfm-cursive", "data-mfm-fantasy", "data-mfm-emoji", "data-mfm-math", - "data-mfm-color" + "data-mfm-deg", ]) Meta.allow_tag_with_this_attribute_values(:code, "class", ["inline"]) From c428cf43e8683f5428341845629013504d0057bf Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 18 May 2026 21:07:12 +0300 Subject: [PATCH 136/391] "changelog" --- changelog.d/mfm-extend.skip | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 changelog.d/mfm-extend.skip diff --git a/changelog.d/mfm-extend.skip b/changelog.d/mfm-extend.skip new file mode 100644 index 000000000..e69de29bb From 46a28086900600d187348b796c486d11575f22da Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 18 May 2026 21:08:24 +0300 Subject: [PATCH 137/391] extraneous commas --- priv/scrubbers/default.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/priv/scrubbers/default.ex b/priv/scrubbers/default.ex index d5c7340d6..8963d99e8 100644 --- a/priv/scrubbers/default.ex +++ b/priv/scrubbers/default.ex @@ -105,7 +105,7 @@ defmodule Pleroma.HTML.Scrubber.Default do "mfm-sparkle", "mfm-rotate", "mfm-ruby", - "mfm-unixtime", + "mfm-unixtime" ]) Meta.allow_tag_with_this_attribute_values(:p, "class", ["quote-inline"]) @@ -131,7 +131,7 @@ defmodule Pleroma.HTML.Scrubber.Default do "data-mfm-fantasy", "data-mfm-emoji", "data-mfm-math", - "data-mfm-deg", + "data-mfm-deg" ]) Meta.allow_tag_with_this_attribute_values(:code, "class", ["inline"]) From 9dd02ecd5074cc0c164cbae86253d8eea921c787 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Fri, 22 May 2026 16:30:14 +0400 Subject: [PATCH 138/391] Fix WebSocket protocol token handshakes --- changelog.d/mastodon-websocket-protocol.fix | 1 + lib/pleroma/web/endpoint.ex | 7 +- .../web/mastodon_api/websocket_handler.ex | 15 +-- .../web/mastodon_api/websocket_plug.ex | 105 ++++++++++++++++++ .../integration/mastodon_websocket_test.exs | 100 ++++++++++++++--- 5 files changed, 200 insertions(+), 28 deletions(-) create mode 100644 changelog.d/mastodon-websocket-protocol.fix create mode 100644 lib/pleroma/web/mastodon_api/websocket_plug.ex diff --git a/changelog.d/mastodon-websocket-protocol.fix b/changelog.d/mastodon-websocket-protocol.fix new file mode 100644 index 000000000..66dab16ed --- /dev/null +++ b/changelog.d/mastodon-websocket-protocol.fix @@ -0,0 +1 @@ +Echo Mastodon-style `Sec-WebSocket-Protocol` tokens in streaming WebSocket handshakes. diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 36e78e1e2..07b33f866 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -9,8 +9,8 @@ defmodule Pleroma.Web.Endpoint do alias Pleroma.Config - socket("/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, - longpoll: false, + plug(Pleroma.Web.MastodonAPI.WebsocketPlug, + path: "/api/v1/streaming", websocket: [ path: "/", compress: false, @@ -169,8 +169,7 @@ defmodule Pleroma.Web.Endpoint do else: "pleroma_key" extra = - Config.get([__MODULE__, :extra_cookie_attrs]) - |> Enum.join(";") + Enum.join(Config.get([__MODULE__, :extra_cookie_attrs]), ";") # The session will be stored in the cookie and signed, # this means its contents can be read but not tampered with. diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex index 2b698bd5d..3dc862a5a 100644 --- a/lib/pleroma/web/mastodon_api/websocket_handler.ex +++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex @@ -67,9 +67,10 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do @impl Phoenix.Socket.Transport def handle_in({text, [opcode: :text]}, state) do - with {:ok, %{} = event} <- Jason.decode(text) do - handle_client_event(event, state) - else + case Jason.decode(text) do + {:ok, %{} = event} -> + handle_client_event(event, state) + _ -> Logger.error("#{__MODULE__} received non-JSON event: #{inspect(text)}") {:ok, state} @@ -85,11 +86,11 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do def handle_info({:render_with_user, view, template, item, topic}, state) do user = %User{} = User.get_cached_by_ap_id(state.user.ap_id) - unless Streamer.filtered_by_user?(user, item) do + if Streamer.filtered_by_user?(user, item) do + {:ok, state} + else message = view.render(template, item, user, topic) {:push, {:text, message}, %{state | user: user}} - else - {:ok, state} end end @@ -253,7 +254,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do defp find_sec_websocket_protocol(sec_headers) do Enum.find_value(sec_headers, fn - {"sec-websocket-protocol", token} -> token + {"sec-websocket-protocol", protocols} -> protocols |> Plug.Conn.Utils.list() |> List.first() _ -> nil end) end diff --git a/lib/pleroma/web/mastodon_api/websocket_plug.ex b/lib/pleroma/web/mastodon_api/websocket_plug.ex new file mode 100644 index 000000000..58ee913b4 --- /dev/null +++ b/lib/pleroma/web/mastodon_api/websocket_plug.ex @@ -0,0 +1,105 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.WebsocketPlug do + @moduledoc """ + A Phoenix 1.8 compatible WebSocket transport for Mastodon streaming. + + It mirrors Phoenix.Transports.WebSocket, but echoes a successfully authenticated + Mastodon-style Sec-WebSocket-Protocol token so browser clients accept the handshake. + """ + + @behaviour Plug + + import Plug.Conn + + alias Phoenix.Socket.Transport + alias Pleroma.Web.Endpoint + alias Pleroma.Web.MastodonAPI.WebsocketHandler + + @connect_info_opts [:check_csrf] + + @impl Plug + def init(opts) do + path = String.split(Keyword.fetch!(opts, :path), "/", trim: true) + websocket = Keyword.fetch!(opts, :websocket) + config = Transport.load_config(websocket, Phoenix.Transports.WebSocket) + + {path, config} + end + + @impl Plug + def call(%{method: "GET", path_info: path} = conn, {path, opts}) do + conn + |> fetch_query_params() + |> Transport.code_reload(Endpoint, opts) + |> Transport.transport_log(opts[:transport_log]) + |> Transport.check_origin(WebsocketHandler, Endpoint, opts) + |> connect(opts) + end + + def call(%{path_info: path} = conn, {path, _opts}) do + conn + |> send_resp(400, "") + |> halt() + end + + def call(conn, _opts), do: conn + + defp connect(%{halted: true} = conn, _opts), do: conn + + defp connect(%{params: params} = conn, opts) do + keys = Keyword.get(opts, :connect_info, []) + + connect_info = + Transport.connect_info(conn, Endpoint, keys, Keyword.take(opts, @connect_info_opts)) + + config = %{ + endpoint: Endpoint, + transport: :websocket, + options: opts, + params: params, + connect_info: connect_info + } + + case WebsocketHandler.connect(config) do + {:ok, arg} -> + try do + conn + |> echo_sec_websocket_protocol() + |> WebSockAdapter.upgrade(WebsocketHandler, arg, opts) + |> halt() + rescue + e in WebSockAdapter.UpgradeError -> + conn + |> send_resp(400, e.message) + |> halt() + end + + :error -> + conn + |> send_resp(403, "") + |> halt() + + {:error, reason} -> + {m, f, args} = opts[:error_handler] + + halt(apply(m, f, [conn, reason | args])) + end + end + + defp echo_sec_websocket_protocol(conn) do + case get_req_header(conn, "sec-websocket-protocol") do + [protocols | _] -> + case Plug.Conn.Utils.list(protocols) do + [protocol | _] -> put_resp_header(conn, "sec-websocket-protocol", protocol) + nil -> conn + [] -> conn + end + + [] -> + conn + end + end +end diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs index 078bb643c..de88e5002 100644 --- a/test/pleroma/integration/mastodon_websocket_test.exs +++ b/test/pleroma/integration/mastodon_websocket_test.exs @@ -11,6 +11,7 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do alias Pleroma.Integration.WebsocketClient alias Pleroma.Web.CommonAPI + alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.OAuth @moduletag needs_streamer: true, capture_log: true @@ -31,6 +32,48 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do WebsocketClient.start_link(self(), path, headers) end + defp raw_websocket_handshake(qs, headers) do + uri = URI.parse(@path <> qs) + port = uri.port || 80 + path = uri.path <> if(uri.query, do: "?" <> uri.query, else: "") + + default_headers = [ + {"host", "#{uri.host}:#{port}"}, + {"upgrade", "websocket"}, + {"connection", "Upgrade"}, + {"sec-websocket-key", Base.encode64(:crypto.strong_rand_bytes(16))}, + {"sec-websocket-version", "13"} + ] + + request = [ + "GET #{path} HTTP/1.1\r\n", + Enum.map(default_headers ++ headers, fn {name, value} -> "#{name}: #{value}\r\n" end), + "\r\n" + ] + + with {:ok, socket} <- + :gen_tcp.connect(String.to_charlist(uri.host), port, [:binary, active: false], 1_000), + :ok <- :gen_tcp.send(socket, request), + {:ok, response} <- :gen_tcp.recv(socket, 0, 1_000) do + :gen_tcp.close(socket) + {:ok, parse_http_response(response)} + end + end + + defp parse_http_response(response) do + [headers | _] = String.split(response, "\r\n\r\n", parts: 2) + [status_line | header_lines] = String.split(headers, "\r\n") + [_, status | _] = String.split(status_line, " ") + + headers = + Enum.map(header_lines, fn line -> + [name, value] = String.split(line, ":", parts: 2) + {String.downcase(name), String.trim(value)} + end) + + %{status: String.to_integer(status), headers: headers} + end + defp decode_json(json) do with {:ok, %{"event" => event, "payload" => payload_text}} <- Jason.decode(json), {:ok, payload} <- Jason.decode(payload_text) do @@ -85,9 +128,7 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do assert json["payload"] assert {:ok, json} = Jason.decode(json["payload"]) - view_json = - Pleroma.Web.MastodonAPI.StatusView.render("show.json", activity: activity, for: nil) - |> atom_key_to_string() + view_json = atom_key_to_string(StatusView.render("show.json", activity: activity, for: nil)) assert json == view_json end @@ -114,10 +155,7 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do assert json["payload"] assert {:ok, json} = Jason.decode(json["payload"]) - view_json = - Pleroma.Web.MastodonAPI.StatusView.render("show.json", activity: activity, for: nil) - |> Jason.encode!() - |> Jason.decode!() + view_json = atom_key_to_string(StatusView.render("show.json", activity: activity, for: nil)) assert json == view_json end @@ -279,6 +317,34 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do end) end + test "echoes the Sec-WebSocket-Protocol token in the handshake", %{token: token} do + assert {:ok, %{status: 101, headers: headers}} = + raw_websocket_handshake("?stream=user", [ + {"sec-websocket-protocol", token.token} + ]) + + assert {"sec-websocket-protocol", token.token} in headers + end + + test "echoes the selected Sec-WebSocket-Protocol token", %{token: token} do + assert {:ok, %{status: 101, headers: headers}} = + raw_websocket_handshake("?stream=user", [ + {"sec-websocket-protocol", "#{token.token}, phoenix"} + ]) + + assert {"sec-websocket-protocol", token.token} in headers + end + + test "does not echo an invalid Sec-WebSocket-Protocol token", %{token: token} do + assert {:ok, %{status: 401, headers: headers}} = + raw_websocket_handshake("?stream=user", [ + {"sec-websocket-protocol", "invalid"} + ]) + + refute {"sec-websocket-protocol", token.token} in headers + refute List.keymember?(headers, "sec-websocket-protocol", 0) + end + test "prefers sec-websocket-protocol token over query access_token", %{ token: token, user: user @@ -450,12 +516,12 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do assert {:ok, json} = Jason.decode(json["payload"]) view_json = - Pleroma.Web.MastodonAPI.StatusView.render("show.json", - activity: activity, - for: reading_user + atom_key_to_string( + StatusView.render("show.json", + activity: activity, + for: reading_user + ) ) - |> Jason.encode!() - |> Jason.decode!() assert json == view_json end @@ -478,12 +544,12 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do activity = Pleroma.Activity.normalize(activity) view_json = - Pleroma.Web.MastodonAPI.StatusView.render("show.json", - activity: activity, - for: reading_user + atom_key_to_string( + StatusView.render("show.json", + activity: activity, + for: reading_user + ) ) - |> Jason.encode!() - |> Jason.decode!() assert {:ok, %{"event" => "status.update", "payload" => ^view_json}} = decode_json(raw_json) end From 1e65c7a69e3cbd84009563a8d1974f772188a4b7 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Fri, 22 May 2026 20:36:52 +0400 Subject: [PATCH 139/391] Add grouped notifications API --- changelog.d/grouped-notifications-api.add | 1 + lib/pleroma/notification.ex | 84 +++++++++ .../operations/notification_operation.ex | 159 ++++++++++++++++ .../controllers/notification_controller.ex | 146 ++++++++++++++- lib/pleroma/web/mastodon_api/mastodon_api.ex | 177 ++++++++++++++++-- .../mastodon_api/views/notification_view.ex | 120 +++++++++++- lib/pleroma/web/router.ex | 10 + .../notification_controller_test.exs | 169 +++++++++++++++++ .../views/notification_view_test.exs | 8 +- 9 files changed, 850 insertions(+), 24 deletions(-) create mode 100644 changelog.d/grouped-notifications-api.add diff --git a/changelog.d/grouped-notifications-api.add b/changelog.d/grouped-notifications-api.add new file mode 100644 index 000000000..82ce8b178 --- /dev/null +++ b/changelog.d/grouped-notifications-api.add @@ -0,0 +1 @@ +Add Mastodon-compatible grouped notifications API endpoints under `/api/v2/notifications` diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index b8693c3a8..ec10854e3 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -62,6 +62,90 @@ defmodule Pleroma.Notification do |> Repo.aggregate(:count, :id) end + @groupable_notification_types ~w{favourite follow reblog} + @group_bucket_seconds 12 * 60 * 60 + + def groupable_notification_types, do: @groupable_notification_types + + def group_notifications(notifications, grouped_types \\ nil) do + grouped_types = normalize_grouped_types(grouped_types) + + {group_keys, grouped_notifications} = + Enum.reduce(notifications, {[], %{}}, fn notification, + {group_keys, grouped_notifications} -> + group_key = group_key(notification, grouped_types) + + if Map.has_key?(grouped_notifications, group_key) do + {group_keys, Map.update!(grouped_notifications, group_key, &[notification | &1])} + else + {[group_key | group_keys], Map.put(grouped_notifications, group_key, [notification])} + end + end) + + group_keys + |> Enum.reverse() + |> Enum.map(fn group_key -> + grouped_notifications + |> Map.fetch!(group_key) + |> Enum.reverse() + end) + end + + def group_key(notification, grouped_types \\ nil) + + def group_key(%Notification{} = notification, nil) do + group_key(notification, @groupable_notification_types) + end + + def group_key(%Notification{type: type} = notification, grouped_types) do + grouped_types = normalize_grouped_types(grouped_types) + + if type in @groupable_notification_types and type in grouped_types do + case group_target_id(notification) do + nil -> ungrouped_group_key(notification) + target_id -> "#{type}-#{target_id}-#{group_time_bucket(notification)}" + end + else + ungrouped_group_key(notification) + end + end + + def normalize_grouped_types(nil), do: @groupable_notification_types + def normalize_grouped_types(types) when is_list(types), do: types + def normalize_grouped_types(type), do: [type] + + defp ungrouped_group_key(%Notification{id: id}), do: "ungrouped-#{id}" + + defp group_time_bucket(%Notification{inserted_at: inserted_at}) do + inserted_at + |> NaiveDateTime.to_erl() + |> :calendar.datetime_to_gregorian_seconds() + |> div(@group_bucket_seconds) + end + + defp group_target_id(%Notification{type: type, activity: activity}) + when type in ["favourite", "reblog"] do + with object_id when is_binary(object_id) <- object_id_for(activity), + %Activity{id: id} <- Activity.get_create_by_object_ap_id(object_id) do + to_string(id) + else + _ -> nil + end + end + + defp group_target_id(%Notification{type: "follow", activity: %{data: %{"object" => ap_id}}}) do + case User.get_cached_by_ap_id(ap_id) do + %User{id: id} -> to_string(id) + _ -> nil + end + end + + defp group_target_id(_), do: nil + + defp object_id_for(%{data: %{"object" => %{"id" => id}}}) when is_binary(id), do: id + defp object_id_for(%{data: %{"object" => id}}) when is_binary(id), do: id + defp object_id_for(_), do: nil + @notification_types ~w{ favourite follow diff --git a/lib/pleroma/web/api_spec/operations/notification_operation.ex b/lib/pleroma/web/api_spec/operations/notification_operation.ex index 94d1f6b82..ba996c13b 100644 --- a/lib/pleroma/web/api_spec/operations/notification_operation.ex +++ b/lib/pleroma/web/api_spec/operations/notification_operation.ex @@ -91,6 +91,79 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do } end + def grouped_index_operation do + %Operation{ + tags: ["Notifications"], + summary: "Retrieve grouped notifications", + description: "Notifications concerning the user, grouped by supported notification types.", + operationId: "NotificationController.grouped_index", + security: [%{"oAuth" => ["read:notifications"]}], + parameters: grouped_notification_params() ++ pagination_params(), + responses: %{ + 200 => Operation.response("Grouped notifications", "application/json", grouped_result()), + 404 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def show_group_operation do + %Operation{ + tags: ["Notifications"], + summary: "Retrieve a notification group", + operationId: "NotificationController.show_group", + security: [%{"oAuth" => ["read:notifications"]}], + parameters: [group_key_param()], + responses: %{ + 200 => Operation.response("Grouped notifications", "application/json", grouped_result()), + 404 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def group_accounts_operation do + %Operation{ + tags: ["Notifications"], + summary: "Retrieve notification group accounts", + operationId: "NotificationController.group_accounts", + security: [%{"oAuth" => ["write:notifications"]}], + parameters: [group_key_param()], + responses: %{ + 200 => + Operation.response("Accounts", "application/json", %Schema{ + type: :array, + items: Account + }) + } + } + end + + def unread_count_operation do + %Operation{ + tags: ["Notifications"], + summary: "Retrieve unread grouped notification count", + operationId: "NotificationController.unread_count", + security: [%{"oAuth" => ["read:notifications"]}], + parameters: + grouped_notification_params() ++ + [ + Operation.parameter( + :limit, + :query, + %Schema{type: :integer}, + "Maximum number of notifications to count" + ) + ], + responses: %{ + 200 => + Operation.response("Unread notification group count", "application/json", %Schema{ + type: :object, + properties: %{count: %Schema{type: :integer}} + }), + 404 => Operation.response("Error", "application/json", ApiError) + } + } + end + def clear_operation do %Operation{ tags: ["Notifications"], @@ -114,6 +187,18 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do } end + def dismiss_group_operation do + %Operation{ + tags: ["Notifications"], + summary: "Dismiss a notification group", + description: "Clear all notifications in a notification group from the server.", + operationId: "NotificationController.dismiss_group", + parameters: [group_key_param()], + security: [%{"oAuth" => ["write:notifications"]}], + responses: %{200 => empty_object_response()} + } + end + def dismiss_via_body_operation do %Operation{ tags: ["Notifications"], @@ -194,6 +279,73 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do } end + defp grouped_result do + %Schema{ + title: "GroupedNotificationsResults", + type: :object, + properties: %{ + accounts: %Schema{type: :array, items: Account}, + statuses: %Schema{type: :array, items: Status}, + notification_groups: %Schema{type: :array, items: notification_group()} + }, + required: [:accounts, :statuses, :notification_groups] + } + end + + defp notification_group do + %Schema{ + title: "NotificationGroup", + type: :object, + properties: %{ + group_key: %Schema{type: :string}, + notifications_count: %Schema{type: :integer}, + type: notification_type(), + most_recent_notification_id: %Schema{type: :string}, + page_min_id: %Schema{type: :string, nullable: true}, + page_max_id: %Schema{type: :string, nullable: true}, + latest_page_notification_at: %Schema{type: :string, format: :"date-time", nullable: true}, + sample_account_ids: %Schema{type: :array, items: %Schema{type: :string}}, + status_id: %Schema{type: :string, nullable: true} + }, + required: [ + :group_key, + :notifications_count, + :type, + :most_recent_notification_id, + :sample_account_ids + ] + } + end + + defp grouped_notification_params do + [ + Operation.parameter( + :exclude_types, + :query, + %Schema{type: :array, items: notification_type()}, + "Array of types to exclude" + ), + Operation.parameter( + :account_id, + :query, + %Schema{type: :string}, + "Return only notifications received from this account" + ), + Operation.parameter( + :types, + :query, + %Schema{type: :array, items: notification_type()}, + "Include the notifications for activities with the given types" + ), + Operation.parameter( + :grouped_types, + :query, + %Schema{type: :array, items: notification_type()}, + "Notification types that may be grouped" + ) + ] + end + defp notification_type do %Schema{ type: :string, @@ -239,4 +391,11 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do required: true ) end + + defp group_key_param do + Operation.parameter(:group_key, :path, :string, "Notification group key", + example: "favourite-123", + required: true + ) + end end diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex index afd83b785..5cd8576c1 100644 --- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex @@ -8,10 +8,12 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] alias Pleroma.Notification + alias Pleroma.User + alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.MastodonAPI alias Pleroma.Web.Plugs.OAuthScopesPlug - @oauth_read_actions [:show, :index] + @oauth_read_actions [:show, :index, :grouped_index, :show_group, :unread_count] plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false) @@ -39,7 +41,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do # GET /api/v1/notifications def index(%{private: %{open_api_spex: %{params: %{account_id: account_id} = params}}} = conn, _) do - case Pleroma.User.get_cached_by_id(account_id) do + case User.get_cached_by_id(account_id) do %{ap_id: account_ap_id} -> params = params @@ -59,10 +61,110 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do do_get_notifications(conn, params) end + # GET /api/v2/notifications + def grouped_index( + %{private: %{open_api_spex: %{params: %{account_id: account_id} = params}}} = conn, + _ + ) do + case User.get_cached_by_id(account_id) do + %{ap_id: account_ap_id} -> + params = + params + |> Map.delete(:account_id) + |> Map.put(:account_ap_id, account_ap_id) + + do_get_grouped_notifications(conn, params) + + _ -> + conn + |> put_status(:not_found) + |> json(%{"error" => "Account is not found"}) + end + end + + def grouped_index(%{private: %{open_api_spex: %{params: params}}} = conn, _) do + do_get_grouped_notifications(conn, params) + end + + # GET /api/v2/notifications/:group_key + def show_group( + %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{group_key: group_key}}}} = + conn, + _ + ) do + notifications = MastodonAPI.get_notification_group(user, group_key, %{}) + + if Enum.empty?(notifications) do + conn + |> put_status(:not_found) + |> json(%{"error" => "Notification group is not found"}) + else + grouped_types = if String.starts_with?(group_key, "ungrouped-"), do: [], else: nil + + render(conn, "grouped_index.json", + notification_groups: [notifications], + for: user, + grouped_types: grouped_types + ) + end + end + + # GET /api/v2/notifications/:group_key/accounts + def group_accounts( + %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{group_key: group_key}}}} = + conn, + _ + ) do + users = + user + |> MastodonAPI.get_notification_group(group_key, %{}) + |> notification_actors() + + json(conn, AccountView.render("index.json", %{users: users, for: user})) + end + + # GET /api/v2/notifications/unread_count + def unread_count( + %{private: %{open_api_spex: %{params: %{account_id: account_id} = params}}} = conn, + _ + ) do + case User.get_cached_by_id(account_id) do + %{ap_id: account_ap_id} -> + params = + params + |> Map.delete(:account_id) + |> Map.put(:account_ap_id, account_ap_id) + + do_get_unread_group_count(conn, params) + + _ -> + conn + |> put_status(:not_found) + |> json(%{"error" => "Account is not found"}) + end + end + + def unread_count(%{private: %{open_api_spex: %{params: params}}} = conn, _) do + do_get_unread_group_count(conn, params) + end + + # POST /api/v2/notifications/:group_key/dismiss + def dismiss_group( + %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{group_key: group_key}}}} = + conn, + _ + ) do + ids = + user + |> MastodonAPI.get_notification_group(group_key, %{}) + |> Enum.map(& &1.id) + + Notification.destroy_multiple(user, ids) + json(conn, %{}) + end + defp do_get_notifications(%{assigns: %{user: user}} = conn, params) do - params = - Map.new(params, fn {k, v} -> {to_string(k), v} end) - |> Map.put_new("types", Map.get(params, :include_types, @default_notification_types)) + params = normalize_notification_params(params) notifications = MastodonAPI.get_notifications(user, params) @@ -74,6 +176,40 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do ) end + defp do_get_grouped_notifications(%{assigns: %{user: user}} = conn, params) do + params = normalize_notification_params(params) + + {notification_groups, page_notifications, notification_group_counts} = + MastodonAPI.get_grouped_notification_page(user, params) + + conn + |> add_link_headers(page_notifications) + |> render("grouped_index.json", + notification_groups: notification_groups, + notification_group_counts: notification_group_counts, + for: user, + grouped_types: params["grouped_types"] + ) + end + + defp do_get_unread_group_count(%{assigns: %{user: user}} = conn, params) do + params = normalize_notification_params(params) + json(conn, %{count: MastodonAPI.unread_notification_group_count(user, params)}) + end + + defp normalize_notification_params(params) do + params + |> Map.new(fn {k, v} -> {to_string(k), v} end) + |> Map.put_new("types", Map.get(params, :include_types, @default_notification_types)) + end + + defp notification_actors(notifications) do + notifications + |> Enum.map(&User.get_cached_by_ap_id(&1.activity.data["actor"])) + |> Enum.filter(& &1) + |> Enum.uniq_by(& &1.id) + end + # GET /api/v1/notifications/:id def show(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do with {:ok, notification} <- Notification.get(user, id) do diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex index c9e045d23..57cbfd6bf 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex @@ -8,6 +8,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do alias Pleroma.Notification alias Pleroma.Pagination + alias Pleroma.Repo alias Pleroma.ScheduledActivity alias Pleroma.User alias Pleroma.Web.CommonAPI @@ -61,29 +62,175 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do end def get_notifications(user, params \\ %{}) do - options = - cast_params(params) |> Map.update(:include_types, [], fn include_types -> include_types end) + user + |> notifications_query(params) + |> Pagination.fetch_paginated(params) + end - options = - if ("pleroma:report" not in options.include_types and - User.privileged?(user, :reports_manage_reports)) or - User.privileged?(user, :reports_manage_reports) do - options - else - options - |> Map.update(:exclude_types, ["pleroma:report"], fn current_exclude_types -> - current_exclude_types ++ ["pleroma:report"] - end) - end + def get_grouped_notification_page(user, params \\ %{}) do + grouped_types = Map.get(params, "grouped_types", Map.get(params, :grouped_types)) + {query, order} = group_pagination_query(notifications_query(user, params), params) + + notifications = + query + |> order_by([n], [{^order, n.id}]) + |> Repo.all() + + notification_group_counts = + Enum.frequencies_by(notifications, &Notification.group_key(&1, grouped_types)) + + page_notifications = + notifications + |> take_grouped_page_notifications(grouped_types, grouped_limit(params)) + + page_notifications = + if order == :asc, do: Enum.reverse(page_notifications), else: page_notifications + + { + Notification.group_notifications(page_notifications, grouped_types), + page_notifications, + notification_group_counts + } + end + + def get_grouped_notification_groups(user, params \\ %{}) do + {groups, _notifications, _notification_group_counts} = + get_grouped_notification_page(user, params) + + groups + end + + def get_notification_group(user, group_key, params \\ %{}) + + def get_notification_group(user, "ungrouped-" <> notification_id, _params) do + case Notification.get(user, notification_id) do + {:ok, notification} -> [notification] + _ -> [] + end + end + + def get_notification_group(user, group_key, params) do + grouped_types = Map.get(params, "grouped_types", Map.get(params, :grouped_types)) + + user + |> notifications_query(params) + |> order_by([n], desc: n.id) + |> Repo.all() + |> Enum.filter(&(Notification.group_key(&1, grouped_types) == group_key)) + end + + defp group_pagination_query(query, params) do + cond do + min_id = Map.get(params, "min_id", Map.get(params, :min_id)) -> + query = where(query, [n], n.id > ^min_id) + + query = + case Map.get(params, "max_id", Map.get(params, :max_id)) do + nil -> query + max_id -> where(query, [n], n.id < ^max_id) + end + + {query, :asc} + + since_id = Map.get(params, "since_id", Map.get(params, :since_id)) -> + {where(query, [n], n.id > ^since_id), :desc} + + max_id = Map.get(params, "max_id", Map.get(params, :max_id)) -> + {where(query, [n], n.id < ^max_id), :desc} + + true -> + {query, :desc} + end + end + + defp grouped_limit(params) do + params + |> Map.get("limit", Map.get(params, :limit, 40)) + |> parse_limit(40) + |> min(80) + end + + defp take_grouped_page_notifications(notifications, grouped_types, group_limit) do + {notifications, _group_keys} = + Enum.reduce_while(notifications, {[], MapSet.new()}, fn notification, + {notifications, group_keys} -> + group_key = Notification.group_key(notification, grouped_types) + + cond do + MapSet.member?(group_keys, group_key) -> + {:cont, {[notification | notifications], group_keys}} + + MapSet.size(group_keys) < group_limit -> + {:cont, {[notification | notifications], MapSet.put(group_keys, group_key)}} + + true -> + {:halt, {notifications, group_keys}} + end + end) + + Enum.reverse(notifications) + end + + def unread_notification_group_count(user, params \\ %{}) do + grouped_types = Map.get(params, "grouped_types", Map.get(params, :grouped_types)) + limit = unread_count_limit(params) + + user + |> notifications_query(params) + |> where([n], n.seen == false) + |> order_by([n], desc: n.id) + |> limit(^limit) + |> Repo.all() + |> Notification.group_notifications(grouped_types) + |> length() + end + + defp notifications_query(user, params) do + options = notification_options(user, params) user |> Notification.for_user_query(options) |> restrict(:types, options) |> restrict(:exclude_types, options) |> restrict(:account_ap_id, options) - |> Pagination.fetch_paginated(params) end + defp notification_options(user, params) do + options = + params + |> cast_params() + |> Map.update(:include_types, [], fn include_types -> include_types end) + + if ("pleroma:report" not in options.include_types and + User.privileged?(user, :reports_manage_reports)) or + User.privileged?(user, :reports_manage_reports) do + options + else + options + |> Map.update(:exclude_types, ["pleroma:report"], fn current_exclude_types -> + current_exclude_types ++ ["pleroma:report"] + end) + end + end + + defp unread_count_limit(params) do + params + |> Map.get("limit", Map.get(params, :limit, 100)) + |> parse_limit(100) + |> min(1000) + end + + defp parse_limit(limit, _default) when is_integer(limit) and limit > 0, do: limit + + defp parse_limit(limit, default) when is_binary(limit) do + case Integer.parse(limit) do + {limit, _} when limit > 0 -> limit + _ -> default + end + end + + defp parse_limit(_, default), do: default + def get_scheduled_activities(user, params \\ %{}) do user |> ScheduledActivity.for_user_query() @@ -95,6 +242,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do exclude_types: {:array, :string}, types: {:array, :string}, exclude_visibilities: {:array, :string}, + grouped_types: {:array, :string}, + limit: :integer, reblogs: :boolean, with_muted: :boolean, account_ap_id: :string, diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex index 27532c42a..466073853 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -72,6 +72,49 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do safe_render_many(notifications, NotificationView, "show.json", opts) end + def render("grouped_index.json", %{notifications: notifications} = opts) do + grouped_types = Notification.normalize_grouped_types(opts[:grouped_types]) + + opts + |> Map.delete(:notifications) + |> Map.put( + :notification_groups, + Notification.group_notifications(notifications, grouped_types) + ) + |> then(&render("grouped_index.json", &1)) + end + + def render( + "grouped_index.json", + %{notification_groups: notification_groups, for: reading_user} = opts + ) do + grouped_types = Notification.normalize_grouped_types(opts[:grouped_types]) + notification_group_counts = Map.get(opts, :notification_group_counts, %{}) + + statuses = + notification_groups + |> Enum.map(&List.first/1) + |> Enum.map(&render("show.json", %{notification: &1, for: reading_user})) + |> Enum.map(& &1[:status]) + |> Enum.filter(& &1) + |> Enum.uniq_by(& &1[:id]) + + actors = + notification_groups + |> List.flatten() + |> notification_actors() + + %{ + accounts: AccountView.render("index.json", %{users: actors, for: reading_user}), + statuses: statuses, + notification_groups: + Enum.map( + notification_groups, + &render_group(&1, reading_user, grouped_types, notification_group_counts) + ) + } + end + def render( "show.json", %{ @@ -95,7 +138,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do response = %{ id: to_string(notification.id), - group_key: "ungrouped-" <> to_string(notification.id), + group_key: Notification.group_key(notification), type: notification.type, created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at), account: account, @@ -137,6 +180,81 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do Map.put(response, :report, report_render) end + defp render_group( + [%Notification{} = notification | _] = notifications, + _reading_user, + grouped_types, + notification_group_counts + ) do + latest_notification = List.first(notifications) + oldest_notification = List.last(notifications) + status_activity = status_activity_for_group(notifications, grouped_types) + group_key = Notification.group_key(notification, grouped_types) + + response = %{ + group_key: group_key, + notifications_count: Map.get(notification_group_counts, group_key, length(notifications)), + type: notification.type, + most_recent_notification_id: to_string(latest_notification.id), + page_min_id: to_string(oldest_notification.id), + page_max_id: to_string(latest_notification.id), + latest_page_notification_at: CommonAPI.Utils.to_masto_date(latest_notification.inserted_at), + sample_account_ids: + notifications + |> notification_actors() + |> Enum.map(&to_string(&1.id)) + } + + if status_activity do + Map.put(response, :status_id, to_string(status_activity.id)) + else + response + end + end + + defp status_activity_for_group([%Notification{} = notification | _], grouped_types) do + status_activity_for(notification, grouped_types) + end + + defp status_activity_for(%Notification{type: type, activity: activity}, _grouped_types) + when type in ["mention", "status", "poll"] do + activity + end + + defp status_activity_for(%Notification{type: type} = notification, grouped_types) + when type in ["favourite", "reblog"] do + group_key = Notification.group_key(notification, grouped_types) + + case String.split(group_key, "-", parts: 3) do + [^type, activity_id, _bucket] -> + case Activity.create_by_id_with_object(activity_id) do + %Activity{} = activity -> activity + _ -> parent_status_activity(notification.activity) + end + + _ -> + parent_status_activity(notification.activity) + end + end + + defp status_activity_for(%Notification{type: type, activity: activity}, _grouped_types) + when type in ["update", "pleroma:emoji_reaction"] do + parent_status_activity(activity) + end + + defp status_activity_for(_, _grouped_types), do: nil + + defp parent_status_activity(activity) do + Activity.get_create_by_object_ap_id(object_id_for(activity)) + end + + defp notification_actors(notifications) do + notifications + |> Enum.map(&User.get_cached_by_ap_id(&1.activity.data["actor"])) + |> Enum.filter(& &1) + |> Enum.uniq_by(& &1.id) + end + defp put_emoji(response, activity) do response |> Map.put(:emoji, activity.data["content"]) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 813c5c482..5657bcad7 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -844,6 +844,16 @@ defmodule Pleroma.Web.Router do get("/directory", DirectoryController, :index) end + scope "/api/v2", Pleroma.Web.MastodonAPI do + pipe_through(:authenticated_api) + + get("/notifications", NotificationController, :grouped_index) + get("/notifications/unread_count", NotificationController, :unread_count) + get("/notifications/:group_key/accounts", NotificationController, :group_accounts) + get("/notifications/:group_key", NotificationController, :show_group) + post("/notifications/:group_key/dismiss", NotificationController, :dismiss_group) + end + scope "/api/v2", Pleroma.Web.MastodonAPI do pipe_through(:api) diff --git a/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs index dd4b7389e..1d3a9e966 100644 --- a/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs @@ -577,6 +577,175 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result end + describe "GET /api/v2/notifications" do + test "groups favourite notifications for the same status" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + other_user1 = insert(:user) + other_user2 = insert(:user) + + {:ok, status} = CommonAPI.post(user, %{status: "hello"}) + {:ok, _} = CommonAPI.favorite(status.id, other_user1) + {:ok, _} = CommonAPI.favorite(status.id, other_user2) + + notification_ids = + Notification + |> Repo.all() + |> Enum.map(&to_string(&1.id)) + + result = + conn + |> get("/api/v2/notifications") + |> json_response_and_validate_schema(200) + + assert [%{"id" => account_id1}, %{"id" => account_id2}] = result["accounts"] + assert account_id1 in [other_user1.id, other_user2.id] + assert account_id2 in [other_user1.id, other_user2.id] + assert [%{"id" => status_id}] = result["statuses"] + assert status_id == status.id + + assert [group] = result["notification_groups"] + assert group["type"] == "favourite" + assert group["notifications_count"] == 2 + assert group["status_id"] == status.id + assert group["most_recent_notification_id"] in notification_ids + assert group["page_min_id"] in notification_ids + assert group["page_max_id"] in notification_ids + assert group["latest_page_notification_at"] + assert Enum.sort(group["sample_account_ids"]) == Enum.sort([other_user1.id, other_user2.id]) + group_key = group["group_key"] + + assert [%{"group_key" => ^group_key}, %{"group_key" => ^group_key}] = + conn + |> get("/api/v1/notifications") + |> json_response_and_validate_schema(200) + + assert %{"notification_groups" => [shown_group]} = + conn + |> get("/api/v2/notifications/#{group_key}") + |> json_response_and_validate_schema(200) + + assert shown_group["group_key"] == group_key + assert shown_group["notifications_count"] == 2 + end + + test "round-trips ungrouped group keys when grouped_types excludes a type" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + other_user = insert(:user) + + {:ok, status} = CommonAPI.post(user, %{status: "hello"}) + {:ok, _} = CommonAPI.favorite(status.id, other_user) + + %{"notification_groups" => [%{"group_key" => "ungrouped-" <> _ = group_key}]} = + conn + |> get("/api/v2/notifications?grouped_types[]=reblog") + |> json_response_and_validate_schema(200) + + assert %{ + "notification_groups" => [%{"group_key" => ^group_key, "notifications_count" => 1}] + } = + conn + |> get("/api/v2/notifications/#{group_key}") + |> json_response_and_validate_schema(200) + end + + test "paginates notification groups instead of raw notifications" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + other_user1 = insert(:user) + other_user2 = insert(:user) + other_user3 = insert(:user) + + {:ok, older_status} = CommonAPI.post(user, %{status: "older"}) + {:ok, _} = CommonAPI.favorite(older_status.id, other_user3) + + {:ok, newer_status} = CommonAPI.post(user, %{status: "newer"}) + {:ok, _} = CommonAPI.favorite(newer_status.id, other_user1) + {:ok, _} = CommonAPI.favorite(newer_status.id, other_user2) + older_status_id = older_status.id + newer_status_id = newer_status.id + + %{"notification_groups" => groups} = + conn + |> get("/api/v2/notifications?limit=2") + |> json_response_and_validate_schema(200) + + assert [ + %{"status_id" => ^newer_status_id, "notifications_count" => 2}, + %{"status_id" => ^older_status_id, "notifications_count" => 1} + ] = groups + end + + test "returns total notification count for a partially represented group" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + other_user1 = insert(:user) + other_user2 = insert(:user) + other_user3 = insert(:user) + + {:ok, grouped_status} = CommonAPI.post(user, %{status: "grouped"}) + {:ok, _} = CommonAPI.favorite(grouped_status.id, other_user1) + + {:ok, other_status} = CommonAPI.post(user, %{status: "other"}) + {:ok, _} = CommonAPI.favorite(other_status.id, other_user3) + {:ok, _} = CommonAPI.favorite(grouped_status.id, other_user2) + grouped_status_id = grouped_status.id + + assert %{ + "notification_groups" => [ + %{"status_id" => ^grouped_status_id, "notifications_count" => 2} + ] + } = + conn + |> get("/api/v2/notifications?limit=1") + |> json_response_and_validate_schema(200) + end + + test "counts unread notification groups" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + other_user1 = insert(:user) + other_user2 = insert(:user) + mentioner = insert(:user) + + {:ok, status} = CommonAPI.post(user, %{status: "hello"}) + {:ok, _} = CommonAPI.favorite(status.id, other_user1) + {:ok, _} = CommonAPI.favorite(status.id, other_user2) + {:ok, _} = CommonAPI.post(mentioner, %{status: "hi @#{user.nickname}"}) + + assert %{"count" => 2} = + conn + |> get("/api/v2/notifications/unread_count") + |> json_response_and_validate_schema(200) + end + + test "lists accounts from and dismisses a notification group" do + %{user: user, conn: conn} = oauth_access(["read:notifications", "write:notifications"]) + other_user1 = insert(:user) + other_user2 = insert(:user) + + {:ok, status} = CommonAPI.post(user, %{status: "hello"}) + {:ok, _} = CommonAPI.favorite(status.id, other_user1) + {:ok, _} = CommonAPI.favorite(status.id, other_user2) + + %{"notification_groups" => [%{"group_key" => group_key}]} = + conn + |> get("/api/v2/notifications") + |> json_response_and_validate_schema(200) + + account_ids = + conn + |> get("/api/v2/notifications/#{group_key}/accounts") + |> json_response_and_validate_schema(200) + |> Enum.map(& &1["id"]) + + assert Enum.sort(account_ids) == Enum.sort([other_user1.id, other_user2.id]) + + assert %{} = + conn + |> post("/api/v2/notifications/#{group_key}/dismiss") + |> json_response_and_validate_schema(200) + + assert [] = Notification.for_user(user) + end + end + test "doesn't see notifications after muting user with notifications" do %{user: user, conn: conn} = oauth_access(["read:notifications"]) user2 = insert(:user) diff --git a/test/pleroma/web/mastodon_api/views/notification_view_test.exs b/test/pleroma/web/mastodon_api/views/notification_view_test.exs index c8287bb79..aa11ad682 100644 --- a/test/pleroma/web/mastodon_api/views/notification_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/notification_view_test.exs @@ -101,7 +101,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do expected = %{ id: to_string(notification.id), - group_key: "ungrouped-#{to_string(notification.id)}", + group_key: Notification.group_key(notification), pleroma: %{is_seen: false, is_muted: false}, type: "favourite", account: AccountView.render("show.json", %{user: another_user, for: user}), @@ -122,7 +122,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do expected = %{ id: to_string(notification.id), - group_key: "ungrouped-#{to_string(notification.id)}", + group_key: Notification.group_key(notification), pleroma: %{is_seen: false, is_muted: false}, type: "reblog", account: AccountView.render("show.json", %{user: another_user, for: user}), @@ -141,7 +141,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do expected = %{ id: to_string(notification.id), - group_key: "ungrouped-#{to_string(notification.id)}", + group_key: Notification.group_key(notification), pleroma: %{is_seen: false, is_muted: false}, type: "follow", account: AccountView.render("show.json", %{user: follower, for: followed}), @@ -333,7 +333,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do expected = %{ id: to_string(notification.id), - group_key: "ungrouped-#{to_string(notification.id)}", + group_key: Notification.group_key(notification), pleroma: %{is_seen: true, is_muted: true}, type: "favourite", account: AccountView.render("show.json", %{user: another_user, for: user}), From b054c2aa42748fd3489ee463f4bfd43b075ec638 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Fri, 22 May 2026 22:11:37 +0400 Subject: [PATCH 140/391] Fix paged featured collection fetches --- changelog.d/featured-collection-page.fix | 1 + lib/pleroma/web/activity_pub/activity_pub.ex | 36 ++++- .../web/activity_pub/activity_pub_test.exs | 133 ++++++++++++++++++ 3 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 changelog.d/featured-collection-page.fix diff --git a/changelog.d/featured-collection-page.fix b/changelog.d/featured-collection-page.fix new file mode 100644 index 000000000..b611a4137 --- /dev/null +++ b/changelog.d/featured-collection-page.fix @@ -0,0 +1 @@ +Support fetching featured collection items from the first collection page diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 0b513ee16..1e8bfe3dd 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1881,11 +1881,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end end + @featured_collection_types ["OrderedCollection", "Collection"] + @featured_collection_page_types ["OrderedCollectionPage", "CollectionPage"] + @featured_collection_item_types @featured_collection_types ++ @featured_collection_page_types + def pin_data_from_featured_collection(%{ "type" => type, "orderedItems" => objects }) - when type in ["OrderedCollection", "Collection"] do + when type in @featured_collection_item_types do Map.new(objects, fn %{"id" => object_ap_id} -> {object_ap_id, NaiveDateTime.utc_now()} object_ap_id when is_binary(object_ap_id) -> {object_ap_id, NaiveDateTime.utc_now()} @@ -1903,7 +1907,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do def fetch_and_prepare_featured_from_ap_id(ap_id) do with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id) do - {:ok, pin_data_from_featured_collection(data)} + {:ok, prepare_featured_collection(data)} else e -> Logger.error("Could not decode featured collection at fetch #{ap_id}, #{inspect(e)}") @@ -1911,6 +1915,34 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end end + defp prepare_featured_collection(%{"orderedItems" => objects} = data) when is_list(objects) do + pin_data_from_featured_collection(data) + end + + defp prepare_featured_collection(%{ + "type" => type, + "first" => %{"type" => page_type} = first + }) + when type in @featured_collection_types and page_type in @featured_collection_page_types do + pin_data_from_featured_collection(first) + end + + defp prepare_featured_collection(%{"type" => type, "first" => first}) + when type in @featured_collection_types and is_binary(first) do + case Fetcher.fetch_and_contain_remote_object_from_id(first) do + {:ok, data} -> + pin_data_from_featured_collection(data) + + e -> + Logger.error("Could not decode featured collection page at fetch #{first}, #{inspect(e)}") + %{} + end + end + + defp prepare_featured_collection(data) do + pin_data_from_featured_collection(data) + end + def enqueue_pin_fetches(%{pinned_objects: pins}) do # enqueue a task to fetch all pinned objects Enum.each(pins, fn {ap_id, _} -> diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs index 9aafc41a5..dd7a82a55 100644 --- a/test/pleroma/web/activity_pub/activity_pub_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_test.exs @@ -415,6 +415,105 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do assert %{data: %{"id" => ^object_url}} = Object.get_by_ap_id(object_url) end + test "fetches user featured collection from the first collection page" do + ap_id = "https://example.com/users/lain" + featured_url = "https://example.com/users/lain/collections/featured" + first_page_url = "#{featured_url}?page=true" + + user_data = + "test/fixtures/users_mock/user.json" + |> File.read!() + |> String.replace("{{nickname}}", "lain") + |> Jason.decode!() + |> Map.put("featured", featured_url) + |> Jason.encode!() + + object_id = Ecto.UUID.generate() + object_url = "https://example.com/objects/#{object_id}" + + featured_data = + Jason.encode!(%{ + "id" => featured_url, + "type" => "OrderedCollection", + "first" => first_page_url + }) + + first_page_data = + Jason.encode!(%{ + "id" => first_page_url, + "type" => "OrderedCollectionPage", + "partOf" => featured_url, + "orderedItems" => [object_url] + }) + + object_data = + "test/fixtures/statuses/note.json" + |> File.read!() + |> String.replace("{{object_id}}", object_id) + |> String.replace("{{nickname}}", "lain") + + Tesla.Mock.mock(fn + %{ + method: :get, + url: ^ap_id + } -> + %Tesla.Env{ + status: 200, + body: user_data, + headers: [{"content-type", "application/activity+json"}] + } + + %{ + method: :get, + url: ^featured_url + } -> + %Tesla.Env{ + status: 200, + body: featured_data, + headers: [{"content-type", "application/activity+json"}] + } + + %{ + method: :get, + url: ^first_page_url + } -> + %Tesla.Env{ + status: 200, + body: first_page_data, + headers: [{"content-type", "application/activity+json"}] + } + + %{ + method: :get, + url: ^object_url + } -> + %Tesla.Env{ + status: 200, + body: object_data, + headers: [{"content-type", "application/activity+json"}] + } + end) + + refute capture_log(fn -> + {:ok, user} = ActivityPub.make_user_from_ap_id(ap_id) + + assert_enqueued( + worker: Pleroma.Workers.RemoteFetcherWorker, + args: %{ + "op" => "fetch_remote", + "id" => object_url, + "depth" => 1 + } + ) + + Pleroma.Tests.ObanHelpers.perform_all() + + assert user.featured_address == featured_url + assert Map.has_key?(user.pinned_objects, object_url) + assert %{data: %{"id" => ^object_url}} = Object.get_by_ap_id(object_url) + end) =~ "Could not parse featured collection" + end + test "fetches user birthday information from misskey" do user_id = "https://misskey.io/@mkljczk" @@ -2818,4 +2917,38 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do "first" => "https://social.example/users/alice/collections/featured?page=true" }) end + + test "fetch_and_prepare_featured_from_ap_id handles embedded first collection pages" do + featured_url = "https://social.example/users/alice/collections/featured" + first_page_url = "#{featured_url}?page=true" + object_url = "https://social.example/objects/1" + + featured_data = + Jason.encode!(%{ + "id" => featured_url, + "type" => "OrderedCollection", + "first" => %{ + "id" => first_page_url, + "type" => "OrderedCollectionPage", + "partOf" => featured_url, + "orderedItems" => [object_url] + } + }) + + Tesla.Mock.mock(fn + %{method: :get, url: ^featured_url} -> + %Tesla.Env{ + status: 200, + body: featured_data, + headers: [{"content-type", "application/activity+json"}] + } + end) + + refute capture_log(fn -> + assert {:ok, pinned_objects} = + ActivityPub.fetch_and_prepare_featured_from_ap_id(featured_url) + + assert Map.has_key?(pinned_objects, object_url) + end) =~ "Could not parse featured collection" + end end From cda6738309117a8ea95f8367175dbed3587e697b Mon Sep 17 00:00:00 2001 From: hj Date: Fri, 22 May 2026 20:47:52 +0000 Subject: [PATCH 141/391] Update priv/scrubbers/default.ex --- priv/scrubbers/default.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/priv/scrubbers/default.ex b/priv/scrubbers/default.ex index 8963d99e8..cc5eba027 100644 --- a/priv/scrubbers/default.ex +++ b/priv/scrubbers/default.ex @@ -105,7 +105,9 @@ defmodule Pleroma.HTML.Scrubber.Default do "mfm-sparkle", "mfm-rotate", "mfm-ruby", - "mfm-unixtime" + "mfm-unixtime", + # Exists in Akkoma but not Misskey? + "mfm-center" ]) Meta.allow_tag_with_this_attribute_values(:p, "class", ["quote-inline"]) From add3844c6d8f6e540d139f55e44047d946d9559a Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 23 May 2026 11:38:33 +0400 Subject: [PATCH 142/391] Persist notification group keys --- changelog.d/grouped-notifications-api.add | 2 +- lib/pleroma/notification.ex | 39 ++- .../operations/notification_operation.ex | 5 +- lib/pleroma/web/controller_helper.ex | 12 +- .../controllers/notification_controller.ex | 37 +-- lib/pleroma/web/mastodon_api/mastodon_api.ex | 307 ++++++++++++++---- .../mastodon_api/views/notification_view.ex | 44 ++- ...3070000_add_group_key_to_notifications.exs | 15 + .../notification_controller_test.exs | 157 ++++++++- .../views/notification_view_test.exs | 3 + 10 files changed, 505 insertions(+), 116 deletions(-) create mode 100644 priv/repo/migrations/20260523070000_add_group_key_to_notifications.exs diff --git a/changelog.d/grouped-notifications-api.add b/changelog.d/grouped-notifications-api.add index 82ce8b178..8c2cc5397 100644 --- a/changelog.d/grouped-notifications-api.add +++ b/changelog.d/grouped-notifications-api.add @@ -1 +1 @@ -Add Mastodon-compatible grouped notifications API endpoints under `/api/v2/notifications` +Add Mastodon-compatible grouped notifications API endpoints under `/api/v2/notifications` for newly-created notifications diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index ec10854e3..afd750823 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -35,6 +35,7 @@ defmodule Pleroma.Notification do # remember to add a migration to add it to the `notifications_type` enum # as well. field(:type, :string) + field(:group_key, :string) belongs_to(:user, User, type: FlakeId.Ecto.CompatType) belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType) @@ -49,7 +50,7 @@ defmodule Pleroma.Notification do |> type_from_activity() notification - |> changeset(%{type: type}) + |> changeset(%{type: type, group_key: grouped_notification_key(type, activity)}) |> Repo.update() end end @@ -97,14 +98,11 @@ defmodule Pleroma.Notification do group_key(notification, @groupable_notification_types) end - def group_key(%Notification{type: type} = notification, grouped_types) do + def group_key(%Notification{type: type, group_key: group_key} = notification, grouped_types) do grouped_types = normalize_grouped_types(grouped_types) - if type in @groupable_notification_types and type in grouped_types do - case group_target_id(notification) do - nil -> ungrouped_group_key(notification) - target_id -> "#{type}-#{target_id}-#{group_time_bucket(notification)}" - end + if type in @groupable_notification_types and type in grouped_types and is_binary(group_key) do + group_key else ungrouped_group_key(notification) end @@ -116,15 +114,27 @@ defmodule Pleroma.Notification do defp ungrouped_group_key(%Notification{id: id}), do: "ungrouped-#{id}" - defp group_time_bucket(%Notification{inserted_at: inserted_at}) do + defp grouped_notification_key(type, activity) when type in @groupable_notification_types do + with target_id when is_binary(target_id) <- group_target_id(type, activity) do + # Mastodon uses Redis to reuse a recent bucket for rolling 12h windows. Pleroma keeps keys + # deterministic and self-contained; clients must treat group_key as opaque either way. + "#{type}-#{target_id}-#{group_time_bucket(activity)}" + end + end + + defp grouped_notification_key(_type, _activity), do: nil + + defp group_time_bucket(%Activity{inserted_at: inserted_at}) when not is_nil(inserted_at) do inserted_at |> NaiveDateTime.to_erl() |> :calendar.datetime_to_gregorian_seconds() |> div(@group_bucket_seconds) end - defp group_target_id(%Notification{type: type, activity: activity}) - when type in ["favourite", "reblog"] do + defp group_time_bucket(_), + do: group_time_bucket(%Activity{inserted_at: NaiveDateTime.utc_now()}) + + defp group_target_id(type, activity) when type in ["favourite", "reblog"] do with object_id when is_binary(object_id) <- object_id_for(activity), %Activity{id: id} <- Activity.get_create_by_object_ap_id(object_id) do to_string(id) @@ -133,14 +143,14 @@ defmodule Pleroma.Notification do end end - defp group_target_id(%Notification{type: "follow", activity: %{data: %{"object" => ap_id}}}) do + defp group_target_id("follow", %{data: %{"object" => ap_id}}) do case User.get_cached_by_ap_id(ap_id) do %User{id: id} -> to_string(id) _ -> nil end end - defp group_target_id(_), do: nil + defp group_target_id(_, _), do: nil defp object_id_for(%{data: %{"object" => %{"id" => id}}}) when is_binary(id), do: id defp object_id_for(%{data: %{"object" => id}}) when is_binary(id), do: id @@ -163,7 +173,7 @@ defmodule Pleroma.Notification do def changeset(%Notification{} = notification, attrs) do notification - |> cast(attrs, [:seen, :type]) + |> cast(attrs, [:seen, :type, :group_key]) |> validate_inclusion(:type, @notification_types) end @@ -542,7 +552,8 @@ defmodule Pleroma.Notification do user_id: user.id, activity: activity, seen: mark_as_read?(activity, user), - type: type + type: type, + group_key: grouped_notification_key(type, activity) }) |> Marker.multi_set_last_read_id(user, "notifications") |> Repo.transaction() diff --git a/lib/pleroma/web/api_spec/operations/notification_operation.ex b/lib/pleroma/web/api_spec/operations/notification_operation.ex index ba996c13b..6c8f7d5e9 100644 --- a/lib/pleroma/web/api_spec/operations/notification_operation.ex +++ b/lib/pleroma/web/api_spec/operations/notification_operation.ex @@ -3,7 +3,6 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ApiSpec.NotificationOperation do - alias OpenApiSpex.Operation alias OpenApiSpex.Operation alias OpenApiSpex.Schema alias Pleroma.Web.ApiSpec.Schemas.Account @@ -125,7 +124,7 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do tags: ["Notifications"], summary: "Retrieve notification group accounts", operationId: "NotificationController.group_accounts", - security: [%{"oAuth" => ["write:notifications"]}], + security: [%{"oAuth" => ["read:notifications"]}], parameters: [group_key_param()], responses: %{ 200 => @@ -269,7 +268,7 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do }, example: %{ "id" => "34975861", - "group-key" => "ungrouped-34975861", + "group_key" => "ungrouped-34975861", "type" => "mention", "created_at" => "2019-11-23T07:49:02.064Z", "account" => Account.schema().example, diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index b15b0ea81..eecd1774f 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -55,16 +55,24 @@ defmodule Pleroma.Web.ControllerHelper do # TODO: Only fetch the params from open_api_spex when everything is converted @id_keys Pagination.page_keys() -- ["limit", "order"] + @id_key_atoms Enum.map(@id_keys, &String.to_atom/1) defp build_pagination_fields(conn, min_id, max_id, extra_params, order) do + path_param_keys = + conn.path_params + |> Map.keys() + |> Enum.flat_map(&[&1, String.to_existing_atom(&1)]) + params = if Map.has_key?(conn.private, :open_api_spex) do get_in(conn, [Access.key(:private), Access.key(:open_api_spex), Access.key(:params)]) else conn.params end - |> Map.drop(Map.keys(conn.path_params) |> Enum.map(&String.to_existing_atom/1)) + |> Map.drop(path_param_keys) |> Map.merge(extra_params) - |> Map.drop(@id_keys) + # OpenApiSpex casts params to atoms while uncast conn params are string-keyed. Drop both so + # generated links replace pagination cursors instead of preserving stale request cursors. + |> Map.drop(@id_keys ++ @id_key_atoms) {{next_id, nid}, {prev_id, pid}} = if order == :desc, diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex index 5cd8576c1..2cc31db0c 100644 --- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex @@ -13,7 +13,9 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do alias Pleroma.Web.MastodonAPI.MastodonAPI alias Pleroma.Web.Plugs.OAuthScopesPlug - @oauth_read_actions [:show, :index, :grouped_index, :show_group, :unread_count] + # Mastodon's docs currently list write:notifications for group accounts, but the endpoint is + # read-only and Mastodon's implementation accepts read:notifications. Prefer least privilege. + @oauth_read_actions [:show, :index, :grouped_index, :show_group, :group_accounts, :unread_count] plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false) @@ -92,7 +94,8 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do conn, _ ) do - notifications = MastodonAPI.get_notification_group(user, group_key, %{}) + {notifications, notification_group_counts, notification_group_bounds} = + MastodonAPI.get_notification_group_result(user, group_key, %{}) if Enum.empty?(notifications) do conn @@ -103,8 +106,11 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do render(conn, "grouped_index.json", notification_groups: [notifications], + notification_group_counts: notification_group_counts, + notification_group_bounds: notification_group_bounds, for: user, - grouped_types: grouped_types + grouped_types: grouped_types, + include_page_metadata: false ) end end @@ -115,10 +121,9 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do conn, _ ) do - users = - user - |> MastodonAPI.get_notification_group(group_key, %{}) - |> notification_actors() + # Mastodon paginates this endpoint in code, but the public docs say it returns accounts of all + # notifications in the group and do not document cursor params here. Follow the documented API. + users = MastodonAPI.get_notification_group_accounts(user, group_key) json(conn, AccountView.render("index.json", %{users: users, for: user})) end @@ -154,12 +159,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do conn, _ ) do - ids = - user - |> MastodonAPI.get_notification_group(group_key, %{}) - |> Enum.map(& &1.id) - - Notification.destroy_multiple(user, ids) + MastodonAPI.dismiss_notification_group(user, group_key) json(conn, %{}) end @@ -179,7 +179,8 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do defp do_get_grouped_notifications(%{assigns: %{user: user}} = conn, params) do params = normalize_notification_params(params) - {notification_groups, page_notifications, notification_group_counts} = + {notification_groups, page_notifications, notification_group_counts, + notification_group_bounds} = MastodonAPI.get_grouped_notification_page(user, params) conn @@ -187,6 +188,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do |> render("grouped_index.json", notification_groups: notification_groups, notification_group_counts: notification_group_counts, + notification_group_bounds: notification_group_bounds, for: user, grouped_types: params["grouped_types"] ) @@ -203,13 +205,6 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do |> Map.put_new("types", Map.get(params, :include_types, @default_notification_types)) end - defp notification_actors(notifications) do - notifications - |> Enum.map(&User.get_cached_by_ap_id(&1.activity.data["actor"])) - |> Enum.filter(& &1) - |> Enum.uniq_by(& &1.id) - end - # GET /api/v1/notifications/:id def show(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do with {:ok, notification} <- Notification.get(user, id) do diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex index 57cbfd6bf..cb11a3f4a 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do import Ecto.Query import Ecto.Changeset + alias Pleroma.Marker alias Pleroma.Notification alias Pleroma.Pagination alias Pleroma.Repo @@ -13,6 +14,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do alias Pleroma.User alias Pleroma.Web.CommonAPI + @notification_group_sample_limit 8 + @spec follow(User.t(), User.t(), map) :: {:ok, User.t()} | {:error, String.t()} def follow(follower, followed, params \\ %{}) do result = @@ -68,38 +71,55 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do end def get_grouped_notification_page(user, params \\ %{}) do - grouped_types = Map.get(params, "grouped_types", Map.get(params, :grouped_types)) - {query, order} = group_pagination_query(notifications_query(user, params), params) + grouped_types = + params + |> Map.get("grouped_types", Map.get(params, :grouped_types)) + |> Notification.normalize_grouped_types() - notifications = + query = notifications_query(user, params) + {order, cursor_filters} = group_pagination(params) + + group_rows = query - |> order_by([n], [{^order, n.id}]) - |> Repo.all() + |> notification_group_rows(grouped_types, grouped_limit(params), order, cursor_filters) - notification_group_counts = - Enum.frequencies_by(notifications, &Notification.group_key(&1, grouped_types)) + group_rows = if order == :asc, do: Enum.reverse(group_rows), else: group_rows - page_notifications = - notifications - |> take_grouped_page_notifications(grouped_types, grouped_limit(params)) + page_notifications = representative_notifications(query, group_rows) - page_notifications = - if order == :asc, do: Enum.reverse(page_notifications), else: page_notifications + notification_groups = + Enum.map(group_rows, ¬ification_group_sample(user, params, &1.group_key)) { - Notification.group_notifications(page_notifications, grouped_types), + notification_groups, page_notifications, - notification_group_counts + notification_group_counts(group_rows), + notification_group_bounds(group_rows) } end def get_grouped_notification_groups(user, params \\ %{}) do - {groups, _notifications, _notification_group_counts} = + {groups, _notifications, _notification_group_counts, _notification_group_bounds} = get_grouped_notification_page(user, params) groups end + def get_notification_group_result(user, group_key, params \\ %{}) do + notifications = notification_group_sample(user, params, group_key) + metadata = notification_group_metadata(user, group_key, params) + + if Enum.empty?(notifications) or is_nil(metadata) do + {[], %{}, %{}} + else + { + notifications, + %{group_key => metadata.notifications_count}, + %{group_key => Map.drop(metadata, [:notifications_count])} + } + end + end + def get_notification_group(user, group_key, params \\ %{}) def get_notification_group(user, "ungrouped-" <> notification_id, _params) do @@ -110,36 +130,58 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do end def get_notification_group(user, group_key, params) do - grouped_types = Map.get(params, "grouped_types", Map.get(params, :grouped_types)) - - user - |> notifications_query(params) - |> order_by([n], desc: n.id) - |> Repo.all() - |> Enum.filter(&(Notification.group_key(&1, grouped_types) == group_key)) + notification_group_sample(user, params, group_key) end - defp group_pagination_query(query, params) do + def get_notification_group_accounts(user, "ungrouped-" <> notification_id) do + with {:ok, notification} <- Notification.get(user, notification_id), + %User{} = actor <- User.get_cached_by_ap_id(notification.activity.data["actor"]) do + [actor] + else + _ -> [] + end + end + + def get_notification_group_accounts(user, group_key) do + user + |> notification_group_query(group_key, %{}) + |> exclude(:preload) + |> distinct(true) + |> select([user_actor: user_actor], user_actor) + |> Repo.all() + end + + def dismiss_notification_group(user, "ungrouped-" <> notification_id) do + Notification.destroy_multiple(user, [notification_id]) + end + + def dismiss_notification_group(%User{id: user_id}, group_key) do + Notification + |> where([n], n.user_id == ^user_id and n.group_key == ^group_key) + |> Repo.delete_all() + end + + defp group_pagination(params) do cond do min_id = Map.get(params, "min_id", Map.get(params, :min_id)) -> - query = where(query, [n], n.id > ^min_id) + cursor_filters = [{:gt, min_id}] - query = + cursor_filters = case Map.get(params, "max_id", Map.get(params, :max_id)) do - nil -> query - max_id -> where(query, [n], n.id < ^max_id) + nil -> cursor_filters + max_id -> [{:lt, max_id} | cursor_filters] end - {query, :asc} + {:asc, cursor_filters} since_id = Map.get(params, "since_id", Map.get(params, :since_id)) -> - {where(query, [n], n.id > ^since_id), :desc} + {:desc, [{:gt, since_id}]} max_id = Map.get(params, "max_id", Map.get(params, :max_id)) -> - {where(query, [n], n.id < ^max_id), :desc} + {:desc, [{:lt, max_id}]} true -> - {query, :desc} + {:desc, []} end end @@ -150,41 +192,190 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do |> min(80) end - defp take_grouped_page_notifications(notifications, grouped_types, group_limit) do - {notifications, _group_keys} = - Enum.reduce_while(notifications, {[], MapSet.new()}, fn notification, - {notifications, group_keys} -> - group_key = Notification.group_key(notification, grouped_types) - - cond do - MapSet.member?(group_keys, group_key) -> - {:cont, {[notification | notifications], group_keys}} - - MapSet.size(group_keys) < group_limit -> - {:cont, {[notification | notifications], MapSet.put(group_keys, group_key)}} - - true -> - {:halt, {notifications, group_keys}} - end - end) - - Enum.reverse(notifications) - end - def unread_notification_group_count(user, params \\ %{}) do - grouped_types = Map.get(params, "grouped_types", Map.get(params, :grouped_types)) + grouped_types = + params + |> Map.get("grouped_types", Map.get(params, :grouped_types)) + |> Notification.normalize_grouped_types() + limit = unread_count_limit(params) user |> notifications_query(params) - |> where([n], n.seen == false) - |> order_by([n], desc: n.id) - |> limit(^limit) - |> Repo.all() - |> Notification.group_notifications(grouped_types) + # The grouped API docs define unread by the notifications marker, not by Pleroma's per-row + # seen flag used by the v1 unread count. Keep this marker-based for Mastodon clients. + |> restrict_after_marker(notification_marker_last_read_id(user)) + |> notification_group_rows(grouped_types, limit, :desc, []) |> length() end + defp notification_group_rows(query, grouped_types, group_limit, :desc, cursor_filters) do + query + |> notification_group_keyed_query(grouped_types) + |> group_by([n], n.group_key) + |> apply_group_cursor_filters(cursor_filters) + |> select([n], %{ + group_key: n.group_key, + representative_id: max(n.id), + notifications_count: count(n.id), + page_min_id: min(n.id), + page_max_id: max(n.id), + latest_page_notification_at: max(n.inserted_at) + }) + |> order_by([n], desc: max(n.id)) + |> limit(^group_limit) + |> Repo.all() + end + + defp notification_group_rows(query, grouped_types, group_limit, :asc, cursor_filters) do + query + |> notification_group_keyed_query(grouped_types) + |> group_by([n], n.group_key) + |> apply_group_cursor_filters(cursor_filters) + |> select([n], %{ + group_key: n.group_key, + representative_id: max(n.id), + notifications_count: count(n.id), + page_min_id: min(n.id), + page_max_id: max(n.id), + latest_page_notification_at: max(n.inserted_at) + }) + |> order_by([n], asc: max(n.id)) + |> limit(^group_limit) + |> Repo.all() + end + + defp apply_group_cursor_filters(query, []), do: query + + defp apply_group_cursor_filters(query, [{:gt, id} | rest]) do + query + |> having([n], max(n.id) > ^id) + |> apply_group_cursor_filters(rest) + end + + defp apply_group_cursor_filters(query, [{:lt, id} | rest]) do + query + |> having([n], max(n.id) < ^id) + |> apply_group_cursor_filters(rest) + end + + defp notification_group_keyed_query(query, grouped_types) do + query + |> exclude(:preload) + |> select([n], %{ + id: n.id, + inserted_at: n.inserted_at, + group_key: + fragment( + "CASE WHEN ? IS NOT NULL AND ?::text = ANY(?) THEN ? ELSE 'ungrouped-' || ?::text END", + n.group_key, + n.type, + type(^grouped_types, {:array, :string}), + n.group_key, + n.id + ) + }) + |> subquery() + end + + defp representative_notifications(_query, []), do: [] + + defp representative_notifications(query, group_rows) do + representative_ids = Enum.map(group_rows, & &1.representative_id) + + notifications_by_id = + query + |> where([n], n.id in ^representative_ids) + |> Repo.all() + |> Map.new(&{to_string(&1.id), &1}) + + group_rows + |> Enum.map(&Map.get(notifications_by_id, to_string(&1.representative_id))) + |> Enum.filter(& &1) + end + + defp notification_group_counts(group_rows) do + Map.new(group_rows, &{&1.group_key, &1.notifications_count}) + end + + defp notification_group_bounds(group_rows) do + Map.new(group_rows, fn row -> + {row.group_key, + %{ + page_min_id: row.page_min_id, + page_max_id: row.page_max_id, + latest_page_notification_at: row.latest_page_notification_at + }} + end) + end + + defp notification_group_sample(user, _params, "ungrouped-" <> notification_id) do + case Notification.get(user, notification_id) do + {:ok, notification} -> [notification] + _ -> [] + end + end + + defp notification_group_sample(user, params, group_key) do + user + |> notification_group_query(group_key, params) + |> order_by([n], desc: n.id) + |> limit(^@notification_group_sample_limit) + |> Repo.all() + end + + defp notification_group_metadata(user, "ungrouped-" <> notification_id, _params) do + case Notification.get(user, notification_id) do + {:ok, notification} -> + %{ + notifications_count: 1, + page_min_id: notification.id, + page_max_id: notification.id, + latest_page_notification_at: notification.inserted_at + } + + _ -> + nil + end + end + + defp notification_group_metadata(user, group_key, params) do + user + |> notification_group_query(group_key, params) + |> exclude(:preload) + |> select([n], %{ + notifications_count: count(n.id), + page_min_id: min(n.id), + page_max_id: max(n.id), + latest_page_notification_at: max(n.inserted_at) + }) + |> Repo.one() + |> case do + %{notifications_count: 0} -> nil + metadata -> metadata + end + end + + defp notification_group_query(user, group_key, params) do + user + |> notifications_query(params) + |> where([n], n.group_key == ^group_key) + end + + defp notification_marker_last_read_id(user) do + Marker + |> where([m], m.user_id == ^user.id and m.timeline == "notifications") + |> select([m], m.last_read_id) + |> Repo.one() + end + + defp restrict_after_marker(query, last_read_id) + when is_binary(last_read_id) and last_read_id != "" do + where(query, [n], n.id > ^last_read_id) + end + + defp restrict_after_marker(query, _last_read_id), do: query + defp notifications_query(user, params) do options = notification_options(user, params) diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex index 466073853..33d4b2924 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -90,6 +90,8 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do ) do grouped_types = Notification.normalize_grouped_types(opts[:grouped_types]) notification_group_counts = Map.get(opts, :notification_group_counts, %{}) + notification_group_bounds = Map.get(opts, :notification_group_bounds, %{}) + include_page_metadata = Map.get(opts, :include_page_metadata, true) statuses = notification_groups @@ -110,7 +112,14 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do notification_groups: Enum.map( notification_groups, - &render_group(&1, reading_user, grouped_types, notification_group_counts) + &render_group( + &1, + reading_user, + grouped_types, + notification_group_counts, + notification_group_bounds, + include_page_metadata + ) ) } end @@ -184,27 +193,50 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do [%Notification{} = notification | _] = notifications, _reading_user, grouped_types, - notification_group_counts + notification_group_counts, + notification_group_bounds, + include_page_metadata ) do latest_notification = List.first(notifications) oldest_notification = List.last(notifications) status_activity = status_activity_for_group(notifications, grouped_types) group_key = Notification.group_key(notification, grouped_types) + bounds = Map.get(notification_group_bounds, group_key, %{}) response = %{ group_key: group_key, notifications_count: Map.get(notification_group_counts, group_key, length(notifications)), type: notification.type, - most_recent_notification_id: to_string(latest_notification.id), - page_min_id: to_string(oldest_notification.id), - page_max_id: to_string(latest_notification.id), - latest_page_notification_at: CommonAPI.Utils.to_masto_date(latest_notification.inserted_at), + most_recent_notification_id: + bounds + |> Map.get(:page_max_id, latest_notification.id) + |> to_string(), sample_account_ids: notifications |> notification_actors() |> Enum.map(&to_string(&1.id)) } + response = + if include_page_metadata do + Map.merge(response, %{ + page_min_id: + bounds + |> Map.get(:page_min_id, oldest_notification.id) + |> to_string(), + page_max_id: + bounds + |> Map.get(:page_max_id, latest_notification.id) + |> to_string(), + latest_page_notification_at: + bounds + |> Map.get(:latest_page_notification_at, latest_notification.inserted_at) + |> CommonAPI.Utils.to_masto_date() + }) + else + response + end + if status_activity do Map.put(response, :status_id, to_string(status_activity.id)) else diff --git a/priv/repo/migrations/20260523070000_add_group_key_to_notifications.exs b/priv/repo/migrations/20260523070000_add_group_key_to_notifications.exs new file mode 100644 index 000000000..7c7d529e0 --- /dev/null +++ b/priv/repo/migrations/20260523070000_add_group_key_to_notifications.exs @@ -0,0 +1,15 @@ +defmodule Pleroma.Repo.Migrations.AddGroupKeyToNotifications do + use Ecto.Migration + + def change do + alter table(:notifications) do + add(:group_key, :string) + end + + create_if_not_exists( + index(:notifications, [:user_id, :group_key, "id desc nulls last"], + where: "group_key IS NOT NULL" + ) + ) + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs index 1d3a9e966..e9c521d6b 100644 --- a/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs @@ -587,10 +587,10 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do {:ok, _} = CommonAPI.favorite(status.id, other_user1) {:ok, _} = CommonAPI.favorite(status.id, other_user2) - notification_ids = - Notification - |> Repo.all() - |> Enum.map(&to_string(&1.id)) + notifications = Repo.all(Notification) + notification_ids = Enum.map(notifications, &to_string(&1.id)) + assert [persisted_group_key] = notifications |> Enum.map(& &1.group_key) |> Enum.uniq() + assert is_binary(persisted_group_key) result = conn @@ -613,6 +613,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do assert group["latest_page_notification_at"] assert Enum.sort(group["sample_account_ids"]) == Enum.sort([other_user1.id, other_user2.id]) group_key = group["group_key"] + assert group_key == persisted_group_key assert [%{"group_key" => ^group_key}, %{"group_key" => ^group_key}] = conn @@ -626,6 +627,9 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do assert shown_group["group_key"] == group_key assert shown_group["notifications_count"] == 2 + refute Map.has_key?(shown_group, "page_min_id") + refute Map.has_key?(shown_group, "page_max_id") + refute Map.has_key?(shown_group, "latest_page_notification_at") end test "round-trips ungrouped group keys when grouped_types excludes a type" do @@ -648,6 +652,48 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do |> json_response_and_validate_schema(200) end + test "keeps legacy notifications without persisted group keys ungrouped" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + other_user1 = insert(:user) + other_user2 = insert(:user) + + {:ok, status} = CommonAPI.post(user, %{status: "hello"}) + {:ok, favorite1} = CommonAPI.favorite(status.id, other_user1) + {:ok, favorite2} = CommonAPI.favorite(status.id, other_user2) + + notification_ids = + [favorite1, favorite2] + |> Enum.map(&get_notification_id_by_activity/1) + + Repo.update_all(Notification, set: [group_key: nil]) + + expected_group_keys = Enum.map(notification_ids, &"ungrouped-#{&1}") + + assert [%{"group_key" => group_key1}, %{"group_key" => group_key2}] = + conn + |> get("/api/v1/notifications") + |> json_response_and_validate_schema(200) + + assert Enum.sort([group_key1, group_key2]) == Enum.sort(expected_group_keys) + + %{"notification_groups" => groups} = + conn + |> get("/api/v2/notifications") + |> json_response_and_validate_schema(200) + + assert Enum.sort(Enum.map(groups, & &1["group_key"])) == Enum.sort(expected_group_keys) + assert Enum.all?(groups, &(&1["notifications_count"] == 1)) + + group_key = List.first(expected_group_keys) + + assert %{ + "notification_groups" => [%{"group_key" => ^group_key, "notifications_count" => 1}] + } = + conn + |> get("/api/v2/notifications/#{group_key}") + |> json_response_and_validate_schema(200) + end + test "paginates notification groups instead of raw notifications" do %{user: user, conn: conn} = oauth_access(["read:notifications"]) other_user1 = insert(:user) @@ -674,6 +720,44 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do ] = groups end + test "uses group-level cursors when paginating notification groups" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + other_user1 = insert(:user) + other_user2 = insert(:user) + other_user3 = insert(:user) + + {:ok, older_status} = CommonAPI.post(user, %{status: "older"}) + {:ok, _} = CommonAPI.favorite(older_status.id, other_user3) + + {:ok, newer_status} = CommonAPI.post(user, %{status: "newer"}) + {:ok, _} = CommonAPI.favorite(newer_status.id, other_user1) + {:ok, _} = CommonAPI.favorite(newer_status.id, other_user2) + older_status_id = older_status.id + newer_status_id = newer_status.id + + assert %{ + "notification_groups" => [ + %{ + "status_id" => ^newer_status_id, + "notifications_count" => 2, + "most_recent_notification_id" => cursor + } + ] + } = + conn + |> get("/api/v2/notifications?limit=1") + |> json_response_and_validate_schema(200) + + assert %{ + "notification_groups" => [ + %{"status_id" => ^older_status_id, "notifications_count" => 1} + ] + } = + conn + |> get("/api/v2/notifications?limit=1&max_id=#{cursor}") + |> json_response_and_validate_schema(200) + end + test "returns total notification count for a partially represented group" do %{user: user, conn: conn} = oauth_access(["read:notifications"]) other_user1 = insert(:user) @@ -715,27 +799,78 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do |> json_response_and_validate_schema(200) end + test "counts unread notification groups newer than the notification marker" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + other_user = insert(:user) + mentioner = insert(:user) + + {:ok, status} = CommonAPI.post(user, %{status: "older"}) + {:ok, favorite_activity} = CommonAPI.favorite(status.id, other_user) + marker_id = get_notification_id_by_activity(favorite_activity) + + {:ok, _} = CommonAPI.post(mentioner, %{status: "newer @#{user.nickname}"}) + + {:ok, _} = + Pleroma.Marker.upsert(user, %{"notifications" => %{"last_read_id" => marker_id}}) + + assert %{"count" => 1} = + conn + |> get("/api/v2/notifications/unread_count") + |> json_response_and_validate_schema(200) + end + + test "does not preserve stale cursor params in grouped notification link headers" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + other_user = insert(:user) + + {:ok, activity1} = CommonAPI.post(other_user, %{status: "one @#{user.nickname}"}) + {:ok, _activity2} = CommonAPI.post(other_user, %{status: "two @#{user.nickname}"}) + {:ok, activity3} = CommonAPI.post(other_user, %{status: "three @#{user.nickname}"}) + + notification1_id = get_notification_id_by_activity(activity1) + notification3_id = get_notification_id_by_activity(activity3) + + conn = get(conn, "/api/v2/notifications?since_id=#{notification1_id}&limit=1") + + assert [link_header] = get_resp_header(conn, "link") + assert link_header =~ ~r/max_id=#{notification3_id}/ + refute link_header =~ "since_id=" + end + test "lists accounts from and dismisses a notification group" do %{user: user, conn: conn} = oauth_access(["read:notifications", "write:notifications"]) - other_user1 = insert(:user) - other_user2 = insert(:user) + other_users = insert_list(9, :user) {:ok, status} = CommonAPI.post(user, %{status: "hello"}) - {:ok, _} = CommonAPI.favorite(status.id, other_user1) - {:ok, _} = CommonAPI.favorite(status.id, other_user2) - %{"notification_groups" => [%{"group_key" => group_key}]} = + Enum.each(other_users, fn other_user -> + {:ok, _} = CommonAPI.favorite(status.id, other_user) + end) + + %{ + "notification_groups" => [ + %{ + "group_key" => group_key, + "notifications_count" => 9, + "sample_account_ids" => sample_account_ids + } + ] + } = conn |> get("/api/v2/notifications") |> json_response_and_validate_schema(200) + assert length(sample_account_ids) == 8 + + %{conn: read_conn} = oauth_access(["read:notifications"], user: user) + account_ids = - conn + read_conn |> get("/api/v2/notifications/#{group_key}/accounts") |> json_response_and_validate_schema(200) |> Enum.map(& &1["id"]) - assert Enum.sort(account_ids) == Enum.sort([other_user1.id, other_user2.id]) + assert Enum.sort(account_ids) == Enum.sort(Enum.map(other_users, & &1.id)) assert %{} = conn diff --git a/test/pleroma/web/mastodon_api/views/notification_view_test.exs b/test/pleroma/web/mastodon_api/views/notification_view_test.exs index aa11ad682..2d0801589 100644 --- a/test/pleroma/web/mastodon_api/views/notification_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/notification_view_test.exs @@ -98,6 +98,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do {:ok, favorite_activity} = CommonAPI.favorite(create_activity.id, another_user) {:ok, [notification]} = Notification.create_notifications(favorite_activity) create_activity = Activity.get_by_id(create_activity.id) + assert is_binary(notification.group_key) expected = %{ id: to_string(notification.id), @@ -119,6 +120,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do {:ok, reblog_activity} = CommonAPI.repeat(create_activity.id, another_user) {:ok, [notification]} = Notification.create_notifications(reblog_activity) reblog_activity = Activity.get_by_id(create_activity.id) + assert is_binary(notification.group_key) expected = %{ id: to_string(notification.id), @@ -138,6 +140,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do followed = insert(:user) {:ok, followed, follower, _activity} = CommonAPI.follow(followed, follower) notification = Notification |> Repo.one() |> Repo.preload(:activity) + assert is_binary(notification.group_key) expected = %{ id: to_string(notification.id), From 1db2b99e4873a5196cbbb393c294419e10045a15 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 23 May 2026 14:00:17 +0400 Subject: [PATCH 143/391] Run group key migration before notification backfill --- ...ions.exs => 20200602125217_add_group_key_to_notifications.exs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename priv/repo/migrations/{20260523070000_add_group_key_to_notifications.exs => 20200602125217_add_group_key_to_notifications.exs} (100%) diff --git a/priv/repo/migrations/20260523070000_add_group_key_to_notifications.exs b/priv/repo/migrations/20200602125217_add_group_key_to_notifications.exs similarity index 100% rename from priv/repo/migrations/20260523070000_add_group_key_to_notifications.exs rename to priv/repo/migrations/20200602125217_add_group_key_to_notifications.exs From a2a23b327b9c32420b5a6aed4198073b4f363864 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 23 May 2026 14:27:11 +0400 Subject: [PATCH 144/391] Keep cursor cleanup scoped to grouped notifications --- lib/pleroma/web/controller_helper.ex | 10 +++++++--- .../controllers/notification_controller.ex | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index eecd1774f..0c85e79f9 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -56,7 +56,11 @@ defmodule Pleroma.Web.ControllerHelper do # TODO: Only fetch the params from open_api_spex when everything is converted @id_keys Pagination.page_keys() -- ["limit", "order"] @id_key_atoms Enum.map(@id_keys, &String.to_atom/1) + @drop_id_params_key :drop_id_params defp build_pagination_fields(conn, min_id, max_id, extra_params, order) do + drop_id_params? = Map.get(extra_params, @drop_id_params_key, false) + extra_params = Map.delete(extra_params, @drop_id_params_key) + path_param_keys = conn.path_params |> Map.keys() @@ -70,9 +74,9 @@ defmodule Pleroma.Web.ControllerHelper do end |> Map.drop(path_param_keys) |> Map.merge(extra_params) - # OpenApiSpex casts params to atoms while uncast conn params are string-keyed. Drop both so - # generated links replace pagination cursors instead of preserving stale request cursors. - |> Map.drop(@id_keys ++ @id_key_atoms) + # Some OpenApiSpex routes cast cursor params to atoms. Keep the historical default for + # existing endpoints, but allow grouped notifications to opt out of stale request cursors. + |> Map.drop(@id_keys ++ if(drop_id_params?, do: @id_key_atoms, else: [])) {{next_id, nid}, {prev_id, pid}} = if order == :desc, diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex index 2cc31db0c..8a5f8e7b6 100644 --- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex @@ -5,7 +5,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do use Pleroma.Web, :controller - import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] + import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2, add_link_headers: 3] alias Pleroma.Notification alias Pleroma.User @@ -184,7 +184,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do MastodonAPI.get_grouped_notification_page(user, params) conn - |> add_link_headers(page_notifications) + |> add_link_headers(page_notifications, %{drop_id_params: true}) |> render("grouped_index.json", notification_groups: notification_groups, notification_group_counts: notification_group_counts, From 94a28d128605bc4e3328293a01585471c193f82b Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 16 May 2026 00:31:02 +0200 Subject: [PATCH 145/391] RichMedia Backfill: Add cachex positive test --- test/pleroma/web/rich_media/backfill_test.exs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/pleroma/web/rich_media/backfill_test.exs b/test/pleroma/web/rich_media/backfill_test.exs index 6d221fcf5..8b4834bf7 100644 --- a/test/pleroma/web/rich_media/backfill_test.exs +++ b/test/pleroma/web/rich_media/backfill_test.exs @@ -23,4 +23,26 @@ defmodule Pleroma.Web.RichMedia.BackfillTest do Backfill.run(%{"url" => url}) end + + test "sets a warm_cache entry" do + url = "https://good.example.com/" + url_hash = Card.url_to_hash(url) + + Tesla.Mock.mock(fn %{url: ^url} -> + {:ok, + %Tesla.Env{ + status: 200, + body: "" + }} + end) + + Pleroma.CachexMock + |> expect(:put, fn :rich_media_cache, + ^url_hash, + %Pleroma.Web.RichMedia.Card{url_hash: ^url_hash} -> + {:ok, true} + end) + + Backfill.run(%{"url" => url}) + end end From 678fe8a0642771e24d7b4d9b1df78578024351fe Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 20 May 2026 20:16:19 +0200 Subject: [PATCH 146/391] RichMedia: Add support for disabling wss streaming out on backfill --- .../web/mastodon_api/views/status_view.ex | 9 +-- lib/pleroma/web/rich_media/backfill.ex | 14 +++-- lib/pleroma/web/rich_media/card.ex | 16 ++++-- test/pleroma/web/rich_media/backfill_test.exs | 56 ++++++++++++++++++- 4 files changed, 79 insertions(+), 16 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 09af60865..80d53adff 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -28,9 +28,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do # This is a naive way to do this, just spawning a process per activity # to fetch the preview. However it should be fine considering # pagination is restricted to 40 activities at a time - defp fetch_rich_media_for_activities(activities) do + defp fetch_rich_media_for_activities(activities, opts) do Enum.each(activities, fn activity -> - Card.get_by_activity(activity) + Card.get_by_activity(activity, opts) end) end @@ -113,7 +113,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do activities = Enum.filter(opts.activities, & &1) # Start prefetching rich media before doing anything else - fetch_rich_media_for_activities(activities) + fetch_rich_media_for_activities(activities, opts) + replied_to_activities = get_replied_to_activities(activities) quoted_activities = get_quoted_activities(activities) @@ -362,7 +363,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do summary = object.data["summary"] || "" card = - case Card.get_by_activity(activity) do + case Card.get_by_activity(activity, opts) do %Card{} = result -> render("card.json", result) _ -> nil end diff --git a/lib/pleroma/web/rich_media/backfill.ex b/lib/pleroma/web/rich_media/backfill.ex index 1cd90629f..607c5238a 100644 --- a/lib/pleroma/web/rich_media/backfill.ex +++ b/lib/pleroma/web/rich_media/backfill.ex @@ -11,6 +11,8 @@ defmodule Pleroma.Web.RichMedia.Backfill do require Logger + @callback run(map()) :: :ok | Parser.parse_errors() | Helpers.get_errors() + @cachex Pleroma.Config.get([:cachex, :provider], Cachex) @stream_out_impl Pleroma.Config.get( [__MODULE__, :stream_out], @@ -26,11 +28,7 @@ defmodule Pleroma.Web.RichMedia.Backfill do {:ok, card} = Card.create(url, fields) maybe_schedule_expiration(url, fields) - - with %{"activity_id" => activity_id} <- args, - false <- is_nil(activity_id) do - stream_update(args) - end + maybe_update_stream(args) warm_cache(url_hash, card) :ok @@ -55,12 +53,16 @@ defmodule Pleroma.Web.RichMedia.Backfill do end end - defp stream_update(%{"activity_id" => activity_id}) do + defp maybe_update_stream(%{"activity_id" => activity_id, "stream" => true}) when is_binary(activity_id) do Pleroma.Activity.get_by_id(activity_id) |> Pleroma.Activity.normalize() |> @stream_out_impl.stream_out() end + # Streamer.stream_out returns noop when unsupported activity type is requested to be streamed. + # Do the same here for unwanted streaming + defp maybe_update_stream(_), do: :noop + defp warm_cache(key, val), do: @cachex.put(:rich_media_cache, key, val) defp negative_cache(key, ttl \\ :timer.minutes(15)), diff --git a/lib/pleroma/web/rich_media/card.ex b/lib/pleroma/web/rich_media/card.ex index 6b4bb9555..86a4f577a 100644 --- a/lib/pleroma/web/rich_media/card.ex +++ b/lib/pleroma/web/rich_media/card.ex @@ -90,8 +90,12 @@ defmodule Pleroma.Web.RichMedia.Card do nil -> activity_id = Keyword.get(opts, :activity_id, nil) + # Nested opts, first layer comes from get_by_activity/2 as Keyword, second from API views/Federation as Map. + # Provide default Map when called directly. + opts = Keyword.get(opts, :opts, %{}) + stream = Map.get(opts, :stream, true) - RichMediaWorker.new(%{"op" => "backfill", "url" => url, "activity_id" => activity_id}) + RichMediaWorker.new(%{"op" => "backfill", "url" => url, "activity_id" => activity_id, "stream" => stream}) |> Oban.insert() nil @@ -112,9 +116,11 @@ defmodule Pleroma.Web.RichMedia.Card do end end - @spec get_by_activity(Activity.t()) :: t() | nil | :error + @spec get_by_activity(Activity.t(), %{}) :: t() | nil | :error + def get_by_activity(activity, opts \\ %{}) + # Fake/Draft activity - def get_by_activity(%Activity{id: "pleroma:fakeid"} = activity) do + def get_by_activity(%Activity{id: "pleroma:fakeid"} = activity, _opts) do with {_, true} <- {:config, @config_impl.get([:rich_media, :enabled])}, %Object{} = object <- Object.normalize(activity, fetch: false), url when not is_nil(url) <- HTML.extract_first_external_url_from_object(object) do @@ -138,13 +144,13 @@ defmodule Pleroma.Web.RichMedia.Card do end end - def get_by_activity(activity) do + def get_by_activity(activity, opts) do with %Object{} = object <- Object.normalize(activity, fetch: false), {_, nil} <- {:cached, get_cached_url(object, activity.id)} do nil else {:cached, url} -> - get_or_backfill_by_url(url, activity_id: activity.id) + get_or_backfill_by_url(url, activity_id: activity.id, opts: opts) _ -> :error diff --git a/test/pleroma/web/rich_media/backfill_test.exs b/test/pleroma/web/rich_media/backfill_test.exs index 8b4834bf7..f7e74c02a 100644 --- a/test/pleroma/web/rich_media/backfill_test.exs +++ b/test/pleroma/web/rich_media/backfill_test.exs @@ -5,12 +5,20 @@ defmodule Pleroma.Web.RichMedia.BackfillTest do use Pleroma.DataCase + alias Pleroma.Web.CommonAPI alias Pleroma.Web.RichMedia.Backfill alias Pleroma.Web.RichMedia.Card import Mox + import Pleroma.Factory - setup_all do: clear_config([:rich_media, :enabled], true) + setup do + clear_config([:rich_media, :enabled], true) + + Mox.stub_with(Pleroma.CachexMock, Pleroma.NullCache) + + :ok + end test "sets a negative cache entry for an error" do url = "https://bad.example.com/" @@ -45,4 +53,50 @@ defmodule Pleroma.Web.RichMedia.BackfillTest do Backfill.run(%{"url" => url}) end + + test "streams out update when stream == true" do + url = "https://example.com" + user = insert(:user) + + Tesla.Mock.mock(fn %{url: ^url} -> + {:ok, + %Tesla.Env{ + status: 200, + body: "" + }} + end) + + {:ok, activity} = CommonAPI.post(user, %{status: "#cofe #{url}"}) + + Pleroma.CachexMock + |> expect(:put, fn :rich_media_cache, _, _ -> {:ok, true} end) + + Pleroma.Web.ActivityPub.ActivityPubMock + |> expect(:stream_out, fn _ -> :ok end) + + Backfill.run(%{"url" => url, "activity_id" => "#{activity.data["id"]}", "stream" => true}) + end + + test "does not stream out update when stream == false" do + url = "https://example.com" + user = insert(:user) + + Tesla.Mock.mock(fn %{url: ^url} -> + {:ok, + %Tesla.Env{ + status: 200, + body: "" + }} + end) + + {:ok, activity} = CommonAPI.post(user, %{status: "#cofe #{url}"}) + + Pleroma.CachexMock + |> expect(:put, fn :rich_media_cache, _, _ -> {:ok, true} end) + + Pleroma.Web.ActivityPub.ActivityPubMock + |> deny(:stream_out, 1) + + Backfill.run(%{"url" => url, "activity_id" => "#{activity.data["id"]}", "stream" => false}) + end end From 5f55c9653c64ced6e1461224a942a303e82627d8 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 20 May 2026 20:18:47 +0200 Subject: [PATCH 147/391] RichMedia: Disable websockets backfill streaming in StatusView --- lib/pleroma/web/mastodon_api/views/status_view.ex | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 80d53adff..cc807f603 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -28,7 +28,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do # This is a naive way to do this, just spawning a process per activity # to fetch the preview. However it should be fine considering # pagination is restricted to 40 activities at a time + # Force disable Websockets streaming for backfill jobs, + # otherwise old posts can show up on timelines. defp fetch_rich_media_for_activities(activities, opts) do + opts = Map.put(opts, :stream, false) Enum.each(activities, fn activity -> Card.get_by_activity(activity, opts) end) @@ -362,8 +365,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do summary = object.data["summary"] || "" + # Force disable Websockets streaming for backfill jobs which the below call will create, + # otherwise old posts can show up on timelines. card = - case Card.get_by_activity(activity, opts) do + case Card.get_by_activity(activity, Map.put(opts, :stream, false)) do %Card{} = result -> render("card.json", result) _ -> nil end From ff7927e219f84ada9235353457f2c2497d336964 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 22 May 2026 14:53:28 +0200 Subject: [PATCH 148/391] changelog --- changelog.d/wss-necroposts.fix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/wss-necroposts.fix diff --git a/changelog.d/wss-necroposts.fix b/changelog.d/wss-necroposts.fix new file mode 100644 index 000000000..743f9087c --- /dev/null +++ b/changelog.d/wss-necroposts.fix @@ -0,0 +1 @@ +RichMedia: Fix backfill causing old posts to show up on timelines by disabling it in MastoAPI StatusView From 6ee40cb2ebe7a6666bedea3773710a3b555791ad Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 23 May 2026 19:43:44 +0200 Subject: [PATCH 149/391] RichMedia: Add StatusView backfill streaming tests --- test/pleroma/web/rich_media/backfill_test.exs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/test/pleroma/web/rich_media/backfill_test.exs b/test/pleroma/web/rich_media/backfill_test.exs index f7e74c02a..a4b2d34fd 100644 --- a/test/pleroma/web/rich_media/backfill_test.exs +++ b/test/pleroma/web/rich_media/backfill_test.exs @@ -8,6 +8,7 @@ defmodule Pleroma.Web.RichMedia.BackfillTest do alias Pleroma.Web.CommonAPI alias Pleroma.Web.RichMedia.Backfill alias Pleroma.Web.RichMedia.Card + alias Pleroma.Tests.ObanHelpers import Mox import Pleroma.Factory @@ -15,6 +16,7 @@ defmodule Pleroma.Web.RichMedia.BackfillTest do setup do clear_config([:rich_media, :enabled], true) + Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig) Mox.stub_with(Pleroma.CachexMock, Pleroma.NullCache) :ok @@ -99,4 +101,64 @@ defmodule Pleroma.Web.RichMedia.BackfillTest do Backfill.run(%{"url" => url, "activity_id" => "#{activity.data["id"]}", "stream" => false}) end + + # NOTE: Below two MastoAPI tests cover almost the same code paths. + # index.json will always prefetch rich media, while show.json will try to get the card and + # fetch it when it isn't cached (both use Card.get_by_activity in the end). + # So if index.json doesn't fetch the rich media, show.json will when it renders the post, + # hence why index.json test will only call ActivityPub.stream_out twice, + # if streaming is re-enabled for in both. + test "does not stream out in MastoAPI StatusView index" do + url = "https://example.com" + user = insert(:user) + + Tesla.Mock.mock(fn %{url: ^url} -> + {:ok, + %Tesla.Env{ + status: 200, + body: "" + }} + end) + + # CommonAPI federation processing will stream out once as a new post + Pleroma.Web.ActivityPub.ActivityPubMock + |> expect(:stream_out, 1, fn _ -> :ok end) + + {:ok, activity} = CommonAPI.post(user, %{status: "#cofe #{url}"}) + ObanHelpers.perform_all() + + # Clear cache to force backfill below + Pleroma.Activity.HTML.invalidate_cache_for(activity.id) + Pleroma.Web.RichMedia.Card.delete(url) + + Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{activities: [activity], as: :activity}) + ObanHelpers.perform_all() + end + + test "does not stream out in MastoAPI StatusView show" do + url = "https://example.com" + user = insert(:user) + + Tesla.Mock.mock(fn %{url: ^url} -> + {:ok, + %Tesla.Env{ + status: 200, + body: "" + }} + end) + + # CommonAPI federation processing will stream out once as a new post + Pleroma.Web.ActivityPub.ActivityPubMock + |> expect(:stream_out, 1, fn _ -> :ok end) + + {:ok, activity} = CommonAPI.post(user, %{status: "#cofe #{url}"}) + ObanHelpers.perform_all() + + # Clear cache to force backfill below + Pleroma.Activity.HTML.invalidate_cache_for(activity.id) + Pleroma.Web.RichMedia.Card.delete(url) + + Pleroma.Web.MastodonAPI.StatusView.render("show.json", activity: activity) + ObanHelpers.perform_all() + end end From 0c0d6873306244ad98273a6ae661a4414cee875b Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 23 May 2026 19:49:22 +0200 Subject: [PATCH 150/391] credo, lint --- lib/pleroma/web/mastodon_api/views/status_view.ex | 1 + lib/pleroma/web/rich_media/backfill.ex | 3 ++- lib/pleroma/web/rich_media/card.ex | 11 +++++++++-- test/pleroma/web/rich_media/backfill_test.exs | 10 +++++++--- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index cc807f603..6949c5f2d 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -32,6 +32,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do # otherwise old posts can show up on timelines. defp fetch_rich_media_for_activities(activities, opts) do opts = Map.put(opts, :stream, false) + Enum.each(activities, fn activity -> Card.get_by_activity(activity, opts) end) diff --git a/lib/pleroma/web/rich_media/backfill.ex b/lib/pleroma/web/rich_media/backfill.ex index 607c5238a..a100b39ab 100644 --- a/lib/pleroma/web/rich_media/backfill.ex +++ b/lib/pleroma/web/rich_media/backfill.ex @@ -53,7 +53,8 @@ defmodule Pleroma.Web.RichMedia.Backfill do end end - defp maybe_update_stream(%{"activity_id" => activity_id, "stream" => true}) when is_binary(activity_id) do + defp maybe_update_stream(%{"activity_id" => activity_id, "stream" => true}) + when is_binary(activity_id) do Pleroma.Activity.get_by_id(activity_id) |> Pleroma.Activity.normalize() |> @stream_out_impl.stream_out() diff --git a/lib/pleroma/web/rich_media/card.ex b/lib/pleroma/web/rich_media/card.ex index 86a4f577a..1e9e66ec1 100644 --- a/lib/pleroma/web/rich_media/card.ex +++ b/lib/pleroma/web/rich_media/card.ex @@ -90,12 +90,19 @@ defmodule Pleroma.Web.RichMedia.Card do nil -> activity_id = Keyword.get(opts, :activity_id, nil) - # Nested opts, first layer comes from get_by_activity/2 as Keyword, second from API views/Federation as Map. + + # Nested opts, first layer comes from get_by_activity/2 as Keyword, + # second from API views/Federation as Map. # Provide default Map when called directly. opts = Keyword.get(opts, :opts, %{}) stream = Map.get(opts, :stream, true) - RichMediaWorker.new(%{"op" => "backfill", "url" => url, "activity_id" => activity_id, "stream" => stream}) + RichMediaWorker.new(%{ + "op" => "backfill", + "url" => url, + "activity_id" => activity_id, + "stream" => stream + }) |> Oban.insert() nil diff --git a/test/pleroma/web/rich_media/backfill_test.exs b/test/pleroma/web/rich_media/backfill_test.exs index a4b2d34fd..a1949533a 100644 --- a/test/pleroma/web/rich_media/backfill_test.exs +++ b/test/pleroma/web/rich_media/backfill_test.exs @@ -5,15 +5,15 @@ defmodule Pleroma.Web.RichMedia.BackfillTest do use Pleroma.DataCase + alias Pleroma.Tests.ObanHelpers alias Pleroma.Web.CommonAPI alias Pleroma.Web.RichMedia.Backfill alias Pleroma.Web.RichMedia.Card - alias Pleroma.Tests.ObanHelpers import Mox import Pleroma.Factory - setup do + setup do clear_config([:rich_media, :enabled], true) Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig) @@ -131,7 +131,11 @@ defmodule Pleroma.Web.RichMedia.BackfillTest do Pleroma.Activity.HTML.invalidate_cache_for(activity.id) Pleroma.Web.RichMedia.Card.delete(url) - Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{activities: [activity], as: :activity}) + Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{ + activities: [activity], + as: :activity + }) + ObanHelpers.perform_all() end From 4c9fc6287e547d908ee0723996fc978653d4652d Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 25 May 2026 09:26:07 +0400 Subject: [PATCH 151/391] Remove unreachable WebSocket protocol match --- lib/pleroma/web/mastodon_api/websocket_plug.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/pleroma/web/mastodon_api/websocket_plug.ex b/lib/pleroma/web/mastodon_api/websocket_plug.ex index 58ee913b4..c5fb5b748 100644 --- a/lib/pleroma/web/mastodon_api/websocket_plug.ex +++ b/lib/pleroma/web/mastodon_api/websocket_plug.ex @@ -94,7 +94,6 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketPlug do [protocols | _] -> case Plug.Conn.Utils.list(protocols) do [protocol | _] -> put_resp_header(conn, "sec-websocket-protocol", protocol) - nil -> conn [] -> conn end From cdb0f103a8f7f141185e2bff6d686b047f368fa2 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 25 May 2026 14:08:07 +0400 Subject: [PATCH 152/391] Tighten rich media backfill stream test --- test/pleroma/web/rich_media/backfill_test.exs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/pleroma/web/rich_media/backfill_test.exs b/test/pleroma/web/rich_media/backfill_test.exs index a1949533a..071f9b48a 100644 --- a/test/pleroma/web/rich_media/backfill_test.exs +++ b/test/pleroma/web/rich_media/backfill_test.exs @@ -74,9 +74,12 @@ defmodule Pleroma.Web.RichMedia.BackfillTest do |> expect(:put, fn :rich_media_cache, _, _ -> {:ok, true} end) Pleroma.Web.ActivityPub.ActivityPubMock - |> expect(:stream_out, fn _ -> :ok end) + |> expect(:stream_out, fn %Pleroma.Activity{id: id} -> + assert id == activity.id + :ok + end) - Backfill.run(%{"url" => url, "activity_id" => "#{activity.data["id"]}", "stream" => true}) + Backfill.run(%{"url" => url, "activity_id" => activity.id, "stream" => true}) end test "does not stream out update when stream == false" do From 8a410bf49f1818ebf631020c55ba05012efcb8f7 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 25 May 2026 16:04:53 +0400 Subject: [PATCH 153/391] Quote E2E entrypoint variables --- docker-entrypoint-e2e.sh | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docker-entrypoint-e2e.sh b/docker-entrypoint-e2e.sh index c83c44a80..528a09619 100755 --- a/docker-entrypoint-e2e.sh +++ b/docker-entrypoint-e2e.sh @@ -5,12 +5,12 @@ SEED_SENTINEL_PATH=/var/lib/pleroma/.e2e_seeded CONFIG_OVERRIDE_PATH=/var/lib/pleroma/config.exs echo '-- Waiting for database...' -while ! pg_isready -U ${DB_USER:-pleroma} -d postgres://${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-pleroma} -t 1; do +while ! pg_isready -U "${DB_USER:-pleroma}" -d "postgres://${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-pleroma}" -t 1; do sleep 1s done echo '-- Writing E2E config overrides...' -cat > $CONFIG_OVERRIDE_PATH < "$CONFIG_OVERRIDE_PATH" </dev/null; then - kill -TERM $PLEROMA_PID - wait $PLEROMA_PID || true + if kill -0 "$PLEROMA_PID" 2>/dev/null; then + kill -TERM "$PLEROMA_PID" + wait "$PLEROMA_PID" || true fi } @@ -48,23 +48,23 @@ for _i in $(seq 1 120); do sleep 1s done -if [ $api_ok != true ]; then +if [ "$api_ok" != true ]; then echo 'Timed out waiting for Pleroma API to become available' exit 1 fi -if [ ! -f $SEED_SENTINEL_PATH ]; then - if [ -n ${E2E_ADMIN_USERNAME:-} ] && [ -n ${E2E_ADMIN_PASSWORD:-} ] && [ -n ${E2E_ADMIN_EMAIL:-} ]; then - echo '-- Seeding admin user' $E2E_ADMIN_USERNAME '...' - if ! /opt/pleroma/bin/pleroma_ctl user new $E2E_ADMIN_USERNAME $E2E_ADMIN_EMAIL --admin --password $E2E_ADMIN_PASSWORD -y; then +if [ ! -f "$SEED_SENTINEL_PATH" ]; then + if [ -n "${E2E_ADMIN_USERNAME:-}" ] && [ -n "${E2E_ADMIN_PASSWORD:-}" ] && [ -n "${E2E_ADMIN_EMAIL:-}" ]; then + echo '-- Seeding admin user' "$E2E_ADMIN_USERNAME" '...' + if ! /opt/pleroma/bin/pleroma_ctl user new "$E2E_ADMIN_USERNAME" "$E2E_ADMIN_EMAIL" --admin --password "$E2E_ADMIN_PASSWORD" -y; then echo '-- User already exists or creation failed, ensuring admin + confirmed...' - /opt/pleroma/bin/pleroma_ctl user set $E2E_ADMIN_USERNAME --admin --confirmed + /opt/pleroma/bin/pleroma_ctl user set "$E2E_ADMIN_USERNAME" --admin --confirmed fi else echo '-- Skipping admin seeding (missing E2E_ADMIN_* env)' fi - touch $SEED_SENTINEL_PATH + touch "$SEED_SENTINEL_PATH" fi -wait $PLEROMA_PID +wait "$PLEROMA_PID" From 54369c93d78d4431f10a44af9c8f28a416262f8e Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 25 May 2026 16:05:21 +0400 Subject: [PATCH 154/391] Use E2E approval config key --- docker-entrypoint-e2e.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-entrypoint-e2e.sh b/docker-entrypoint-e2e.sh index 528a09619..772ac7cec 100755 --- a/docker-entrypoint-e2e.sh +++ b/docker-entrypoint-e2e.sh @@ -19,7 +19,7 @@ config :pleroma, Pleroma.Captcha, config :pleroma, :instance, registrations_open: true, account_activation_required: false, - approval_required: false + account_approval_required: false EOF echo '-- Running migrations...' From e023ed7a5a450d909565cfd5451b4fb4ab7df4fe Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 25 May 2026 16:06:21 +0400 Subject: [PATCH 155/391] Seed E2E admin before API readiness --- docker-entrypoint-e2e.sh | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docker-entrypoint-e2e.sh b/docker-entrypoint-e2e.sh index 772ac7cec..d7aa332f7 100755 --- a/docker-entrypoint-e2e.sh +++ b/docker-entrypoint-e2e.sh @@ -25,6 +25,20 @@ EOF echo '-- Running migrations...' /opt/pleroma/bin/pleroma_ctl migrate +if [ ! -f "$SEED_SENTINEL_PATH" ]; then + if [ -n "${E2E_ADMIN_USERNAME:-}" ] && [ -n "${E2E_ADMIN_PASSWORD:-}" ] && [ -n "${E2E_ADMIN_EMAIL:-}" ]; then + echo '-- Seeding admin user' "$E2E_ADMIN_USERNAME" '...' + if ! PLEROMA_CTL_RPC_DISABLED=true /opt/pleroma/bin/pleroma_ctl user new "$E2E_ADMIN_USERNAME" "$E2E_ADMIN_EMAIL" --admin --password "$E2E_ADMIN_PASSWORD" -y; then + echo '-- User already exists or creation failed, ensuring admin + confirmed...' + PLEROMA_CTL_RPC_DISABLED=true /opt/pleroma/bin/pleroma_ctl user set "$E2E_ADMIN_USERNAME" --admin --confirmed + fi + else + echo '-- Skipping admin seeding (missing E2E_ADMIN_* env)' + fi + + touch "$SEED_SENTINEL_PATH" +fi + echo '-- Starting!' /opt/pleroma/bin/pleroma start & PLEROMA_PID=$! @@ -53,18 +67,4 @@ if [ "$api_ok" != true ]; then exit 1 fi -if [ ! -f "$SEED_SENTINEL_PATH" ]; then - if [ -n "${E2E_ADMIN_USERNAME:-}" ] && [ -n "${E2E_ADMIN_PASSWORD:-}" ] && [ -n "${E2E_ADMIN_EMAIL:-}" ]; then - echo '-- Seeding admin user' "$E2E_ADMIN_USERNAME" '...' - if ! /opt/pleroma/bin/pleroma_ctl user new "$E2E_ADMIN_USERNAME" "$E2E_ADMIN_EMAIL" --admin --password "$E2E_ADMIN_PASSWORD" -y; then - echo '-- User already exists or creation failed, ensuring admin + confirmed...' - /opt/pleroma/bin/pleroma_ctl user set "$E2E_ADMIN_USERNAME" --admin --confirmed - fi - else - echo '-- Skipping admin seeding (missing E2E_ADMIN_* env)' - fi - - touch "$SEED_SENTINEL_PATH" -fi - wait "$PLEROMA_PID" From a84d16831f788d82ec77eceb51debf359cf397d2 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 25 May 2026 18:09:18 +0400 Subject: [PATCH 156/391] Fix account favourites ordering --- changelog.d/account-favourites-order.fix | 1 + lib/pleroma/web/activity_pub/activity_pub.ex | 17 +++- .../controllers/account_controller_test.exs | 84 +++++++++++++++++-- 3 files changed, 90 insertions(+), 12 deletions(-) create mode 100644 changelog.d/account-favourites-order.fix diff --git a/changelog.d/account-favourites-order.fix b/changelog.d/account-favourites-order.fix new file mode 100644 index 000000000..94e27e499 --- /dev/null +++ b/changelog.d/account-favourites-order.fix @@ -0,0 +1 @@ +Fixed `/api/v1/pleroma/accounts/:id/favourites` ordering and pagination to use the favourite date. diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 1e8bfe3dd..be7b63725 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -537,9 +537,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do # and extra sorting on "activities.id DESC NULLS LAST" would worse the query plan opts = Map.put(opts, :skip_extra_order, true) - Pagination.fetch_paginated(query, opts, pagination) + Pagination.fetch_paginated(query, opts, pagination, pagination_binding(opts)) end + defp pagination_binding(%{favorited_by: _}), do: :favorited_activity + defp pagination_binding(_), do: nil + def fetch_activities(recipients, opts \\ %{}, pagination \\ :keyset) do list_memberships = Pleroma.List.memberships(opts[:user]) @@ -786,6 +789,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp restrict_since(query, %{since_id: ""}), do: query + defp restrict_since(query, %{favorited_by: _, since_id: _}), do: query + defp restrict_since(query, %{since_id: since_id}) do from(activity in query, where: activity.id > ^since_id) end @@ -1013,8 +1018,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp restrict_favorited_by(query, %{favorited_by: ap_id}) do from( - [_activity, object] in query, - where: fragment("(?)->'likes' \\? (?)", object.data, ^ap_id) + [activity, object: object] in query, + join: favorited_activity in Activity, + as: :favorited_activity, + on: + favorited_activity.actor == ^ap_id and + fragment("?->>'type' = ?", favorited_activity.data, "Like") and + fragment("associated_object_id(?) = (?)->>'id'", favorited_activity.data, object.data), + select: %Activity{activity | pagination_id: favorited_activity.id} ) end diff --git a/test/pleroma/web/pleroma_api/controllers/account_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/account_controller_test.exs index 38a928f30..e93b277cc 100644 --- a/test/pleroma/web/pleroma_api/controllers/account_controller_test.exs +++ b/test/pleroma/web/pleroma_api/controllers/account_controller_test.exs @@ -105,6 +105,28 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do assert length(response) == 1 end + test "orders statuses by favorite date", %{ + conn: conn, + user: user + } do + other_user = insert(:user) + + {:ok, first_post} = CommonAPI.post(other_user, %{status: "first post"}) + {:ok, second_post} = CommonAPI.post(other_user, %{status: "second post"}) + {:ok, third_post} = CommonAPI.post(other_user, %{status: "third post"}) + + {:ok, _} = CommonAPI.favorite(third_post.id, user) + {:ok, _} = CommonAPI.favorite(first_post.id, user) + {:ok, _} = CommonAPI.favorite(second_post.id, user) + + response = + conn + |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") + |> json_response_and_validate_schema(:ok) + + assert Enum.map(response, & &1["id"]) == [second_post.id, first_post.id, third_post.id] + end + test "returns favorited DM only when user is logged in and he is one of recipients", %{ current_user: current_user, user: user @@ -164,23 +186,67 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do } do activities = insert_list(10, :note_activity) - Enum.each(activities, fn activity -> - CommonAPI.favorite(activity.id, user) - end) + favorite_pairs = + Enum.map(activities, fn activity -> + {:ok, favorite} = CommonAPI.favorite(activity.id, user) + {favorite, activity} + end) + |> Enum.sort_by(fn {favorite, _activity} -> FlakeId.from_string(favorite.id) end) - third_activity = Enum.at(activities, 2) - seventh_activity = Enum.at(activities, 6) + {third_favorite, _} = Enum.at(favorite_pairs, 2) + {seventh_favorite, _} = Enum.at(favorite_pairs, 6) + + expected_ids = + favorite_pairs + |> Enum.slice(3, 3) + |> Enum.reverse() + |> Enum.map(fn {_favorite, activity} -> activity.id end) response = conn |> get( - "/api/v1/pleroma/accounts/#{user.id}/favourites?since_id=#{third_activity.id}&max_id=#{seventh_activity.id}" + "/api/v1/pleroma/accounts/#{user.id}/favourites?since_id=#{third_favorite.id}&max_id=#{seventh_favorite.id}" ) |> json_response_and_validate_schema(:ok) - assert length(response) == 3 - refute third_activity in response - refute seventh_activity in response + assert Enum.map(response, & &1["id"]) == expected_ids + end + + test "paginates favorites using min_id and limit", %{ + conn: conn, + user: user + } do + activities = insert_list(10, :note_activity) + + favorite_pairs = + Enum.map(activities, fn activity -> + {:ok, favorite} = CommonAPI.favorite(activity.id, user) + {favorite, activity} + end) + |> Enum.sort_by(fn {favorite, _activity} -> FlakeId.from_string(favorite.id) end) + + {third_favorite, _} = Enum.at(favorite_pairs, 2) + {fourth_favorite, _} = Enum.at(favorite_pairs, 3) + {fifth_favorite, _} = Enum.at(favorite_pairs, 4) + + expected_ids = + favorite_pairs + |> Enum.slice(3, 2) + |> Enum.reverse() + |> Enum.map(fn {_favorite, activity} -> activity.id end) + + conn = + get( + conn, + "/api/v1/pleroma/accounts/#{user.id}/favourites?min_id=#{third_favorite.id}&limit=2" + ) + + response = json_response_and_validate_schema(conn, :ok) + + assert Enum.map(response, & &1["id"]) == expected_ids + assert [link_header] = get_resp_header(conn, "link") + assert link_header =~ "max_id=#{fourth_favorite.id}" + assert link_header =~ "min_id=#{fifth_favorite.id}" end test "limits favorites using limit parameter", %{ From cd28c2089c2fddce4e480bd097830ce22f923c47 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 25 May 2026 18:45:47 +0400 Subject: [PATCH 157/391] Fix ParadeDB search indexing --- changelog.d/paradedb-search.add | 2 +- config/config.exs | 2 +- docs/configuration/search.md | 8 +- lib/pleroma/search.ex | 74 +++++++++------ .../search/{paradedb.ex => parade_db.ex} | 15 +-- .../search/{paradedb => parade_db}/client.ex | 0 .../search/{paradedb => parade_db}/repo.ex | 0 .../activity_pub/mrf/anti_followbot_policy.ex | 6 +- .../web/activity_pub/mrf/simple_policy.ex | 2 +- .../web/mastodon_api/views/status_view.ex | 4 +- lib/pleroma/workers/mailer_worker.ex | 4 +- test/pleroma/search/meilisearch_test.exs | 2 +- ...est.exs => parade_db_integration_test.exs} | 0 .../{paradedb_test.exs => parade_db_test.exs} | 20 ++-- test/pleroma/search_test.exs | 94 +++++++++++++++++-- 15 files changed, 175 insertions(+), 58 deletions(-) rename lib/pleroma/search/{paradedb.ex => parade_db.ex} (96%) rename lib/pleroma/search/{paradedb => parade_db}/client.ex (100%) rename lib/pleroma/search/{paradedb => parade_db}/repo.ex (100%) rename test/pleroma/search/{paradedb_integration_test.exs => parade_db_integration_test.exs} (100%) rename test/pleroma/search/{paradedb_test.exs => parade_db_test.exs} (94%) diff --git a/changelog.d/paradedb-search.add b/changelog.d/paradedb-search.add index 00a6e2361..f72ebca14 100644 --- a/changelog.d/paradedb-search.add +++ b/changelog.d/paradedb-search.add @@ -1 +1 @@ -Add ParadeDB (pg_search) as an optional external search backend. +Add ParadeDB (pg_search) as an optional external search backend, indexing status content, content warnings, and attachment descriptions. diff --git a/config/config.exs b/config/config.exs index 2e405413c..3c308f093 100644 --- a/config/config.exs +++ b/config/config.exs @@ -946,7 +946,7 @@ config :pleroma, Pleroma.Search.ParadeDB, fuzzy_distance: 0 config :pleroma, Pleroma.Search.ParadeDB.Repo, - pool_size: 10, + pool_size: 2, prepare: :unnamed config :pleroma, Pleroma.Application, diff --git a/docs/configuration/search.md b/docs/configuration/search.md index c0d3f8462..1d5056ddf 100644 --- a/docs/configuration/search.md +++ b/docs/configuration/search.md @@ -85,7 +85,7 @@ You will see a "Default Admin API Key", this is the key you actually put into yo ### Initial indexing -After setting up the configuration, you'll want to index all of your already existing posts. Only public posts are indexed. You'll only +After setting up the configuration, you'll want to index all of your already existing posts. Only public/unlisted posts are indexed. You'll only have to do it one time, but it might take a while, depending on the amount of posts your instance has seen. This is also a fairly RAM consuming process for `meilisearch`, and it will take a lot of RAM when running if you have a lot of posts (seems to be around 5G for ~1.2 million posts while idle and up to 7G while indexing initially, but your experience may be different). @@ -153,8 +153,10 @@ external search backend by pointing Pleroma at a separate Postgres instance with Pleroma will maintain a small `pleroma_search_documents` table in that database (via the existing search indexing queue) and run search queries against it. +The indexed text includes status content, content warnings, and attachment descriptions. To use it, set the search module to `Pleroma.Search.ParadeDB` and configure the ParadeDB database URL: +These settings must be in static/runtime config (or `PARADEDB_DATABASE_URL`) so the dedicated ParadeDB Repo is started with the application. > config :pleroma, Pleroma.Search, module: Pleroma.Search.ParadeDB > @@ -162,6 +164,10 @@ To use it, set the search module to `Pleroma.Search.ParadeDB` and configure the > url: System.get_env("PARADEDB_DATABASE_URL") || "postgres://postgres:postgres@127.0.0.1:5432/paradedb", > table: "pleroma_search_documents", > fuzzy_distance: 0 +> +> config :pleroma, Pleroma.Search.ParadeDB.Repo, +> pool_size: 2, +> prepare: :unnamed Then, create the table and BM25 index once: diff --git a/lib/pleroma/search.ex b/lib/pleroma/search.ex index 9cd2768c4..cd4907026 100644 --- a/lib/pleroma/search.ex +++ b/lib/pleroma/search.ex @@ -5,13 +5,12 @@ defmodule Pleroma.Search do alias Pleroma.Workers.SearchIndexingWorker @spec add_to_index(Activity.t()) :: {:ok, Oban.Job.t() | :noop} | {:error, Oban.Job.changeset()} - def add_to_index(%Activity{id: activity_id, object: %Object{} = object} = activity) do - with {_, true} <- {:indexable, indexable?(activity)}, - {_, "public"} <- {:visibility, Visibility.get_visibility(object)} do + def add_to_index(%Activity{id: activity_id, object: %Object{}} = activity) do + if indexable?(activity) do SearchIndexingWorker.new(%{"op" => "add_to_index", "activity" => activity_id}) |> Oban.insert() else - _ -> {:ok, :noop} + {:ok, :noop} end end @@ -41,40 +40,59 @@ defmodule Pleroma.Search do def object_to_search_data(%Object{} = object) do data = object.data - content_str = - case data["content"] do - [nil | rest] -> to_string(rest) - str -> str - end - content = - with {:ok, scrubbed} <- - FastSanitize.Sanitizer.scrub(content_str, Pleroma.HTML.Scrubber.SearchIndexing), - trimmed <- String.trim(scrubbed) do - trimmed - end - - # Make sure we have a non-empty string - if content != "" do - {:ok, published, _} = DateTime.from_iso8601(data["published"]) + data + |> search_texts() + |> Enum.map(&sanitize_search_text/1) + |> Enum.reject(&(&1 in ["", "."])) + |> Enum.join(" ") + with true <- content != "", + published when is_binary(published) <- data["published"], + {:ok, published, _} <- DateTime.from_iso8601(published) do %{ id: object.id, content: content, ap: data["id"], published: published |> DateTime.to_unix() } + else + _ -> nil end end - defp indexable?(%Activity{ - data: %{"type" => "Create"}, - object: %Object{ - data: %{"content" => content, "published" => published, "type" => "Note"} - } - }) - when not is_nil(content) and content not in ["", "."] and not is_nil(published), - do: true + def indexable?(%Activity{ + data: %{"type" => "Create"}, + object: %Object{data: %{"published" => published, "type" => "Note"}} = object + }) + when not is_nil(published) do + Visibility.get_visibility(object) in ["public", "unlisted"] and + not is_nil(object_to_search_data(object)) + end - defp indexable?(_), do: false + def indexable?(_), do: false + + defp search_texts(data) do + [data["content"], data["summary"] | attachment_names(data["attachment"])] + end + + defp attachment_names(attachments) when is_list(attachments) do + Enum.map(attachments, fn + %{"name" => name} -> name + _ -> nil + end) + end + + defp attachment_names(_), do: [] + + defp sanitize_search_text([nil | rest]), do: sanitize_search_text(to_string(rest)) + + defp sanitize_search_text(text) when is_binary(text) do + case FastSanitize.Sanitizer.scrub(text, Pleroma.HTML.Scrubber.SearchIndexing) do + {:ok, scrubbed} -> String.trim(scrubbed) + _ -> "" + end + end + + defp sanitize_search_text(_), do: "" end diff --git a/lib/pleroma/search/paradedb.ex b/lib/pleroma/search/parade_db.ex similarity index 96% rename from lib/pleroma/search/paradedb.ex rename to lib/pleroma/search/parade_db.ex index 20be5b6f9..ee00e74af 100644 --- a/lib/pleroma/search/paradedb.ex +++ b/lib/pleroma/search/parade_db.ex @@ -66,9 +66,10 @@ defmodule Pleroma.Search.ParadeDB do @impl true def drop_index do - with {:ok, _} <- client_impl().query("DROP TABLE IF EXISTS #{table()}", []) do - :ok - else + case client_impl().query("DROP TABLE IF EXISTS #{table()}", []) do + {:ok, _} -> + :ok + {:error, error} -> Logger.error("ParadeDB drop_index failed: #{inspect(error)}") {:error, error} @@ -80,9 +81,9 @@ defmodule Pleroma.Search.ParadeDB do @impl true def add_to_index(activity) do - maybe_search_data = Search.object_to_search_data(activity.object) - - if activity.data["type"] == "Create" and maybe_search_data do + with true <- Search.indexable?(activity), + maybe_search_data when not is_nil(maybe_search_data) <- + Search.object_to_search_data(activity.object) do dumped_activity_id = dump_activity_id(activity.id) actor_ap_id = activity.data["actor"] published_at = DateTime.from_unix!(maybe_search_data.published) @@ -122,7 +123,7 @@ defmodule Pleroma.Search.ParadeDB do end end else - :ok + _ -> :ok end end diff --git a/lib/pleroma/search/paradedb/client.ex b/lib/pleroma/search/parade_db/client.ex similarity index 100% rename from lib/pleroma/search/paradedb/client.ex rename to lib/pleroma/search/parade_db/client.ex diff --git a/lib/pleroma/search/paradedb/repo.ex b/lib/pleroma/search/parade_db/repo.ex similarity index 100% rename from lib/pleroma/search/paradedb/repo.ex rename to lib/pleroma/search/parade_db/repo.ex diff --git a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex index 8ea61aec2..d0320ce49 100644 --- a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex @@ -24,7 +24,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do defp score_displayname("fedibot"), do: 1.0 defp score_displayname(_), do: 0.0 - defp determine_if_followbot(%User{nickname: nickname, name: displayname, actor_type: actor_type}) do + defp determine_if_followbot(%User{ + nickname: nickname, + name: displayname, + actor_type: actor_type + }) do # nickname will be a binary string except when following a relay nick_score = if is_binary(nickname) do diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex index a97e8db7b..93f0616ec 100644 --- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex @@ -108,7 +108,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do end defp intersection(list1, list2) do - list1 -- list1 -- list2 + list1 -- (list1 -- list2) end defp check_followers_only(%{host: actor_host} = _actor_info, activity) do diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 6949c5f2d..5b18261a7 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -680,7 +680,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do %{ancestors: ancestors, descendants: descendants} = activities |> Enum.reverse() - |> Enum.group_by(fn %{id: id} -> if id < activity.id, do: :ancestors, else: :descendants end) + |> Enum.group_by(fn %{id: id} -> + if id < activity.id, do: :ancestors, else: :descendants + end) |> Map.put_new(:ancestors, []) |> Map.put_new(:descendants, []) diff --git a/lib/pleroma/workers/mailer_worker.ex b/lib/pleroma/workers/mailer_worker.ex index b0259b191..9ef6a80c9 100644 --- a/lib/pleroma/workers/mailer_worker.ex +++ b/lib/pleroma/workers/mailer_worker.ex @@ -6,7 +6,9 @@ defmodule Pleroma.Workers.MailerWorker do use Oban.Worker, queue: :background @impl true - def perform(%Job{args: %{"op" => "email", "encoded_email" => encoded_email, "config" => config}}) do + def perform(%Job{ + args: %{"op" => "email", "encoded_email" => encoded_email, "config" => config} + }) do encoded_email |> Base.decode64!() |> :erlang.binary_to_term() diff --git a/test/pleroma/search/meilisearch_test.exs b/test/pleroma/search/meilisearch_test.exs index ff32491c5..6e4a42c95 100644 --- a/test/pleroma/search/meilisearch_test.exs +++ b/test/pleroma/search/meilisearch_test.exs @@ -99,7 +99,7 @@ defmodule Pleroma.Search.MeilisearchTest do %{method: :delete, url: "http://127.0.0.1:7700/indexes/objects/documents/" <> id} -> send(self(), "called_delete") - assert String.length(id) > 1 + assert id != "" json(%{}) end) diff --git a/test/pleroma/search/paradedb_integration_test.exs b/test/pleroma/search/parade_db_integration_test.exs similarity index 100% rename from test/pleroma/search/paradedb_integration_test.exs rename to test/pleroma/search/parade_db_integration_test.exs diff --git a/test/pleroma/search/paradedb_test.exs b/test/pleroma/search/parade_db_test.exs similarity index 94% rename from test/pleroma/search/paradedb_test.exs rename to test/pleroma/search/parade_db_test.exs index 3a3b8298f..3133b6b38 100644 --- a/test/pleroma/search/paradedb_test.exs +++ b/test/pleroma/search/parade_db_test.exs @@ -75,15 +75,21 @@ defmodule Pleroma.Search.ParadeDBTest do }) args = %{"op" => "add_to_index", "activity" => activity.id} - assert_enqueued(worker: SearchIndexingWorker, args: args) + refute_enqueued(worker: SearchIndexingWorker, args: args) + end) + end - Config - |> expect(:get, fn - [Pleroma.Search, :module], nil -> - ParadeDB - end) + test "backend skips non-public posts when called directly" do + user = insert(:user) - assert :ok = perform_job(SearchIndexingWorker, args) + Enum.each(["private", "direct"], fn visibility -> + {:ok, activity} = + CommonAPI.post(user, %{ + status: "guys i just don't wanna leave the swamp", + visibility: visibility + }) + + assert :ok = ParadeDB.add_to_index(activity) end) end diff --git a/test/pleroma/search_test.exs b/test/pleroma/search_test.exs index d777bcda2..00d8b10ed 100644 --- a/test/pleroma/search_test.exs +++ b/test/pleroma/search_test.exs @@ -3,26 +3,32 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.SearchTest do + require Pleroma.Constants + use Pleroma.DataCase, async: true use Oban.Testing, repo: Pleroma.Repo import Pleroma.Factory + alias Pleroma.Object + alias Pleroma.Search alias Pleroma.Web.CommonAPI alias Pleroma.Workers.SearchIndexingWorker - test "indexes posts that are public" do + test "indexes posts that are public or unlisted" do user = insert(:user) - {:ok, activity} = - CommonAPI.post(user, %{ - status: "Well this is a story all about how my life got flipped turned upside down", - visibility: "public" - }) + Enum.each(["public", "unlisted"], fn visibility -> + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Well this is a story all about how my life got flipped turned upside down", + visibility: visibility + }) - args = %{"op" => "add_to_index", "activity" => activity.id} + args = %{"op" => "add_to_index", "activity" => activity.id} - assert_enqueued(worker: SearchIndexingWorker, args: args) + assert_enqueued(worker: SearchIndexingWorker, args: args) + end) end test "doesn't index posts that are not public" do @@ -66,4 +72,76 @@ defmodule Pleroma.SearchTest do refute_enqueued(worker: SearchIndexingWorker, args: args) end + + test "object_to_search_data includes content warnings and attachment descriptions" do + object = %Object{ + id: 1, + data: %{ + "id" => "https://example.com/objects/1", + "type" => "Note", + "content" => "body text", + "summary" => "subject text", + "attachment" => [ + %{"type" => "Document", "name" => "image description"}, + %{"type" => "Document", "name" => ""} + ], + "published" => DateTime.utc_now() |> DateTime.to_iso8601() + } + } + + assert %{content: content} = Search.object_to_search_data(object) + assert content =~ "body text" + assert content =~ "subject text" + assert content =~ "image description" + end + + test "object_to_search_data ignores objects with invalid published dates" do + for published <- [nil, 123, "not a date"] do + object = %Object{ + id: 1, + data: %{ + "id" => "https://example.com/objects/1", + "type" => "Note", + "content" => "body text", + "published" => published + } + } + + refute Search.object_to_search_data(object) + end + end + + test "indexable? accepts searchable content warnings and attachment descriptions" do + public = Pleroma.Constants.as_public() + published = DateTime.utc_now() |> DateTime.to_iso8601() + + for data <- [ + %{"content" => ".", "summary" => "subject text", "attachment" => []}, + %{ + "content" => ".", + "summary" => "", + "attachment" => [%{"type" => "Document", "name" => "image description"}] + } + ] do + object = %Object{ + id: 1, + data: + Map.merge(data, %{ + "id" => "https://example.com/objects/1", + "type" => "Note", + "to" => [public], + "published" => published + }) + } + + activity = %Pleroma.Activity{ + id: Ecto.UUID.generate(), + data: %{"type" => "Create"}, + object: object, + recipients: [public] + } + + assert Search.indexable?(activity) + end + end end From afe33bcc44b2dede05b58135c302960e813c6560 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 25 May 2026 18:37:37 +0400 Subject: [PATCH 158/391] Fix webhook create test expectations --- .../controllers/webhook_controller_test.exs | 6 +++--- test/pleroma/webhook/notify_test.exs | 4 ++-- test/pleroma/webhook_test.exs | 19 ++++++++++--------- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/test/pleroma/web/admin_api/controllers/webhook_controller_test.exs b/test/pleroma/web/admin_api/controllers/webhook_controller_test.exs index 0cd00e753..0e3b5311e 100644 --- a/test/pleroma/web/admin_api/controllers/webhook_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/webhook_controller_test.exs @@ -52,7 +52,7 @@ defmodule Pleroma.Web.AdminAPI.WebhookControllerTest do describe "PATCH /api/pleroma/admin/webhooks" do test "edits a webhook", %{conn: conn} do - %{id: id} = + {:ok, %{id: id}} = Webhook.create(%{url: "https://example.com/webhook1", events: [:"report.created"]}) conn @@ -66,7 +66,7 @@ defmodule Pleroma.Web.AdminAPI.WebhookControllerTest do end test "can't edit an internal webhook", %{conn: conn} do - %{id: id} = + {:ok, %{id: id}} = Webhook.create(%{url: "https://example.com/webhook1", events: [], internal: true}) conn @@ -82,7 +82,7 @@ defmodule Pleroma.Web.AdminAPI.WebhookControllerTest do describe "DELETE /api/pleroma/admin/webhooks" do test "deletes a webhook", %{conn: conn} do - %{id: id} = + {:ok, %{id: id}} = Webhook.create(%{url: "https://example.com/webhook1", events: [:"report.created"]}) conn diff --git a/test/pleroma/webhook/notify_test.exs b/test/pleroma/webhook/notify_test.exs index c99489940..568ed0d14 100644 --- a/test/pleroma/webhook/notify_test.exs +++ b/test/pleroma/webhook/notify_test.exs @@ -59,8 +59,8 @@ defmodule Pleroma.Webhook.NotifyTest do test "notifies have a valid signature" do activity = insert(:report_activity) - %{secret: secret} = - webhook = Webhook.create(%{url: "https://example.com/webhook", events: [:"report.created"]}) + {:ok, %{secret: secret} = webhook} = + Webhook.create(%{url: "https://example.com/webhook", events: [:"report.created"]}) Tesla.Mock.mock(fn %{url: "https://example.com/webhook", body: body, headers: headers} = _ -> {"X-Hub-Signature", "sha256=" <> signature} = diff --git a/test/pleroma/webhook_test.exs b/test/pleroma/webhook_test.exs index ea1378730..ea1408708 100644 --- a/test/pleroma/webhook_test.exs +++ b/test/pleroma/webhook_test.exs @@ -8,14 +8,15 @@ defmodule Pleroma.WebhookTest do alias Pleroma.Webhook test "creating a webhook" do - %{id: id} = Webhook.create(%{url: "https://example.com/webhook", events: [:"report.created"]}) + {:ok, %{id: id}} = + Webhook.create(%{url: "https://example.com/webhook", events: [:"report.created"]}) assert %{url: "https://example.com/webhook"} = Webhook.get(id) end test "editing a webhook" do - %{id: id} = - webhook = Webhook.create(%{url: "https://example.com/webhook", events: [:"report.created"]}) + {:ok, %{id: id} = webhook} = + Webhook.create(%{url: "https://example.com/webhook", events: [:"report.created"]}) Webhook.update(webhook, %{events: [:"account.created"]}) @@ -23,10 +24,10 @@ defmodule Pleroma.WebhookTest do end test "filter webhooks by type" do - %{id: id1} = + {:ok, %{id: id1}} = Webhook.create(%{url: "https://example.com/webhook1", events: [:"report.created"]}) - %{id: id2} = + {:ok, %{id: id2}} = Webhook.create(%{ url: "https://example.com/webhook2", events: [:"account.created", :"report.created"] @@ -38,16 +39,16 @@ defmodule Pleroma.WebhookTest do end test "change webhook state" do - %{id: id, enabled: true} = - webhook = Webhook.create(%{url: "https://example.com/webhook", events: [:"report.created"]}) + {:ok, %{id: id, enabled: true} = webhook} = + Webhook.create(%{url: "https://example.com/webhook", events: [:"report.created"]}) Webhook.set_enabled(webhook, false) assert %{enabled: false} = Webhook.get(id) end test "rotate webhook secrets" do - %{id: id, secret: secret} = - webhook = Webhook.create(%{url: "https://example.com/webhook", events: [:"report.created"]}) + {:ok, %{id: id, secret: secret} = webhook} = + Webhook.create(%{url: "https://example.com/webhook", events: [:"report.created"]}) Webhook.rotate_secret(webhook) %{secret: new_secret} = Webhook.get(id) From e44d2329c8d7a9815cb74890b6c7299fc0b424b9 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Tue, 26 May 2026 14:29:08 +0400 Subject: [PATCH 159/391] Reject invalid webhook events --- lib/pleroma/webhook.ex | 12 ------------ test/pleroma/webhook_test.exs | 10 ++++++++++ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/pleroma/webhook.ex b/lib/pleroma/webhook.ex index 83ce344fa..9a1806f71 100644 --- a/lib/pleroma/webhook.ex +++ b/lib/pleroma/webhook.ex @@ -37,7 +37,6 @@ defmodule Pleroma.Webhook do |> cast(params, [:url, :events, :enabled, :internal]) |> validate_required([:url, :events]) |> unique_constraint(:url) - |> strip_events() |> put_secret() end @@ -45,7 +44,6 @@ defmodule Pleroma.Webhook do webhook |> cast(params, [:url, :events, :enabled, :internal]) |> unique_constraint(:url) - |> strip_events() end def create(params) do @@ -75,16 +73,6 @@ defmodule Pleroma.Webhook do |> Repo.update() end - defp strip_events(changeset) do - case get_change(changeset, :events) do - nil -> - changeset - - events -> - put_change(changeset, :events, Enum.filter(events, &(&1 in @event_types))) - end - end - defp put_secret(changeset) do changeset |> put_change(:secret, generate_secret()) diff --git a/test/pleroma/webhook_test.exs b/test/pleroma/webhook_test.exs index ea1408708..1f64d350f 100644 --- a/test/pleroma/webhook_test.exs +++ b/test/pleroma/webhook_test.exs @@ -23,6 +23,16 @@ defmodule Pleroma.WebhookTest do assert %{events: [:"account.created"]} = Webhook.get(id) end + test "rejects invalid events" do + assert {:error, _changeset} = + Webhook.create(%{url: "https://example.com/webhook", events: [:"status.created"]}) + + {:ok, webhook} = + Webhook.create(%{url: "https://example.com/valid-webhook", events: [:"report.created"]}) + + assert {:error, _changeset} = Webhook.update(webhook, %{events: [:"status.created"]}) + end + test "filter webhooks by type" do {:ok, %{id: id1}} = Webhook.create(%{url: "https://example.com/webhook1", events: [:"report.created"]}) From 719b5d65b545403adf5fcfbec5571936b5165be1 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Tue, 26 May 2026 14:30:50 +0400 Subject: [PATCH 160/391] Require webhook events --- docs/development/API/admin_api.md | 2 +- .../operations/admin/webhook_operation.ex | 10 +++++----- lib/pleroma/webhook.ex | 11 +++++++++++ test/pleroma/webhook_test.exs | 18 ++++++++++++++++++ 4 files changed, 35 insertions(+), 6 deletions(-) diff --git a/docs/development/API/admin_api.md b/docs/development/API/admin_api.md index a30db9bad..ec58e8c19 100644 --- a/docs/development/API/admin_api.md +++ b/docs/development/API/admin_api.md @@ -1877,7 +1877,7 @@ Note that this differs from the Mastodon API variant: Mastodon API only returns - Method: `POST` - Params: - `url`: **string** Webhook URL - - *optional* `events`: **[string]** Types of events to trigger on (`account.created`, `report.created`) + - `events`: **[string]** Types of events to trigger on (`account.created`, `report.created`) - *optional* `enabled`: **boolean** Whether webhook is enabled - Response: A webhook diff --git a/lib/pleroma/web/api_spec/operations/admin/webhook_operation.ex b/lib/pleroma/web/api_spec/operations/admin/webhook_operation.ex index fd5f3a561..e2869ac44 100644 --- a/lib/pleroma/web/api_spec/operations/admin/webhook_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/webhook_operation.ex @@ -55,9 +55,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.WebhookOperation do %Schema{ description: "POST body for creating a webhook", type: :object, + required: [:url, :events], properties: %{ - url: %Schema{type: :string, format: :uri, required: true}, - events: event_type(true), + url: %Schema{type: :string, format: :uri}, + events: event_type(), enabled: %Schema{type: :boolean} } } @@ -179,15 +180,14 @@ defmodule Pleroma.Web.ApiSpec.Admin.WebhookOperation do } end - defp event_type(required \\ nil) do + defp event_type do %Schema{ type: :array, items: %Schema{ title: "Event", description: "Event type", type: :string, - enum: ["account.created", "report.created"], - required: required + enum: ["account.created", "report.created"] } } end diff --git a/lib/pleroma/webhook.ex b/lib/pleroma/webhook.ex index 9a1806f71..f4d65b1a6 100644 --- a/lib/pleroma/webhook.ex +++ b/lib/pleroma/webhook.ex @@ -36,6 +36,7 @@ defmodule Pleroma.Webhook do webhook |> cast(params, [:url, :events, :enabled, :internal]) |> validate_required([:url, :events]) + |> validate_events_present() |> unique_constraint(:url) |> put_secret() end @@ -43,6 +44,7 @@ defmodule Pleroma.Webhook do def update_changeset(%__MODULE__{} = webhook, params \\ %{}) do webhook |> cast(params, [:url, :events, :enabled, :internal]) + |> validate_events_present() |> unique_constraint(:url) end @@ -73,6 +75,15 @@ defmodule Pleroma.Webhook do |> Repo.update() end + defp validate_events_present(changeset) do + cond do + Keyword.has_key?(changeset.errors, :events) -> changeset + get_field(changeset, :internal) -> changeset + match?([_ | _], get_field(changeset, :events)) -> changeset + true -> add_error(changeset, :events, "can't be blank") + end + end + defp put_secret(changeset) do changeset |> put_change(:secret, generate_secret()) diff --git a/test/pleroma/webhook_test.exs b/test/pleroma/webhook_test.exs index 1f64d350f..df84abef6 100644 --- a/test/pleroma/webhook_test.exs +++ b/test/pleroma/webhook_test.exs @@ -33,6 +33,24 @@ defmodule Pleroma.WebhookTest do assert {:error, _changeset} = Webhook.update(webhook, %{events: [:"status.created"]}) end + test "rejects empty events for non-internal webhooks" do + assert {:error, changeset} = Webhook.create(%{url: "https://example.com/webhook", events: []}) + assert %{events: ["can't be blank"]} = errors_on(changeset) + + {:ok, webhook} = + Webhook.create(%{url: "https://example.com/valid-webhook", events: [:"report.created"]}) + + assert {:error, changeset} = Webhook.update(webhook, %{events: []}) + assert %{events: ["can't be blank"]} = errors_on(changeset) + + assert {:ok, _webhook} = + Webhook.create(%{ + url: "https://example.com/internal-webhook", + events: [], + internal: true + }) + end + test "filter webhooks by type" do {:ok, %{id: id1}} = Webhook.create(%{url: "https://example.com/webhook1", events: [:"report.created"]}) From 9061851fed33fde331a7dcda709406f48e79328c Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Tue, 26 May 2026 14:31:12 +0400 Subject: [PATCH 161/391] Fix webhook read API scope --- .../web/api_spec/operations/admin/webhook_operation.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/admin/webhook_operation.ex b/lib/pleroma/web/api_spec/operations/admin/webhook_operation.ex index e2869ac44..5bce837f7 100644 --- a/lib/pleroma/web/api_spec/operations/admin/webhook_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/webhook_operation.ex @@ -19,7 +19,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.WebhookOperation do tags: ["Webhooks"], summary: "Retrieve a list of webhooks", operationId: "AdminAPI.WebhookController.index", - security: [%{"oAuth" => ["admin:show"]}], + security: [%{"oAuth" => ["admin:read"]}], responses: %{ 200 => Operation.response("Array of webhooks", "application/json", %Schema{ @@ -35,7 +35,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.WebhookOperation do tags: ["Webhooks"], summary: "Retrieve a webhook", operationId: "AdminAPI.WebhookController.show", - security: [%{"oAuth" => ["admin:show"]}], + security: [%{"oAuth" => ["admin:read"]}], parameters: [id_param()], responses: %{ 200 => Operation.response("Webhook", "application/json", webhook()) From 27090ad7e6042beeedb92ab151bbd2f418454198 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Tue, 26 May 2026 14:32:51 +0400 Subject: [PATCH 162/391] Require admin access for webhooks --- lib/pleroma/web/router.ex | 18 +++++++++--------- .../controllers/webhook_controller_test.exs | 15 ++++++++++++++- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 4b1c4140b..1800c5ede 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -316,6 +316,15 @@ defmodule Pleroma.Web.Router do post("/rules", RuleController, :create) patch("/rules/:id", RuleController, :update) delete("/rules/:id", RuleController, :delete) + + get("/webhooks", WebhookController, :index) + get("/webhooks/:id", WebhookController, :show) + post("/webhooks", WebhookController, :create) + patch("/webhooks/:id", WebhookController, :update) + delete("/webhooks/:id", WebhookController, :delete) + post("/webhooks/:id/enable", WebhookController, :enable) + post("/webhooks/:id/disable", WebhookController, :disable) + post("/webhooks/:id/rotate_secret", WebhookController, :rotate_secret) end # AdminAPI: admins and mods (staff) can perform these actions (if privileged by role) @@ -327,15 +336,6 @@ defmodule Pleroma.Web.Router do get("/announcements/:id", AnnouncementController, :show) patch("/announcements/:id", AnnouncementController, :change) delete("/announcements/:id", AnnouncementController, :delete) - - get("/webhooks", WebhookController, :index) - get("/webhooks/:id", WebhookController, :show) - post("/webhooks", WebhookController, :create) - patch("/webhooks/:id", WebhookController, :update) - delete("/webhooks/:id", WebhookController, :delete) - post("/webhooks/:id/enable", WebhookController, :enable) - post("/webhooks/:id/disable", WebhookController, :disable) - post("/webhooks/:id/rotate_secret", WebhookController, :rotate_secret) end # AdminAPI: admins and mods (staff) can perform these actions (if privileged by role) diff --git a/test/pleroma/web/admin_api/controllers/webhook_controller_test.exs b/test/pleroma/web/admin_api/controllers/webhook_controller_test.exs index 0e3b5311e..78edbf05a 100644 --- a/test/pleroma/web/admin_api/controllers/webhook_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/webhook_controller_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.AdminAPI.WebhookControllerTest do - use Pleroma.Web.ConnCase, async: true + use Pleroma.Web.ConnCase, async: false import Pleroma.Factory @@ -33,6 +33,19 @@ defmodule Pleroma.Web.AdminAPI.WebhookControllerTest do assert length(response) == 2 end + + test "requires an admin even if a moderator can manage announcements" do + clear_config([:instance, :moderator_privileges], [:announcements_manage_announcements]) + + moderator = insert(:user, is_moderator: true) + token = insert(:oauth_admin_token, user: moderator) + + build_conn() + |> assign(:user, moderator) + |> assign(:token, token) + |> get("/api/pleroma/admin/webhooks") + |> json_response(:forbidden) + end end describe "POST /api/pleroma/admin/webhooks" do From ab634a4aa35905948c99ebbca73bb96d009842d4 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Tue, 26 May 2026 15:03:22 +0400 Subject: [PATCH 163/391] Harden webhook API input --- .../controllers/webhook_controller.ex | 8 +++- .../operations/admin/webhook_operation.ex | 19 +++++--- .../controllers/webhook_controller_test.exs | 45 +++++++++++++++++++ 3 files changed, 64 insertions(+), 8 deletions(-) diff --git a/lib/pleroma/web/admin_api/controllers/webhook_controller.ex b/lib/pleroma/web/admin_api/controllers/webhook_controller.ex index cc606916e..4d064e0b5 100644 --- a/lib/pleroma/web/admin_api/controllers/webhook_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/webhook_controller.ex @@ -40,14 +40,14 @@ defmodule Pleroma.Web.AdminAPI.WebhookController do end def create(%{body_params: params} = conn, _) do - with {:ok, webhook} <- Webhook.create(params) do + with {:ok, webhook} <- Webhook.create(webhook_params(params)) do render(conn, "show.json", webhook: webhook) end end def update(%{body_params: params} = conn, %{id: id}) do with %Webhook{internal: false} = webhook <- Webhook.get(id), - {:ok, webhook} <- Webhook.update(webhook, params) do + {:ok, webhook} <- Webhook.update(webhook, webhook_params(params)) do render(conn, "show.json", webhook: webhook) else %Webhook{internal: true} -> {:error, :forbidden} @@ -94,4 +94,8 @@ defmodule Pleroma.Web.AdminAPI.WebhookController do nil -> {:error, :not_found} end end + + defp webhook_params(params) do + Map.drop(params, [:internal, "internal"]) + end end diff --git a/lib/pleroma/web/api_spec/operations/admin/webhook_operation.ex b/lib/pleroma/web/api_spec/operations/admin/webhook_operation.ex index 5bce837f7..2b538ca8b 100644 --- a/lib/pleroma/web/api_spec/operations/admin/webhook_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/webhook_operation.ex @@ -58,9 +58,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.WebhookOperation do required: [:url, :events], properties: %{ url: %Schema{type: :string, format: :uri}, - events: event_type(), + events: event_type(min_items: 1), enabled: %Schema{type: :boolean} - } + }, + additionalProperties: false } ), responses: %{ @@ -84,9 +85,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.WebhookOperation do type: :object, properties: %{ url: %Schema{type: :string, format: :uri}, - events: event_type(), + events: event_type(min_items: 1), enabled: %Schema{type: :boolean} - } + }, + additionalProperties: false } ), responses: %{ @@ -180,8 +182,8 @@ defmodule Pleroma.Web.ApiSpec.Admin.WebhookOperation do } end - defp event_type do - %Schema{ + defp event_type(opts \\ []) do + schema = %Schema{ type: :array, items: %Schema{ title: "Event", @@ -190,6 +192,11 @@ defmodule Pleroma.Web.ApiSpec.Admin.WebhookOperation do enum: ["account.created", "report.created"] } } + + case Keyword.fetch(opts, :min_items) do + {:ok, min_items} -> %{schema | minItems: min_items} + :error -> schema + end end defp id_param do diff --git a/test/pleroma/web/admin_api/controllers/webhook_controller_test.exs b/test/pleroma/web/admin_api/controllers/webhook_controller_test.exs index 78edbf05a..800d6367e 100644 --- a/test/pleroma/web/admin_api/controllers/webhook_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/webhook_controller_test.exs @@ -61,6 +61,27 @@ defmodule Pleroma.Web.AdminAPI.WebhookControllerTest do assert %{url: "http://example.com/webhook", events: [:"account.created"]} = Webhook.get(id) end + + test "rejects empty events", %{conn: conn} do + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/webhooks", %{ + url: "http://example.com/webhook", + events: [] + }) + |> json_response(:bad_request) + end + + test "rejects internal flag", %{conn: conn} do + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/webhooks", %{ + url: "http://example.com/webhook", + events: ["account.created"], + internal: true + }) + |> json_response(:bad_request) + end end describe "PATCH /api/pleroma/admin/webhooks" do @@ -78,6 +99,30 @@ defmodule Pleroma.Web.AdminAPI.WebhookControllerTest do assert %{events: [:"report.created", :"account.created"]} = Webhook.get(id) end + test "rejects empty events", %{conn: conn} do + {:ok, %{id: id}} = + Webhook.create(%{url: "https://example.com/webhook1", events: [:"report.created"]}) + + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/pleroma/admin/webhooks/#{id}", %{events: []}) + |> json_response(:bad_request) + + assert %{events: [:"report.created"]} = Webhook.get(id) + end + + test "rejects internal flag", %{conn: conn} do + {:ok, %{id: id}} = + Webhook.create(%{url: "https://example.com/webhook1", events: [:"report.created"]}) + + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/pleroma/admin/webhooks/#{id}", %{internal: true}) + |> json_response(:bad_request) + + assert %{internal: false} = Webhook.get(id) + end + test "can't edit an internal webhook", %{conn: conn} do {:ok, %{id: id}} = Webhook.create(%{url: "https://example.com/webhook1", events: [], internal: true}) From fc9e6793ce3aaf1c9f4b305d2b9692ed59d3c058 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Fri, 29 May 2026 12:30:33 +0400 Subject: [PATCH 164/391] Address grouped notification review cleanup --- lib/pleroma/web/mastodon_api/mastodon_api.ex | 23 +++----- .../notification_controller_test.exs | 59 +++++++++++-------- 2 files changed, 43 insertions(+), 39 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex index cb11a3f4a..d6f2dd3a3 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex @@ -73,7 +73,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do def get_grouped_notification_page(user, params \\ %{}) do grouped_types = params - |> Map.get("grouped_types", Map.get(params, :grouped_types)) + |> Map.get("grouped_types") |> Notification.normalize_grouped_types() query = notifications_query(user, params) @@ -98,13 +98,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do } end - def get_grouped_notification_groups(user, params \\ %{}) do - {groups, _notifications, _notification_group_counts, _notification_group_bounds} = - get_grouped_notification_page(user, params) - - groups - end - def get_notification_group_result(user, group_key, params \\ %{}) do notifications = notification_group_sample(user, params, group_key) metadata = notification_group_metadata(user, group_key, params) @@ -163,21 +156,21 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do defp group_pagination(params) do cond do - min_id = Map.get(params, "min_id", Map.get(params, :min_id)) -> + min_id = Map.get(params, "min_id") -> cursor_filters = [{:gt, min_id}] cursor_filters = - case Map.get(params, "max_id", Map.get(params, :max_id)) do + case Map.get(params, "max_id") do nil -> cursor_filters max_id -> [{:lt, max_id} | cursor_filters] end {:asc, cursor_filters} - since_id = Map.get(params, "since_id", Map.get(params, :since_id)) -> + since_id = Map.get(params, "since_id") -> {:desc, [{:gt, since_id}]} - max_id = Map.get(params, "max_id", Map.get(params, :max_id)) -> + max_id = Map.get(params, "max_id") -> {:desc, [{:lt, max_id}]} true -> @@ -187,7 +180,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do defp grouped_limit(params) do params - |> Map.get("limit", Map.get(params, :limit, 40)) + |> Map.get("limit", 40) |> parse_limit(40) |> min(80) end @@ -195,7 +188,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do def unread_notification_group_count(user, params \\ %{}) do grouped_types = params - |> Map.get("grouped_types", Map.get(params, :grouped_types)) + |> Map.get("grouped_types") |> Notification.normalize_grouped_types() limit = unread_count_limit(params) @@ -406,7 +399,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do defp unread_count_limit(params) do params - |> Map.get("limit", Map.get(params, :limit, 100)) + |> Map.get("limit", 100) |> parse_limit(100) |> min(1000) end diff --git a/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs index e9c521d6b..55df41ec6 100644 --- a/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs @@ -837,40 +837,28 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do refute link_header =~ "since_id=" end - test "lists accounts from and dismisses a notification group" do - %{user: user, conn: conn} = oauth_access(["read:notifications", "write:notifications"]) - other_users = insert_list(9, :user) + test "lists accounts from a notification group" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) - {:ok, status} = CommonAPI.post(user, %{status: "hello"}) - - Enum.each(other_users, fn other_user -> - {:ok, _} = CommonAPI.favorite(status.id, other_user) - end) - - %{ - "notification_groups" => [ - %{ - "group_key" => group_key, - "notifications_count" => 9, - "sample_account_ids" => sample_account_ids - } - ] - } = - conn - |> get("/api/v2/notifications") - |> json_response_and_validate_schema(200) + {other_users, %{"group_key" => group_key, "sample_account_ids" => sample_account_ids}} = + create_favourite_notification_group(user, conn) assert length(sample_account_ids) == 8 - %{conn: read_conn} = oauth_access(["read:notifications"], user: user) - account_ids = - read_conn + conn |> get("/api/v2/notifications/#{group_key}/accounts") |> json_response_and_validate_schema(200) |> Enum.map(& &1["id"]) assert Enum.sort(account_ids) == Enum.sort(Enum.map(other_users, & &1.id)) + end + + test "dismisses a notification group" do + %{user: user, conn: conn} = oauth_access(["read:notifications", "write:notifications"]) + + {_other_users, %{"group_key" => group_key}} = + create_favourite_notification_group(user, conn) assert %{} = conn @@ -1012,6 +1000,29 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do |> to_string() end + defp create_favourite_notification_group(user, conn) do + other_users = insert_list(9, :user) + + {:ok, status} = CommonAPI.post(user, %{status: "hello"}) + + Enum.each(other_users, fn other_user -> + {:ok, _} = CommonAPI.favorite(status.id, other_user) + end) + + %{ + "notification_groups" => [ + %{ + "notifications_count" => 9 + } = group + ] + } = + conn + |> get("/api/v2/notifications") + |> json_response_and_validate_schema(200) + + {other_users, group} + end + defp params_to_query(%{} = params) do Enum.map_join(params, "&", fn {k, v} when is_list(v) -> Enum.map_join(v, "&", &"#{k}[]=#{&1}") From e676ec350713d766b653f60f906fceec34c2c6a1 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Fri, 29 May 2026 18:35:15 +0400 Subject: [PATCH 165/391] Reject unsupported HTTP Message Signatures --- .../activitypub-bot-rfc9421-fallback.fix | 1 + .../activity_pub/activity_pub_controller.ex | 33 ++++++-- .../activity_pub_controller_test.exs | 77 +++++++++++++++++++ 3 files changed, 103 insertions(+), 8 deletions(-) create mode 100644 changelog.d/activitypub-bot-rfc9421-fallback.fix diff --git a/changelog.d/activitypub-bot-rfc9421-fallback.fix b/changelog.d/activitypub-bot-rfc9421-fallback.fix new file mode 100644 index 000000000..22642fee1 --- /dev/null +++ b/changelog.d/activitypub-bot-rfc9421-fallback.fix @@ -0,0 +1 @@ +Reject unsupported RFC 9421 inbox signatures instead of queueing a doomed failed-signature retry, allowing clients to fall back to legacy HTTP Signatures. diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 3d1875599..dd644c132 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -353,15 +353,21 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do end def inbox(%{assigns: %{valid_signature: false}} = conn, params) do - Federator.incoming_failed_signature_ap_doc(%{ - method: conn.method, - req_headers: conn.req_headers, - request_path: conn.request_path, - params: params, - query_string: conn.query_string - }) + if unsupported_http_message_signature?(conn) do + conn + |> put_status(:unauthorized) + |> json("error, unsupported HTTP Message Signature") + else + Federator.incoming_failed_signature_ap_doc(%{ + method: conn.method, + req_headers: conn.req_headers, + request_path: conn.request_path, + params: params, + query_string: conn.query_string + }) - json(conn, "ok") + json(conn, "ok") + end end # POST /relay/inbox -or- POST /internal/fetch/inbox @@ -381,6 +387,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do |> json("error, missing HTTP Signature") end + defp unsupported_http_message_signature?(conn) do + Plug.Conn.get_req_header(conn, "signature-input") != [] and + not cavage_signature?(conn) + end + + defp cavage_signature?(conn) do + conn + |> Plug.Conn.get_req_header("signature") + |> Enum.any?(&Regex.match?(~r/(^|,)\s*keyId\s*=/, &1)) + end + defp post_inbox_relayed_create(conn, params) do Logger.debug( "Signature missing or not from author, relayed Create message, fetching object from source" diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index 68e39b3a0..e9a777758 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -757,6 +757,83 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert Activity.get_by_ap_id(data["id"]) end + test "rejects unsupported HTTP Message Signatures so clients can fall back", %{conn: conn} do + data = %{ + "type" => "Create", + "actor" => "https://activitypubbot.example/user/ok", + "id" => "https://activitypubbot.example/activities/rfc9421-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => "https://activitypubbot.example/objects/rfc9421-note", + "actor" => "https://activitypubbot.example/user/ok", + "attributedTo" => "https://activitypubbot.example/user/ok", + "content" => "hello from activitypub-bot", + "published" => "2026-05-29T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> false end) + + conn = + conn + |> put_req_header("content-type", "application/activity+json") + |> put_req_header("date", "Fri, 29 May 2026 13:33:31 GMT") + |> put_req_header("content-digest", "sha-256=:fake-digest:") + |> put_req_header( + "signature-input", + "sig1=(\"@method\" \"@target-uri\" \"date\" \"content-type\" \"content-digest\");keyid=\"https://activitypubbot.example/user/ok/publickey\";alg=\"rsa-v1_5-sha256\";created=1780061611" + ) + |> put_req_header("signature", "sig1=:fake-signature:") + |> post("/inbox", data) + + assert "error, unsupported HTTP Message Signature" == json_response(conn, 401) + assert [] == all_enqueued(worker: SignatureRetryWorker) + end + + test "keeps failed-signature retry for legacy signatures with Signature-Input", %{conn: conn} do + data = %{ + "type" => "Create", + "actor" => "https://activitypubbot.example/user/ok", + "id" => "https://activitypubbot.example/activities/legacy-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => "https://activitypubbot.example/objects/legacy-note", + "actor" => "https://activitypubbot.example/user/ok", + "attributedTo" => "https://activitypubbot.example/user/ok", + "content" => "hello from activitypub-bot", + "published" => "2026-05-29T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> false end) + + conn = + conn + |> put_req_header("content-type", "application/activity+json") + |> put_req_header("date", "Fri, 29 May 2026 13:33:31 GMT") + |> put_req_header("digest", "SHA-256=fake-digest") + |> put_req_header( + "signature-input", + "sig1=(\"@method\" \"@target-uri\" \"date\" \"content-type\" \"digest\");keyid=\"https://activitypubbot.example/user/ok/publickey\";alg=\"rsa-v1_5-sha256\";created=1780061611" + ) + |> put_req_header( + "signature", + "keyId=\"https://activitypubbot.example/user/ok#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" + ) + |> post("/inbox", data) + + assert "ok" == json_response(conn, 200) + assert [_] = all_enqueued(worker: SignatureRetryWorker) + end + test "does not create a forged post after failed signature retry", %{conn: conn} do alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") From 86dd9663fc3e1162d5747fc140774374dcc65896 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 1 Jun 2026 22:06:12 +0300 Subject: [PATCH 166/391] allow
(used by mfm) --- priv/scrubbers/default.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/priv/scrubbers/default.ex b/priv/scrubbers/default.ex index cc5eba027..8e2d16650 100644 --- a/priv/scrubbers/default.ex +++ b/priv/scrubbers/default.ex @@ -46,6 +46,7 @@ defmodule Pleroma.HTML.Scrubber.Default do Meta.allow_tag_with_these_attributes(:b, ["lang"]) Meta.allow_tag_with_these_attributes(:blockquote, ["lang"]) Meta.allow_tag_with_these_attributes(:br, ["lang"]) + Meta.allow_tag_with_these_attributes(:center, []) Meta.allow_tag_with_these_attributes(:cite, ["lang"]) Meta.allow_tag_with_these_attributes(:code, ["lang"]) Meta.allow_tag_with_these_attributes(:del, ["lang"]) From 07c65adb0c5ad94610a998730cd81439bf498f8b Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 10 Jun 2026 22:26:16 +0200 Subject: [PATCH 167/391] Grouped Notifications: Add feature flag to instance view and nodeinfo --- changelog.d/grouped_notifications_flag.skip | 0 lib/pleroma/web/mastodon_api/views/instance_view.ex | 1 + test/pleroma/web/node_info_test.exs | 1 + 3 files changed, 2 insertions(+) create mode 100644 changelog.d/grouped_notifications_flag.skip diff --git a/changelog.d/grouped_notifications_flag.skip b/changelog.d/grouped_notifications_flag.skip new file mode 100644 index 000000000..e69de29bb diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index d64ce4fb5..4fccdd01e 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -166,6 +166,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do [ "pleroma_api", "mastodon_api", + "mastodon_api_grouped_notifications", "mastodon_api_streaming", "polls", "v2_suggestions", diff --git a/test/pleroma/web/node_info_test.exs b/test/pleroma/web/node_info_test.exs index afe4ebb36..e2bf3400a 100644 --- a/test/pleroma/web/node_info_test.exs +++ b/test/pleroma/web/node_info_test.exs @@ -160,6 +160,7 @@ defmodule Pleroma.Web.NodeInfoTest do default_features = [ "pleroma_api", "mastodon_api", + "mastodon_api_grouped_notifications", "mastodon_api_streaming", "polls", "pleroma_explicit_addressing", From 9782bcf979c12b06b7aefde14ed3708e74ff0657 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 16 Jun 2026 18:22:23 +0200 Subject: [PATCH 168/391] Elixir 1.19: Fix most MRFs being absent from AdminAPI suggestions. Elixir 1.19 started lazily loading modules, so modules only get loaded when referenced. Thus :code.all_loaded would no longer include all MRF policies which is used on startup to generate sugestions for AdminAPI. --- lib/pleroma/docs/generator.ex | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/docs/generator.ex b/lib/pleroma/docs/generator.ex index 93b19484f..70413837a 100644 --- a/lib/pleroma/docs/generator.ex +++ b/lib/pleroma/docs/generator.ex @@ -12,13 +12,27 @@ defmodule Pleroma.Docs.Generator do @spec list_behaviour_implementations(behaviour :: module()) :: [module()] def list_behaviour_implementations(behaviour) do - :code.all_loaded() - |> Enum.filter(fn {module, _} -> + # :code.all_loaded() no longer works on Elixir 1.19 and newer since modules are lazily loaded. + # We can get a complete list of modules by interating over Application.loaded_applications/0, + # but that includes every module which would get loaded below, which is very slow. + # Hardcode the Pleroma application instead, which is good enough for now. + application_modules = + case :application.get_key(:pleroma, :modules) do + {:ok, modules} -> modules + :undefined -> [] + end + + # At compile time or in tests some modules will be outside of the Pleroma application. + loaded_modules = Enum.map(:code.all_loaded(), fn {module, _} -> module end) + + (application_modules ++ loaded_modules) + |> Enum.uniq() + |> Enum.filter(fn module -> + Code.ensure_loaded(module) + # This shouldn't be needed as all modules are expected to have module_info/1, # but in test environments some transient modules `:elixir_compiler_XX` # are loaded for some reason (where XX is a random integer). - Code.ensure_loaded(module) - if function_exported?(module, :module_info, 1) do module.module_info(:attributes) |> Keyword.get_values(:behaviour) @@ -26,7 +40,6 @@ defmodule Pleroma.Docs.Generator do |> Enum.member?(behaviour) end end) - |> Enum.map(fn {module, _} -> module end) end @doc """ From c6d1cead8323ac8ce7ebd13395f84d90fea43518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Tue, 23 Jun 2026 16:08:52 +0200 Subject: [PATCH 169/391] Allow attaching emojis to user lists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/list-emoji.add | 1 + lib/pleroma/list.ex | 42 ++++++++++++++++++- .../web/api_spec/operations/list_operation.ex | 9 ++-- lib/pleroma/web/api_spec/schemas/list.ex | 17 +++++++- .../web/mastodon_api/views/list_view.ex | 23 +++++++++- .../20260623000000_add_emoji_to_lists.exs | 9 ++++ test/pleroma/list_test.exs | 23 ++++++++++ .../controllers/list_controller_test.exs | 33 +++++++++++++++ .../web/mastodon_api/views/list_view_test.exs | 18 +++++++- 9 files changed, 168 insertions(+), 7 deletions(-) create mode 100644 changelog.d/list-emoji.add create mode 100644 priv/repo/migrations/20260623000000_add_emoji_to_lists.exs diff --git a/changelog.d/list-emoji.add b/changelog.d/list-emoji.add new file mode 100644 index 000000000..dce4874d3 --- /dev/null +++ b/changelog.d/list-emoji.add @@ -0,0 +1 @@ +Allow attaching emojis to user lists diff --git a/lib/pleroma/list.ex b/lib/pleroma/list.ex index cd23bd0a9..3e8a83342 100644 --- a/lib/pleroma/list.ex +++ b/lib/pleroma/list.ex @@ -9,6 +9,7 @@ defmodule Pleroma.List do import Ecto.Changeset alias Pleroma.Activity + alias Pleroma.Emoji alias Pleroma.Repo alias Pleroma.User @@ -18,14 +19,53 @@ defmodule Pleroma.List do field(:following, {:array, :string}, default: []) field(:ap_id, :string) field(:exclusive, :boolean, default: false) + field(:emoji, :string) timestamps() end def update_changeset(list, attrs \\ %{}) do list - |> cast(attrs, [:title, :exclusive]) + |> cast(attrs, [:title, :exclusive, :emoji]) |> validate_required([:title]) + |> fix_emoji() + |> validate_emoji() + end + + defp fix_emoji(changeset) do + with {:emoji_field, emoji} when is_binary(emoji) <- + {:emoji_field, get_field(changeset, :emoji)}, + {:fixed_emoji, emoji} <- + {:fixed_emoji, + emoji + |> Emoji.fully_qualify_emoji() + |> Emoji.maybe_quote()} do + put_change(changeset, :emoji, emoji) + else + {:emoji_field, _} -> changeset + end + end + + defp validate_emoji(changeset) do + validate_change(changeset, :emoji, fn + :emoji, nil -> + [] + + :emoji, emoji -> + if Emoji.unicode?(emoji) or valid_local_custom_emoji?(emoji) do + [] + else + [emoji: "Invalid emoji"] + end + end) + end + + defp valid_local_custom_emoji?(emoji) do + with %{file: _path} <- Emoji.get(emoji) do + true + else + _ -> false + end end def follow_changeset(list, attrs \\ %{}) do diff --git a/lib/pleroma/web/api_spec/operations/list_operation.ex b/lib/pleroma/web/api_spec/operations/list_operation.ex index 87189edc2..8aec1c352 100644 --- a/lib/pleroma/web/api_spec/operations/list_operation.ex +++ b/lib/pleroma/web/api_spec/operations/list_operation.ex @@ -41,7 +41,8 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do responses: %{ 200 => Operation.response("List", "application/json", List), 400 => Operation.response("Error", "application/json", ApiError), - 404 => Operation.response("Error", "application/json", ApiError) + 404 => Operation.response("Error", "application/json", ApiError), + 422 => Operation.response("Error", "application/json", ApiError) } } end @@ -175,7 +176,8 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do exclusive: %Schema{ type: :boolean, description: "Whether members of the list should be removed from the “Home” feed" - } + }, + emoji: %Schema{type: :string, description: "List emoji", nullable: true} }, required: [:title] }, @@ -194,7 +196,8 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do exclusive: %Schema{ type: :boolean, description: "Whether members of the list should be removed from the “Home” feed" - } + }, + emoji: %Schema{type: :string, description: "List emoji", nullable: true} } }, required: true diff --git a/lib/pleroma/web/api_spec/schemas/list.ex b/lib/pleroma/web/api_spec/schemas/list.ex index 5df674894..ff52b1def 100644 --- a/lib/pleroma/web/api_spec/schemas/list.ex +++ b/lib/pleroma/web/api_spec/schemas/list.ex @@ -17,11 +17,26 @@ defmodule Pleroma.Web.ApiSpec.Schemas.List do exclusive: %Schema{ type: :boolean, description: "Whether members of the list should be removed from the “Home” feed" + }, + pleroma: %Schema{ + type: :object, + properties: %{ + emoji: %Schema{type: :string, description: "List emoji", nullable: true}, + emoji_url: %Schema{ + type: :string, + description: "URL of the list emoji if it's a custom emoji, null otherwise", + nullable: true + } + } } }, example: %{ "id" => "12249", - "title" => "Friends" + "title" => "Friends", + "pleroma" => %{ + "emoji" => nil, + "emoji_url" => nil + } } }) end diff --git a/lib/pleroma/web/mastodon_api/views/list_view.ex b/lib/pleroma/web/mastodon_api/views/list_view.ex index 740f458d4..508169fc1 100644 --- a/lib/pleroma/web/mastodon_api/views/list_view.ex +++ b/lib/pleroma/web/mastodon_api/views/list_view.ex @@ -4,6 +4,8 @@ defmodule Pleroma.Web.MastodonAPI.ListView do use Pleroma.Web, :view + + alias Pleroma.Emoji alias Pleroma.Web.MastodonAPI.ListView def render("index.json", %{lists: lists} = opts) do @@ -11,10 +13,29 @@ defmodule Pleroma.Web.MastodonAPI.ListView do end def render("show.json", %{list: list}) do + {emoji, emoji_url} = get_emoji(list.emoji) + %{ id: to_string(list.id), title: list.title, - exclusive: list.exclusive + exclusive: list.exclusive, + pleroma: %{ + emoji: emoji, + emoji_url: emoji_url + } } end + + defp get_emoji(nil), do: {nil, nil} + + defp get_emoji(emoji) do + if Emoji.unicode?(emoji) do + {emoji, nil} + else + case Emoji.get(emoji) do + nil -> {nil, nil} + emoji_data -> {emoji, Emoji.local_url(emoji_data.file)} + end + end + end end diff --git a/priv/repo/migrations/20260623000000_add_emoji_to_lists.exs b/priv/repo/migrations/20260623000000_add_emoji_to_lists.exs new file mode 100644 index 000000000..b72ff0c13 --- /dev/null +++ b/priv/repo/migrations/20260623000000_add_emoji_to_lists.exs @@ -0,0 +1,9 @@ +defmodule Pleroma.Repo.Migrations.AddEmojiToLists do + use Ecto.Migration + + def change do + alter table(:lists) do + add(:emoji, :string) + end + end +end diff --git a/test/pleroma/list_test.exs b/test/pleroma/list_test.exs index d44310689..32121b8fd 100644 --- a/test/pleroma/list_test.exs +++ b/test/pleroma/list_test.exs @@ -23,6 +23,29 @@ defmodule Pleroma.ListTest do assert changeset.errors == [title: {"can't be blank", [validation: :required]}] end + test "creating a list with an emoji" do + user = insert(:user) + {:ok, %Pleroma.List{} = list} = Pleroma.List.create(%{title: "title", emoji: "🕓"}, user) + %Pleroma.List{emoji: emoji} = Pleroma.List.get(list.id, user) + assert emoji == "🕓" + end + + test "validates emoji" do + user = insert(:user) + + assert {:error, changeset} = + Pleroma.List.create(%{title: "title", emoji: "not an emoji"}, user) + + assert changeset.errors == [emoji: {"Invalid emoji", []}] + end + + test "updating a list's emoji" do + user = insert(:user) + {:ok, list} = Pleroma.List.create(%{title: "title"}, user) + {:ok, %{emoji: emoji}} = Pleroma.List.update(list, %{emoji: "🕓"}) + assert "🕓" == emoji + end + test "getting a list not belonging to the user" do user = insert(:user) other_user = insert(:user) diff --git a/test/pleroma/web/mastodon_api/controllers/list_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/list_controller_test.exs index bfda48be4..2845f67ed 100644 --- a/test/pleroma/web/mastodon_api/controllers/list_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/list_controller_test.exs @@ -19,6 +19,16 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do |> json_response_and_validate_schema(:ok) end + test "creating a list with an emoji" do + %{conn: conn} = oauth_access(["write:lists"]) + + assert %{"title" => "cuties", "pleroma" => %{"emoji" => "🕓", "emoji_url" => nil}} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/lists", %{"title" => "cuties", "emoji" => "🕓"}) + |> json_response_and_validate_schema(:ok) + end + test "renders error for invalid params" do %{conn: conn} = oauth_access(["write:lists"]) @@ -31,6 +41,18 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do json_response_and_validate_schema(conn, 400) end + test "renders error for an invalid emoji" do + %{conn: conn} = oauth_access(["write:lists"]) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/lists", %{"title" => "cuties", "emoji" => "not an emoji"}) + + assert %{"error" => "Invalid emoji"} = + json_response_and_validate_schema(conn, :unprocessable_entity) + end + test "listing a user's lists" do %{conn: conn} = oauth_access(["read:lists", "write:lists"]) @@ -159,6 +181,17 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do |> json_response_and_validate_schema(:ok) end + test "updating a list's emoji" do + %{user: user, conn: conn} = oauth_access(["write:lists"]) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) + + assert %{"pleroma" => %{"emoji" => "🕓", "emoji_url" => nil}} = + conn + |> put_req_header("content-type", "application/json") + |> put("/api/v1/lists/#{list.id}", %{"title" => "name", "emoji" => "🕓"}) + |> json_response_and_validate_schema(:ok) + end + test "validates title when renaming a list" do %{user: user, conn: conn} = oauth_access(["write:lists"]) {:ok, list} = Pleroma.List.create(%{title: "name"}, user) diff --git a/test/pleroma/web/mastodon_api/views/list_view_test.exs b/test/pleroma/web/mastodon_api/views/list_view_test.exs index ae0593b6b..86f461ded 100644 --- a/test/pleroma/web/mastodon_api/views/list_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/list_view_test.exs @@ -15,12 +15,28 @@ defmodule Pleroma.Web.MastodonAPI.ListViewTest do expected = %{ id: to_string(list.id), title: title, - exclusive: false + exclusive: false, + pleroma: %{ + emoji: nil, + emoji_url: nil + } } assert expected == ListView.render("show.json", %{list: list}) end + test "show with a unicode emoji" do + user = insert(:user) + {:ok, list} = Pleroma.List.create(%{title: "mortal enemies", emoji: "🕓"}, user) + + assert %{ + pleroma: %{ + emoji: "🕓", + emoji_url: nil + } + } = ListView.render("show.json", %{list: list}) + end + test "index" do user = insert(:user) From 19a84baf82da5b8e8204bff9e305eeac5bdb4189 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 10 Jun 2026 22:54:22 +0200 Subject: [PATCH 170/391] InstanceView: Add feature flag tests --- .../mastodon_api/views/instance_view_test.exs | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 test/pleroma/web/mastodon_api/views/instance_view_test.exs diff --git a/test/pleroma/web/mastodon_api/views/instance_view_test.exs b/test/pleroma/web/mastodon_api/views/instance_view_test.exs new file mode 100644 index 000000000..46bb80ba4 --- /dev/null +++ b/test/pleroma/web/mastodon_api/views/instance_view_test.exs @@ -0,0 +1,98 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2026 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do + use Pleroma.DataCase, async: true + + alias Pleroma.Language.LanguageDetectorMock + alias Pleroma.StaticStubbedConfigMock + alias Pleroma.Web.MastodonAPI.InstanceView + + import Mox + + @features [ + "pleroma_api", + "mastodon_api", + "mastodon_api_grouped_notifications", + "mastodon_api_streaming", + "polls", + "v2_suggestions", + "pleroma_explicit_addressing", + "shareable_emoji_packs", + "multifetch", + "pleroma:api/v1/notifications:include_types_filter", + "editing", + "quote_posting", + "pleroma_emoji_reactions", + "pleroma_custom_emoji_reactions", + "pleroma_chat_messages", + "pleroma:pin_chats", + "pleroma:get:main/ostatus", + "pleroma:group_actors", + "pleroma:bookmark_folders", + "pleroma:block_expiration" + ] + + @configureable_features [ + "blockers_visible", + "media_proxy", + "gopher", + "chat", + "shout", + "relay", + "safe_dm_mentions", + "exposable_reactions", + "profile_directory", + "pleroma:language_detection" + ] + @configurable_features_flags [ + [:activitypub, :blockers_visible], + [:media_proxy, :enabled], + [:gopher, :enabled], + [:shout, :enabled], + [:instance, :allow_relay], + [:instance, :safe_dm_mentions], + [:instance, :show_reactions], + [:instance, :profile_directory] + ] + + # When this fails, a new feature flag was probably added. Add it to the macros above. + describe "uncofigurable features" do + setup do + Enum.each(@configurable_features_flags, &clear_config(&1, false)) + end + + test "returns always enabled features" do + features = @features + + assert Enum.sort(InstanceView.features()) == Enum.sort(features) + end + end + + describe "all features" do + setup do + Enum.each(@configurable_features_flags, &clear_config(&1, true)) + + # Stub the StaticStubbedConfigMock to return our mock for the provider + StaticStubbedConfigMock + |> stub(:get, fn + [Pleroma.Language.LanguageDetector, :provider] -> LanguageDetectorMock + _other -> nil + end) + + # Stub the LanguageDetectorMock with default implementations + LanguageDetectorMock + |> stub(:missing_dependencies, fn -> [] end) + |> stub(:configured?, fn -> true end) + + :ok + end + + test "returns all features including configurable ones" do + features = @features ++ @configureable_features + + assert Enum.sort(InstanceView.features()) == Enum.sort(features) + end + end +end From 7e9b8a766340248578a675df772410b1960ee22c Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 23 Jun 2026 18:20:16 +0200 Subject: [PATCH 171/391] InstanceView: Add federation/0 func test --- .../mastodon_api/views/instance_view_test.exs | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/test/pleroma/web/mastodon_api/views/instance_view_test.exs b/test/pleroma/web/mastodon_api/views/instance_view_test.exs index 46bb80ba4..d34aad25b 100644 --- a/test/pleroma/web/mastodon_api/views/instance_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/instance_view_test.exs @@ -95,4 +95,72 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do assert Enum.sort(InstanceView.features()) == Enum.sort(features) end end + + describe "federation/0" do + setup do: clear_config([:mrf, :transparency], true) + + test "quarantined instances" do + clear_config([:instance, :quarantined_instances], [{"quarantine.example.com", "crimes"}]) + + output = InstanceView.federation() + assert %{quarantined_instances: ["quarantine.example.com"]} = output + assert %{quarantined_instances_info: %{"quarantined_instances" => %{"quarantine.example.com" => %{"reason" => "crimes"}}}} = output + end + + test "rejected instances" do + clear_config([:instance, :rejected_instances], [{"rejected.example.com", "not enough #cofe posting"}]) + + output = InstanceView.federation() + assert %{rejected_instances: %{"rejected.example.com" => %{"reason" => "not enough #cofe posting"}}} = output + end + + test "federating" do + clear_config([:instance, :federating], true) + + output = InstanceView.federation() + assert %{enabled: true} = output + end + + test "transparency" do + clear_config([:instance, :quarantined_instances], [{"quarantine.example.com", "crimes"}]) + clear_config([:instance, :rejected_instances], [{"rejected.example.com", "not enough #cofe posting"}]) + clear_config([:instance, :federating], true) + clear_config([:mrf, :transparency], false) + + output = InstanceView.federation() + assert %{enabled: true} == output + end + + test "MRFs" do + clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy]) + clear_config([:mrf, :transparency_exclusions], [{"media1.example.com", "the Fediverse doesn't need to know"}]) + clear_config([:mrf_simple, :media_removal], [{"media1.example.com", "NSFW"}, {"media2.example.com", "usual suspects"}]) + clear_config([:mrf_simple, :reject], [{"rejected.example.com", "not enough #cofe posting"}]) + + output = InstanceView.federation() + + expected = %{ + mrf_simple: %{ + reject: ["rejected.example.com"], + media_removal: ["media2.example.com"], + }, + mrf_hashtag: %{ + sensitive: ["nsfw"], + }, + exclusions: true, + mrf_policies: ["SimplePolicy", "HashtagPolicy"], + mrf_simple_info: %{ + reject: %{ + "rejected.example.com" => %{"reason" => "not enough #cofe posting"} + }, + media_removal: %{"media2.example.com" => %{"reason" => "usual suspects"}} + } + } + + assert expected.mrf_simple.reject == output.mrf_simple.reject + assert expected.mrf_simple.media_removal == output.mrf_simple.media_removal + assert expected.mrf_simple_info == output.mrf_simple_info + assert expected.exclusions == output.exclusions + end + end end From 86725b5a000a6992cad522be06d0aa99dd829bcb Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 23 Jun 2026 22:22:49 +0200 Subject: [PATCH 172/391] InstanceView: add rule(s), translation render tests --- .../mastodon_api/views/instance_view_test.exs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/test/pleroma/web/mastodon_api/views/instance_view_test.exs b/test/pleroma/web/mastodon_api/views/instance_view_test.exs index d34aad25b..d3e850766 100644 --- a/test/pleroma/web/mastodon_api/views/instance_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/instance_view_test.exs @@ -163,4 +163,49 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do assert expected.exclusions == output.exclusions end end + + describe "render rule.json" do + setup do + rule = Pleroma.Rule.create(%{text: "hambaga"}) + + %{rule: rule} + end + + test "renders properly", %{rule: rule} do + output = InstanceView.render("rule.json", %{rule: rule}) + + assert %{text: "hambaga"} = output + end + end + + describe "render rules.json" do + setup do + Pleroma.Rule.create(%{text: "rule 1"}) + Pleroma.Rule.create(%{text: "rule 2"}) + Pleroma.Rule.create(%{text: "rule 3"}) + Pleroma.Rule.create(%{text: "rule 4"}) + + :ok + end + + test "renders properly" do + output = InstanceView.render("rules.json", %{}) + keys = Enum.map(output, fn %{text: text} -> text end) + + assert length(keys) == 4 + assert Enum.all?(output, fn %{text: "rule " <> num} -> String.to_integer(num) end) + end + end + + describe "render translation_languages.json" do + setup do + clear_config([Pleroma.Language.Translation, :provider], TranslationMock) + end + + test "returns language matrix" do + output = InstanceView.render("translation_languages.json", %{}) + + assert %{"en" => ["pl"], "pl" => ["en"]} == output + end + end end From df67f219748418b18063d9e788a1945d9dbf14a8 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 23 Jun 2026 22:24:44 +0200 Subject: [PATCH 173/391] InstanceView: use shorter setup syntax when applicable --- .../pleroma/web/mastodon_api/views/instance_view_test.exs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/test/pleroma/web/mastodon_api/views/instance_view_test.exs b/test/pleroma/web/mastodon_api/views/instance_view_test.exs index d3e850766..f1852cadd 100644 --- a/test/pleroma/web/mastodon_api/views/instance_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/instance_view_test.exs @@ -59,9 +59,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do # When this fails, a new feature flag was probably added. Add it to the macros above. describe "uncofigurable features" do - setup do - Enum.each(@configurable_features_flags, &clear_config(&1, false)) - end + setup do: Enum.each(@configurable_features_flags, &clear_config(&1, false)) test "returns always enabled features" do features = @features @@ -198,9 +196,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do end describe "render translation_languages.json" do - setup do - clear_config([Pleroma.Language.Translation, :provider], TranslationMock) - end + setup do: clear_config([Pleroma.Language.Translation, :provider], TranslationMock) test "returns language matrix" do output = InstanceView.render("translation_languages.json", %{}) From e45dad21046435a3f5e93412a183955c34df16fb Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 23 Jun 2026 22:42:15 +0200 Subject: [PATCH 174/391] InstanceView: Add domain_blocks.json render test --- .../mastodon_api/views/instance_view_test.exs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/test/pleroma/web/mastodon_api/views/instance_view_test.exs b/test/pleroma/web/mastodon_api/views/instance_view_test.exs index f1852cadd..9701fab85 100644 --- a/test/pleroma/web/mastodon_api/views/instance_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/instance_view_test.exs @@ -195,6 +195,46 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do end end + describe "render domain_blocks.json" do + setup do + clear_config([:mrf, :transparency], true) + clear_config([:mrf, :transparency_exclusions], [{"removed1.example.com", "the Fediverse doesn't need to know"}]) + clear_config([:mrf_simple, :media_removal], [{"media1.example.com", "NSFW"}]) + clear_config([:mrf_simple, :reject], [{"rejected.example.com", "not enough #cofe posting"}, {"rejected-without-reason.example.com", ""}]) + clear_config([:mrf_simple, :federated_timeline_removal], [{"removed1.example.com", "spam"}, {"removed2.example.com", "more spam"}]) + + :ok + end + + test "renders properly" do + output = InstanceView.render("domain_blocks.json", %{}) + + # Media removal is not in @block_severities in the view. + # NOTE: Order of these two matters! + expected = [ + %{ + domain: "removed2.example.com", + comment: "more spam", + severity: "silence", + digest: "5f6649cfc780fac5172dbbcc8a552953f6621a14368d82728b6e8faf51295dc0" + }, + %{ + domain: "rejected.example.com", + comment: "not enough #cofe posting", + severity: "suspend", + digest: "c15082d347fb7eccd251e199a98dcdce94a8dfc1d33d6ece80889eed374b6f92" + }, + %{ + domain: "rejected-without-reason.example.com", + severity: "suspend", + digest: "48297155018c626c8867ca0bba4a2621eccdf51c6f7ac91b7099ed8b2ee9863f" + } + ] + + assert expected == output + end + end + describe "render translation_languages.json" do setup do: clear_config([Pleroma.Language.Translation, :provider], TranslationMock) From c290f022de1b262d8e4d84be84ad7095a89043b5 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 24 Jun 2026 00:28:44 +0200 Subject: [PATCH 175/391] InstanceView: Add show.json render test --- .../web/mastodon_api/views/instance_view.ex | 2 + .../mastodon_api/views/instance_view_test.exs | 194 ++++++++++++++++++ 2 files changed, 196 insertions(+) diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index 4fccdd01e..1a0f63ffa 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -17,6 +17,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do reject: "suspend" } + def mastodon_api_level, do: @mastodon_api_level + def render("show.json", _) do instance = Config.get(:instance) diff --git a/test/pleroma/web/mastodon_api/views/instance_view_test.exs b/test/pleroma/web/mastodon_api/views/instance_view_test.exs index 9701fab85..3b24e8e5f 100644 --- a/test/pleroma/web/mastodon_api/views/instance_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/instance_view_test.exs @@ -5,12 +5,63 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do use Pleroma.DataCase, async: true + import Pleroma.Factory + alias Pleroma.Language.LanguageDetectorMock alias Pleroma.StaticStubbedConfigMock alias Pleroma.Web.MastodonAPI.InstanceView import Mox + @atom_common_information_keys [ + :languages, + :rules, + :title, + :version + ] + + @common_information_keys Enum.map(@atom_common_information_keys, &to_string/1) + + @atom_configuration_keys [ + :accounts, + :statuses, + :media_attachments, + :polls + ] + + @atom_pleroma_configuration_keys [ + :metadata, + :stats, + :vapid_public_key + ] + + @show_keys @common_information_keys ++ [ + "uri", + "description", + "short_description", + "email", + "urls", + "stats", + "thumbnail", + "registrations", + "approval_required", + "contact_account", + "configuration", + # Extra (not present in Mastodon)" + "max_toot_chars", + "max_media_attachments", + "poll_limits", + "upload_limit", + "avatar_upload_limit", + "background_upload_limit", + "banner_upload_limit", + "background_image", + "shout_limit", + "description_limit", + "chat_limit", + "pleroma" + ] + @features [ "pleroma_api", "mastodon_api", @@ -57,6 +108,96 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do [:instance, :profile_directory] ] + defp check_common_information(info) do + filtered_info = Map.reject(info, fn {key, _value} -> key not in @atom_common_information_keys end) + + expected = %{ + languages: ["en", "cs"], + rules: InstanceView.render("rules.json", %{}), + title: "test title", + version: InstanceView.mastodon_api_level() <> " (compatible; #{Pleroma.Application.named_version()})" + } + + Map.equal?(expected, filtered_info) + end + + defp check_contact_name(info, user) do + contact = info[:contact_account] + view = Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user, for: nil}) + + contact == view + end + + defp check_configuration(info) do + configuration = info[:configuration] + filtered_configuration = Map.reject(configuration, fn {key, _value} -> key not in @atom_configuration_keys end) + + expected = %{ + accounts: %{ + max_featured_tags: 0 + }, + statuses: %{ + max_characters: 4096, + max_media_attachments: 1 + }, + media_attachments: %{ + image_size_limit: 1024, + video_size_limit: 1024, + supported_mime_types: ["application/octet-stream"] + }, + polls: %{ + max_options: 4, + max_characters_per_option: 120, + min_expiration: 1, + max_expiration: 2 + } + } + + Map.equal?(expected, filtered_configuration) + end + + defp check_pleroma_configuration(info) do + configuration = info[:pleroma] + filtered_configuration = Map.reject(configuration, fn {key, _value} -> key not in @atom_pleroma_configuration_keys end) + metadata = Map.fetch!(filtered_configuration, :metadata) + # Tested elsewhere already + filtered_metadata = Map.reject(metadata, fn {key, _value} -> key in [:features, :federation] end) + filtered_configuration = %{filtered_configuration | metadata: filtered_metadata} + + expected = %{ + metadata: %{ + account_activation_required: true, + fields_limits: %{ + max_fields: 1, + max_remote_fields: 1, + name_length: 1, + value_length: 1 + }, + post_formats: ["text/plain"], + birthday_required: true, + birthday_min_age: 1, + translation: + %{ + source_languages: ["en", "pl"], + target_languages: ["en", "pl"] + }, + base_urls: %{ + media_proxy: "https://mediaproxy.example.com", + upload: "https://upload.example.com" + }, + markup: %{ + allow_inline_images: true, + allow_headings: true, + allow_tables: true + } + }, + stats: %{mau: Pleroma.User.active_user_count()}, + vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) + } + + Map.equal?(expected, filtered_configuration) + end + # When this fails, a new feature flag was probably added. Add it to the macros above. describe "uncofigurable features" do setup do: Enum.each(@configurable_features_flags, &clear_config(&1, false)) @@ -162,6 +303,59 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do end end + describe "render show.json" do + setup do + Pleroma.Rule.create(%{text: "rule 1"}) + Pleroma.Rule.create(%{text: "rule 2"}) + + user = insert(:user) + + # Wall of configs for testing whether they are rendered properly + clear_config([Pleroma.Language.Translation, :provider], TranslationMock) + + clear_config([:instance, :languages], ["en", "cs"]) + clear_config([:instance, :name], "test title") + clear_config([:instance, :contact_username], user.nickname) + clear_config([:instance, :limit], 4096) + clear_config([:instance, :max_media_attachments], 1) + clear_config([:instance, :upload_limit], 1024) + clear_config([:instance, :poll_limits, :max_options], 4) + clear_config([:instance, :poll_limits, :max_option_chars], 120) + clear_config([:instance, :poll_limits, :min_expiration], 1) + clear_config([:instance, :poll_limits, :max_expiration], 2) + clear_config([:instance, :account_activation_required], true) + clear_config([:instance, :max_account_fields], 1) + clear_config([:instance, :max_remote_account_fields], 1) + clear_config([:instance, :account_field_name_length], 1) + clear_config([:instance, :account_field_value_length], 1) + clear_config([:instance, :allowed_post_formats], ["text/plain"]) + clear_config([:instance, :birthday_required], true) + clear_config([:instance, :birthday_min_age], 1) + + clear_config([:media_proxy, :enabled], true) + clear_config([:media_proxy, :base_url], "https://mediaproxy.example.com") + + clear_config([Pleroma.Upload, :base_url], "https://upload.example.com") + + clear_config([:markup, :allow_inline_images], true) + clear_config([:markup, :allow_headings], true) + clear_config([:markup, :allow_tables], true) + + %{user: user} + end + + test "renders properly", %{user: user} do + output = InstanceView.render("show.json", %{}) + expected_keys = Enum.sort(@show_keys) + + assert check_common_information(output) + assert check_contact_name(output, user) + assert check_configuration(output) + assert check_pleroma_configuration(output) + assert expected_keys == Enum.sort(Enum.map(Map.keys(output), &to_string/1)) + end + end + describe "render rule.json" do setup do rule = Pleroma.Rule.create(%{text: "hambaga"}) From 0337bd0ae4ff1adf26dda28af5405a092c4782e1 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 25 Jun 2026 00:19:15 +0200 Subject: [PATCH 176/391] InstanceView: Add show2.json render test --- .../mastodon_api/views/instance_view_test.exs | 194 +++++++++++++++++- 1 file changed, 192 insertions(+), 2 deletions(-) diff --git a/test/pleroma/web/mastodon_api/views/instance_view_test.exs b/test/pleroma/web/mastodon_api/views/instance_view_test.exs index 3b24e8e5f..b4622191b 100644 --- a/test/pleroma/web/mastodon_api/views/instance_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/instance_view_test.exs @@ -29,12 +29,20 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do :polls ] + @atom_configuration2_keys @atom_configuration_keys ++ [ + :urls, + :vapid, + :translation, + :timelines_access + ] + @atom_pleroma_configuration_keys [ :metadata, :stats, :vapid_public_key ] + # TODO: Simplify this, so ideally only atoms are macroed @show_keys @common_information_keys ++ [ "uri", "description", @@ -62,6 +70,19 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do "pleroma" ] + @show2_keys @common_information_keys ++ [ + "domain", + "source_url", + "description", + "usage", + "thumbnail", + "configuration", + "registrations", + "contact", + # Extra (not present in Mastodon): + "pleroma" + ] + @features [ "pleroma_api", "mastodon_api", @@ -121,6 +142,27 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do Map.equal?(expected, filtered_info) end + defp check_thumbnail(info) do + thumbnail = info[:thumbnail] + + expected = %{ + url: URI.merge(Pleroma.Web.Endpoint.url(), "https://example.com/thumbnail.png") |> to_string + } + + expected == thumbnail + end + + defp check_contact(info, user) do + contact = info[:contact] + + expected = %{ + email: "nonexist@example.com", + account: Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user, for: nil}) + } + + Map.equal?(expected, contact) + end + defp check_contact_name(info, user) do contact = info[:contact_account] view = Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user, for: nil}) @@ -156,6 +198,70 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do Map.equal?(expected, filtered_configuration) end + defp check_configuration2(info) do + configuration = info[:configuration] + filtered_configuration = Map.reject(configuration, fn {key, _value} -> key not in @atom_configuration2_keys end) + + # configuration2 also includes parts from configuration + expected = %{ + accounts: %{ + max_featured_tags: 0, + max_pinned_statuses: 1, + max_profile_fields: 1, + profile_field_name_limit: 1, + profile_field_value_limit: 1 + }, + statuses: %{ + max_characters: 4096, + max_media_attachments: 1, + characters_reserved_per_url: 0 + }, + media_attachments: %{ + image_size_limit: 1024, + video_size_limit: 1024, + supported_mime_types: ["application/octet-stream"] + }, + polls: %{ + max_options: 4, + max_characters_per_option: 120, + min_expiration: 1, + max_expiration: 2 + }, + urls: %{ + streaming: Pleroma.Web.Endpoint.websocket_url(), + status: "https://status.example.com" + }, + vapid: %{ + public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) + }, + translation: %{enabled: true}, + timelines_access: %{ + live_feeds: %{local: "authenticated", remote: "authenticated"}, + hashtag_feeds: %{local: "authenticated", remote: "authenticated"}, + # not implemented in Pleroma + trending_link_feeds: %{ + local: "disabled", + remote: "disabled" + } + } + } + + Map.equal?(expected, filtered_configuration) + end + + defp check_registrations(info) do + registrations = info[:registrations] + + expected = %{ + enabled: false, + approval_required: true, + message: nil, + url: nil + } + + Map.equal?(expected, registrations) + end + defp check_pleroma_configuration(info) do configuration = info[:pleroma] filtered_configuration = Map.reject(configuration, fn {key, _value} -> key not in @atom_pleroma_configuration_keys end) @@ -198,6 +304,56 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do Map.equal?(expected, filtered_configuration) end + defp check_pleroma_configuration2(info) do + configuration = info[:pleroma] + filtered_configuration = Map.reject(configuration, fn {key, _value} -> key not in @atom_pleroma_configuration_keys end) + metadata = Map.fetch!(filtered_configuration, :metadata) + # Tested elsewhere already + filtered_metadata = Map.reject(metadata, fn {key, _value} -> key in [:features, :federation] end) + filtered_configuration = %{filtered_configuration | metadata: filtered_metadata} + + # pleroma_configuration2 also includes parts from pleroma_configuration + expected = %{ + metadata: %{ + account_activation_required: true, + fields_limits: %{ + max_fields: 1, + max_remote_fields: 1, + name_length: 1, + value_length: 1 + }, + post_formats: ["text/plain"], + birthday_required: true, + birthday_min_age: 1, + translation: + %{ + source_languages: ["en", "pl"], + target_languages: ["en", "pl"] + }, + base_urls: %{ + media_proxy: "https://mediaproxy.example.com", + upload: "https://upload.example.com" + }, + markup: %{ + allow_inline_images: true, + allow_headings: true, + allow_tables: true + }, + avatar_upload_limit: 1024, + background_upload_limit: 1024, + banner_upload_limit: 1024, + background_image: Pleroma.Web.Endpoint.url() <> "/media/background.png", + chat_limit: 120, + description_limit: 120, + shout_limit: 120 + }, + stats: %{mau: Pleroma.User.active_user_count()}, + vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) + } + + Map.equal?(expected, filtered_configuration) + end + # When this fails, a new feature flag was probably added. Add it to the macros above. describe "uncofigurable features" do setup do: Enum.each(@configurable_features_flags, &clear_config(&1, false)) @@ -303,7 +459,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do end end - describe "render show.json" do + describe "render show.json/show2.json" do setup do Pleroma.Rule.create(%{text: "rule 1"}) Pleroma.Rule.create(%{text: "rule 2"}) @@ -332,6 +488,23 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do clear_config([:instance, :birthday_required], true) clear_config([:instance, :birthday_min_age], 1) + clear_config([:instance, :short_description], "Corndog Emporium!") + clear_config([:instance, :instance_thumbnail], "https://example.com/thumbnail.png") + clear_config([:instance, :registrations_open], false) + clear_config([:instance, :account_approval_required], true) + + clear_config([:instance, :max_pinned_statuses], 1) + clear_config([:instance, :status_page], "https://status.example.com") + + clear_config([:instance, :email], "nonexist@example.com") + + clear_config([:instance, :avatar_upload_limit], 1024) + clear_config([:instance, :background_upload_limit], 1024) + clear_config([:instance, :banner_upload_limit], 1024) + clear_config([:instance, :background_image], "/media/background.png") + clear_config([:instance, :chat_limit], 120) + clear_config([:instance, :description_limit], 120) + clear_config([:media_proxy, :enabled], true) clear_config([:media_proxy, :base_url], "https://mediaproxy.example.com") @@ -341,10 +514,14 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do clear_config([:markup, :allow_headings], true) clear_config([:markup, :allow_tables], true) + clear_config([:restrict_unauthenticated, :timelines], %{local: true, federated: true}) + + clear_config([:shout, :limit], 120) + %{user: user} end - test "renders properly", %{user: user} do + test "renders show.json properly", %{user: user} do output = InstanceView.render("show.json", %{}) expected_keys = Enum.sort(@show_keys) @@ -354,6 +531,19 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do assert check_pleroma_configuration(output) assert expected_keys == Enum.sort(Enum.map(Map.keys(output), &to_string/1)) end + + test "renders show2.json properly", %{user: user} do + output = InstanceView.render("show2.json", %{}) + expected_keys = Enum.sort(@show2_keys) + + assert check_common_information(output) + assert check_thumbnail(output) + assert check_configuration2(output) + assert check_registrations(output) + assert check_contact(output, user) + assert check_pleroma_configuration2(output) + assert expected_keys == Enum.sort(Enum.map(Map.keys(output), &to_string/1)) + end end describe "render rule.json" do From 6393fe0a9e62051393c44acc1d66bd196616ea54 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 25 Jun 2026 23:21:09 +0200 Subject: [PATCH 177/391] InstanceView: removing unneeded doubling of attributes in tests --- .../mastodon_api/views/instance_view_test.exs | 97 +++++++++---------- 1 file changed, 47 insertions(+), 50 deletions(-) diff --git a/test/pleroma/web/mastodon_api/views/instance_view_test.exs b/test/pleroma/web/mastodon_api/views/instance_view_test.exs index b4622191b..93b5c60c9 100644 --- a/test/pleroma/web/mastodon_api/views/instance_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/instance_view_test.exs @@ -13,74 +13,71 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do import Mox - @atom_common_information_keys [ + @common_information_keys [ :languages, :rules, :title, :version ] - @common_information_keys Enum.map(@atom_common_information_keys, &to_string/1) - - @atom_configuration_keys [ + @configuration_keys [ :accounts, :statuses, :media_attachments, :polls ] - @atom_configuration2_keys @atom_configuration_keys ++ [ + @configuration2_keys @configuration_keys ++ [ :urls, :vapid, :translation, :timelines_access ] - @atom_pleroma_configuration_keys [ + @pleroma_configuration_keys [ :metadata, :stats, :vapid_public_key ] - # TODO: Simplify this, so ideally only atoms are macroed @show_keys @common_information_keys ++ [ - "uri", - "description", - "short_description", - "email", - "urls", - "stats", - "thumbnail", - "registrations", - "approval_required", - "contact_account", - "configuration", - # Extra (not present in Mastodon)" - "max_toot_chars", - "max_media_attachments", - "poll_limits", - "upload_limit", - "avatar_upload_limit", - "background_upload_limit", - "banner_upload_limit", - "background_image", - "shout_limit", - "description_limit", - "chat_limit", - "pleroma" + :uri, + :description, + :short_description, + :email, + :urls, + :stats, + :thumbnail, + :registrations, + :approval_required, + :contact_account, + :configuration, + # Extra (not present in Mastodon) + :max_toot_chars, + :max_media_attachments, + :poll_limits, + :upload_limit, + :avatar_upload_limit, + :background_upload_limit, + :banner_upload_limit, + :background_image, + :shout_limit, + :description_limit, + :chat_limit, + :pleroma ] @show2_keys @common_information_keys ++ [ - "domain", - "source_url", - "description", - "usage", - "thumbnail", - "configuration", - "registrations", - "contact", - # Extra (not present in Mastodon): - "pleroma" + :domain, + :source_url, + :description, + :usage, + :thumbnail, + :configuration, + :registrations, + :contact, + # Extra (not present in Mastodon) + :pleroma ] @features [ @@ -106,7 +103,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do "pleroma:block_expiration" ] - @configureable_features [ + @configurable_features [ "blockers_visible", "media_proxy", "gopher", @@ -130,7 +127,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do ] defp check_common_information(info) do - filtered_info = Map.reject(info, fn {key, _value} -> key not in @atom_common_information_keys end) + filtered_info = Map.reject(info, fn {key, _value} -> key not in @common_information_keys end) expected = %{ languages: ["en", "cs"], @@ -172,7 +169,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do defp check_configuration(info) do configuration = info[:configuration] - filtered_configuration = Map.reject(configuration, fn {key, _value} -> key not in @atom_configuration_keys end) + filtered_configuration = Map.reject(configuration, fn {key, _value} -> key not in @configuration_keys end) expected = %{ accounts: %{ @@ -200,7 +197,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do defp check_configuration2(info) do configuration = info[:configuration] - filtered_configuration = Map.reject(configuration, fn {key, _value} -> key not in @atom_configuration2_keys end) + filtered_configuration = Map.reject(configuration, fn {key, _value} -> key not in @configuration2_keys end) # configuration2 also includes parts from configuration expected = %{ @@ -264,7 +261,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do defp check_pleroma_configuration(info) do configuration = info[:pleroma] - filtered_configuration = Map.reject(configuration, fn {key, _value} -> key not in @atom_pleroma_configuration_keys end) + filtered_configuration = Map.reject(configuration, fn {key, _value} -> key not in @pleroma_configuration_keys end) metadata = Map.fetch!(filtered_configuration, :metadata) # Tested elsewhere already filtered_metadata = Map.reject(metadata, fn {key, _value} -> key in [:features, :federation] end) @@ -306,7 +303,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do defp check_pleroma_configuration2(info) do configuration = info[:pleroma] - filtered_configuration = Map.reject(configuration, fn {key, _value} -> key not in @atom_pleroma_configuration_keys end) + filtered_configuration = Map.reject(configuration, fn {key, _value} -> key not in @pleroma_configuration_keys end) metadata = Map.fetch!(filtered_configuration, :metadata) # Tested elsewhere already filtered_metadata = Map.reject(metadata, fn {key, _value} -> key in [:features, :federation] end) @@ -385,7 +382,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do end test "returns all features including configurable ones" do - features = @features ++ @configureable_features + features = @features ++ @configurable_features assert Enum.sort(InstanceView.features()) == Enum.sort(features) end @@ -529,7 +526,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do assert check_contact_name(output, user) assert check_configuration(output) assert check_pleroma_configuration(output) - assert expected_keys == Enum.sort(Enum.map(Map.keys(output), &to_string/1)) + assert expected_keys == Enum.sort(Map.keys(output)) end test "renders show2.json properly", %{user: user} do @@ -542,7 +539,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do assert check_registrations(output) assert check_contact(output, user) assert check_pleroma_configuration2(output) - assert expected_keys == Enum.sort(Enum.map(Map.keys(output), &to_string/1)) + assert expected_keys == Enum.sort(Map.keys(output)) end end From 93fe0f5ab5ed258d6b6e3b42e007ca37336218fb Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 26 Jun 2026 01:41:32 +0200 Subject: [PATCH 178/391] InstanceView: Filter out *configuration keys from *configuration2 --- .../mastodon_api/views/instance_view_test.exs | 88 ++++++++----------- 1 file changed, 39 insertions(+), 49 deletions(-) diff --git a/test/pleroma/web/mastodon_api/views/instance_view_test.exs b/test/pleroma/web/mastodon_api/views/instance_view_test.exs index 93b5c60c9..e6c8185c1 100644 --- a/test/pleroma/web/mastodon_api/views/instance_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/instance_view_test.exs @@ -34,12 +34,26 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do :timelines_access ] + # Used for filtering out values from configuration.json render in configuration2.json render + @configuration2_filter [ + {:accounts, [:max_pinned_statuses, :max_profile_fields, :profile_field_name_limit, :profile_field_value_limit]}, + {:statuses, [:characters_reserved_per_url]}, + {:urls, [:streaming, :status]}, + {:vapid, [:public_key]}, + {:translation, [:enabled]}, + {:timelines_access, [:live_feeds, :hashtag_feeds, :trending_link_feeds]} + ] + @pleroma_configuration_keys [ :metadata, :stats, :vapid_public_key ] + @pleroma_configuration2_filter [ + {:metadata, [:avatar_upload_limit, :background_upload_limit, :banner_upload_limit, :background_image, :chat_limit, :description_limit, :shout_limit]} + ] + @show_keys @common_information_keys ++ [ :uri, :description, @@ -126,6 +140,17 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do [:instance, :profile_directory] ] + # Filters out values from one layer deep nested Maps. + defp filter_render(%{}, [], acc), do: acc + + defp filter_render(input, filter, acc) do + {{filter_key, filters}, filter_rest} = List.pop_at(filter, 0) + {config_key_values, input_rest} = Map.pop!(input, filter_key) + filtered = Map.reject(config_key_values, fn {key, _} -> key not in filters end) + + filter_render(input_rest, filter_rest, Map.put(acc, filter_key, filtered)) + end + defp check_common_information(info) do filtered_info = Map.reject(info, fn {key, _value} -> key not in @common_information_keys end) @@ -197,33 +222,21 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do defp check_configuration2(info) do configuration = info[:configuration] - filtered_configuration = Map.reject(configuration, fn {key, _value} -> key not in @configuration2_keys end) + filtered_configuration = + configuration + |> Map.reject(fn {key, _value} -> key not in @configuration2_keys end) + |> filter_render(@configuration2_filter, %{}) - # configuration2 also includes parts from configuration expected = %{ accounts: %{ - max_featured_tags: 0, max_pinned_statuses: 1, max_profile_fields: 1, profile_field_name_limit: 1, profile_field_value_limit: 1 }, statuses: %{ - max_characters: 4096, - max_media_attachments: 1, characters_reserved_per_url: 0 }, - media_attachments: %{ - image_size_limit: 1024, - video_size_limit: 1024, - supported_mime_types: ["application/octet-stream"] - }, - polls: %{ - max_options: 4, - max_characters_per_option: 120, - min_expiration: 1, - max_expiration: 2 - }, urls: %{ streaming: Pleroma.Web.Endpoint.websocket_url(), status: "https://status.example.com" @@ -262,8 +275,9 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do defp check_pleroma_configuration(info) do configuration = info[:pleroma] filtered_configuration = Map.reject(configuration, fn {key, _value} -> key not in @pleroma_configuration_keys end) - metadata = Map.fetch!(filtered_configuration, :metadata) + # Tested elsewhere already + metadata = Map.fetch!(filtered_configuration, :metadata) filtered_metadata = Map.reject(metadata, fn {key, _value} -> key in [:features, :federation] end) filtered_configuration = %{filtered_configuration | metadata: filtered_metadata} @@ -304,38 +318,16 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do defp check_pleroma_configuration2(info) do configuration = info[:pleroma] filtered_configuration = Map.reject(configuration, fn {key, _value} -> key not in @pleroma_configuration_keys end) - metadata = Map.fetch!(filtered_configuration, :metadata) - # Tested elsewhere already - filtered_metadata = Map.reject(metadata, fn {key, _value} -> key in [:features, :federation] end) - filtered_configuration = %{filtered_configuration | metadata: filtered_metadata} - # pleroma_configuration2 also includes parts from pleroma_configuration + # Tested elsewhere already + metadata = Map.fetch!(filtered_configuration, :metadata) + filtered_metadata = Map.reject(metadata, fn {key, _value} -> key in [:features, :federation] end) + filtered_configuration = + %{filtered_configuration | metadata: filtered_metadata} + |> filter_render(@pleroma_configuration2_filter, %{}) + expected = %{ metadata: %{ - account_activation_required: true, - fields_limits: %{ - max_fields: 1, - max_remote_fields: 1, - name_length: 1, - value_length: 1 - }, - post_formats: ["text/plain"], - birthday_required: true, - birthday_min_age: 1, - translation: - %{ - source_languages: ["en", "pl"], - target_languages: ["en", "pl"] - }, - base_urls: %{ - media_proxy: "https://mediaproxy.example.com", - upload: "https://upload.example.com" - }, - markup: %{ - allow_inline_images: true, - allow_headings: true, - allow_tables: true - }, avatar_upload_limit: 1024, background_upload_limit: 1024, banner_upload_limit: 1024, @@ -343,9 +335,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do chat_limit: 120, description_limit: 120, shout_limit: 120 - }, - stats: %{mau: Pleroma.User.active_user_count()}, - vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) + } } Map.equal?(expected, filtered_configuration) From 9912b2d16af01d322a1e3a4eb78efbd6f42efdf5 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 26 Jun 2026 01:57:32 +0200 Subject: [PATCH 179/391] InstanceView: lint tests --- .../mastodon_api/views/instance_view_test.exs | 207 ++++++++++++------ 1 file changed, 136 insertions(+), 71 deletions(-) diff --git a/test/pleroma/web/mastodon_api/views/instance_view_test.exs b/test/pleroma/web/mastodon_api/views/instance_view_test.exs index e6c8185c1..cae704b90 100644 --- a/test/pleroma/web/mastodon_api/views/instance_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/instance_view_test.exs @@ -27,16 +27,23 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do :polls ] - @configuration2_keys @configuration_keys ++ [ - :urls, - :vapid, - :translation, - :timelines_access - ] + @configuration2_keys @configuration_keys ++ + [ + :urls, + :vapid, + :translation, + :timelines_access + ] # Used for filtering out values from configuration.json render in configuration2.json render @configuration2_filter [ - {:accounts, [:max_pinned_statuses, :max_profile_fields, :profile_field_name_limit, :profile_field_value_limit]}, + {:accounts, + [ + :max_pinned_statuses, + :max_profile_fields, + :profile_field_name_limit, + :profile_field_value_limit + ]}, {:statuses, [:characters_reserved_per_url]}, {:urls, [:streaming, :status]}, {:vapid, [:public_key]}, @@ -51,48 +58,59 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do ] @pleroma_configuration2_filter [ - {:metadata, [:avatar_upload_limit, :background_upload_limit, :banner_upload_limit, :background_image, :chat_limit, :description_limit, :shout_limit]} + {:metadata, + [ + :avatar_upload_limit, + :background_upload_limit, + :banner_upload_limit, + :background_image, + :chat_limit, + :description_limit, + :shout_limit + ]} ] - @show_keys @common_information_keys ++ [ - :uri, - :description, - :short_description, - :email, - :urls, - :stats, - :thumbnail, - :registrations, - :approval_required, - :contact_account, - :configuration, - # Extra (not present in Mastodon) - :max_toot_chars, - :max_media_attachments, - :poll_limits, - :upload_limit, - :avatar_upload_limit, - :background_upload_limit, - :banner_upload_limit, - :background_image, - :shout_limit, - :description_limit, - :chat_limit, - :pleroma - ] + @show_keys @common_information_keys ++ + [ + :uri, + :description, + :short_description, + :email, + :urls, + :stats, + :thumbnail, + :registrations, + :approval_required, + :contact_account, + :configuration, + # Extra (not present in Mastodon) + :max_toot_chars, + :max_media_attachments, + :poll_limits, + :upload_limit, + :avatar_upload_limit, + :background_upload_limit, + :banner_upload_limit, + :background_image, + :shout_limit, + :description_limit, + :chat_limit, + :pleroma + ] - @show2_keys @common_information_keys ++ [ - :domain, - :source_url, - :description, - :usage, - :thumbnail, - :configuration, - :registrations, - :contact, - # Extra (not present in Mastodon) - :pleroma - ] + @show2_keys @common_information_keys ++ + [ + :domain, + :source_url, + :description, + :usage, + :thumbnail, + :configuration, + :registrations, + :contact, + # Extra (not present in Mastodon) + :pleroma + ] @features [ "pleroma_api", @@ -158,7 +176,9 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do languages: ["en", "cs"], rules: InstanceView.render("rules.json", %{}), title: "test title", - version: InstanceView.mastodon_api_level() <> " (compatible; #{Pleroma.Application.named_version()})" + version: + InstanceView.mastodon_api_level() <> + " (compatible; #{Pleroma.Application.named_version()})" } Map.equal?(expected, filtered_info) @@ -194,7 +214,9 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do defp check_configuration(info) do configuration = info[:configuration] - filtered_configuration = Map.reject(configuration, fn {key, _value} -> key not in @configuration_keys end) + + filtered_configuration = + Map.reject(configuration, fn {key, _value} -> key not in @configuration_keys end) expected = %{ accounts: %{ @@ -222,6 +244,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do defp check_configuration2(info) do configuration = info[:configuration] + filtered_configuration = configuration |> Map.reject(fn {key, _value} -> key not in @configuration2_keys end) @@ -274,11 +297,16 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do defp check_pleroma_configuration(info) do configuration = info[:pleroma] - filtered_configuration = Map.reject(configuration, fn {key, _value} -> key not in @pleroma_configuration_keys end) + + filtered_configuration = + Map.reject(configuration, fn {key, _value} -> key not in @pleroma_configuration_keys end) # Tested elsewhere already metadata = Map.fetch!(filtered_configuration, :metadata) - filtered_metadata = Map.reject(metadata, fn {key, _value} -> key in [:features, :federation] end) + + filtered_metadata = + Map.reject(metadata, fn {key, _value} -> key in [:features, :federation] end) + filtered_configuration = %{filtered_configuration | metadata: filtered_metadata} expected = %{ @@ -293,19 +321,18 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do post_formats: ["text/plain"], birthday_required: true, birthday_min_age: 1, - translation: - %{ - source_languages: ["en", "pl"], - target_languages: ["en", "pl"] - }, + translation: %{ + source_languages: ["en", "pl"], + target_languages: ["en", "pl"] + }, base_urls: %{ media_proxy: "https://mediaproxy.example.com", upload: "https://upload.example.com" }, markup: %{ - allow_inline_images: true, - allow_headings: true, - allow_tables: true + allow_inline_images: true, + allow_headings: true, + allow_tables: true } }, stats: %{mau: Pleroma.User.active_user_count()}, @@ -317,11 +344,16 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do defp check_pleroma_configuration2(info) do configuration = info[:pleroma] - filtered_configuration = Map.reject(configuration, fn {key, _value} -> key not in @pleroma_configuration_keys end) + + filtered_configuration = + Map.reject(configuration, fn {key, _value} -> key not in @pleroma_configuration_keys end) # Tested elsewhere already metadata = Map.fetch!(filtered_configuration, :metadata) - filtered_metadata = Map.reject(metadata, fn {key, _value} -> key in [:features, :federation] end) + + filtered_metadata = + Map.reject(metadata, fn {key, _value} -> key in [:features, :federation] end) + filtered_configuration = %{filtered_configuration | metadata: filtered_metadata} |> filter_render(@pleroma_configuration2_filter, %{}) @@ -386,14 +418,26 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do output = InstanceView.federation() assert %{quarantined_instances: ["quarantine.example.com"]} = output - assert %{quarantined_instances_info: %{"quarantined_instances" => %{"quarantine.example.com" => %{"reason" => "crimes"}}}} = output + + assert %{ + quarantined_instances_info: %{ + "quarantined_instances" => %{"quarantine.example.com" => %{"reason" => "crimes"}} + } + } = output end test "rejected instances" do - clear_config([:instance, :rejected_instances], [{"rejected.example.com", "not enough #cofe posting"}]) + clear_config([:instance, :rejected_instances], [ + {"rejected.example.com", "not enough #cofe posting"} + ]) output = InstanceView.federation() - assert %{rejected_instances: %{"rejected.example.com" => %{"reason" => "not enough #cofe posting"}}} = output + + assert %{ + rejected_instances: %{ + "rejected.example.com" => %{"reason" => "not enough #cofe posting"} + } + } = output end test "federating" do @@ -405,29 +449,39 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do test "transparency" do clear_config([:instance, :quarantined_instances], [{"quarantine.example.com", "crimes"}]) - clear_config([:instance, :rejected_instances], [{"rejected.example.com", "not enough #cofe posting"}]) clear_config([:instance, :federating], true) clear_config([:mrf, :transparency], false) + clear_config([:instance, :rejected_instances], [ + {"rejected.example.com", "not enough #cofe posting"} + ]) + output = InstanceView.federation() assert %{enabled: true} == output end test "MRFs" do clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy]) - clear_config([:mrf, :transparency_exclusions], [{"media1.example.com", "the Fediverse doesn't need to know"}]) - clear_config([:mrf_simple, :media_removal], [{"media1.example.com", "NSFW"}, {"media2.example.com", "usual suspects"}]) clear_config([:mrf_simple, :reject], [{"rejected.example.com", "not enough #cofe posting"}]) + clear_config([:mrf, :transparency_exclusions], [ + {"media1.example.com", "the Fediverse doesn't need to know"} + ]) + + clear_config([:mrf_simple, :media_removal], [ + {"media1.example.com", "NSFW"}, + {"media2.example.com", "usual suspects"} + ]) + output = InstanceView.federation() expected = %{ mrf_simple: %{ reject: ["rejected.example.com"], - media_removal: ["media2.example.com"], + media_removal: ["media2.example.com"] }, mrf_hashtag: %{ - sensitive: ["nsfw"], + sensitive: ["nsfw"] }, exclusions: true, mrf_policies: ["SimplePolicy", "HashtagPolicy"], @@ -569,10 +623,21 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do describe "render domain_blocks.json" do setup do clear_config([:mrf, :transparency], true) - clear_config([:mrf, :transparency_exclusions], [{"removed1.example.com", "the Fediverse doesn't need to know"}]) clear_config([:mrf_simple, :media_removal], [{"media1.example.com", "NSFW"}]) - clear_config([:mrf_simple, :reject], [{"rejected.example.com", "not enough #cofe posting"}, {"rejected-without-reason.example.com", ""}]) - clear_config([:mrf_simple, :federated_timeline_removal], [{"removed1.example.com", "spam"}, {"removed2.example.com", "more spam"}]) + + clear_config([:mrf, :transparency_exclusions], [ + {"removed1.example.com", "the Fediverse doesn't need to know"} + ]) + + clear_config([:mrf_simple, :reject], [ + {"rejected.example.com", "not enough #cofe posting"}, + {"rejected-without-reason.example.com", ""} + ]) + + clear_config([:mrf_simple, :federated_timeline_removal], [ + {"removed1.example.com", "spam"}, + {"removed2.example.com", "more spam"} + ]) :ok end From 6c79cc792f856027fb8c1e9372ede341272d166f Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 27 Jun 2026 22:41:45 +0200 Subject: [PATCH 180/391] InstanceView: improve filtering in tests, simplify --- .../mastodon_api/views/instance_view_test.exs | 108 +++++++----------- 1 file changed, 42 insertions(+), 66 deletions(-) diff --git a/test/pleroma/web/mastodon_api/views/instance_view_test.exs b/test/pleroma/web/mastodon_api/views/instance_view_test.exs index cae704b90..7bdd9a126 100644 --- a/test/pleroma/web/mastodon_api/views/instance_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/instance_view_test.exs @@ -20,21 +20,6 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do :version ] - @configuration_keys [ - :accounts, - :statuses, - :media_attachments, - :polls - ] - - @configuration2_keys @configuration_keys ++ - [ - :urls, - :vapid, - :translation, - :timelines_access - ] - # Used for filtering out values from configuration.json render in configuration2.json render @configuration2_filter [ {:accounts, @@ -51,12 +36,6 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do {:timelines_access, [:live_feeds, :hashtag_feeds, :trending_link_feeds]} ] - @pleroma_configuration_keys [ - :metadata, - :stats, - :vapid_public_key - ] - @pleroma_configuration2_filter [ {:metadata, [ @@ -159,14 +138,37 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do ] # Filters out values from one layer deep nested Maps. - defp filter_render(%{}, [], acc), do: acc + # Filtering types: + # - :inclusive -> filters out everything that is not in the filters + # - :discriminative -> filters out what is in the filters + defp filter_render(input, filter, type, acc) - defp filter_render(input, filter, acc) do - {{filter_key, filters}, filter_rest} = List.pop_at(filter, 0) - {config_key_values, input_rest} = Map.pop!(input, filter_key) - filtered = Map.reject(config_key_values, fn {key, _} -> key not in filters end) + defp filter_render(_input_rest, [], type, acc) when type == :inclusive, do: acc - filter_render(input_rest, filter_rest, Map.put(acc, filter_key, filtered)) + defp filter_render(input_rest, [], type, acc) when type == :discriminative, + do: Map.merge(input_rest, acc) + + defp filter_render(input, filter, type, acc) do + {current_filter, filter_rest} = List.pop_at(filter, 0) + + case current_filter do + nil -> + filter_render(input, filter_rest, type, acc) + + {filter_key, filters} -> + {config_key_values, input_rest} = Map.pop!(input, filter_key) + + filtered = + case type do + :inclusive -> + Map.reject(config_key_values, fn {key, _} -> key not in filters end) + + :discriminative -> + Map.reject(config_key_values, fn {key, _} -> key in filters end) + end + + filter_render(input_rest, filter_rest, type, Map.put(acc, filter_key, filtered)) + end end defp check_common_information(info) do @@ -215,9 +217,6 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do defp check_configuration(info) do configuration = info[:configuration] - filtered_configuration = - Map.reject(configuration, fn {key, _value} -> key not in @configuration_keys end) - expected = %{ accounts: %{ max_featured_tags: 0 @@ -239,16 +238,13 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do } } - Map.equal?(expected, filtered_configuration) + Map.equal?(expected, configuration) end defp check_configuration2(info) do - configuration = info[:configuration] - - filtered_configuration = - configuration - |> Map.reject(fn {key, _value} -> key not in @configuration2_keys end) - |> filter_render(@configuration2_filter, %{}) + configuration = + info[:configuration] + |> filter_render(@configuration2_filter, :inclusive, %{}) expected = %{ accounts: %{ @@ -279,7 +275,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do } } - Map.equal?(expected, filtered_configuration) + Map.equal?(expected, configuration) end defp check_registrations(info) do @@ -296,18 +292,9 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do end defp check_pleroma_configuration(info) do - configuration = info[:pleroma] - - filtered_configuration = - Map.reject(configuration, fn {key, _value} -> key not in @pleroma_configuration_keys end) - - # Tested elsewhere already - metadata = Map.fetch!(filtered_configuration, :metadata) - - filtered_metadata = - Map.reject(metadata, fn {key, _value} -> key in [:features, :federation] end) - - filtered_configuration = %{filtered_configuration | metadata: filtered_metadata} + configuration = + info[:pleroma] + |> filter_render([{:metadata, [:features, :federation]}], :discriminative, %{}) expected = %{ metadata: %{ @@ -339,24 +326,13 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) } - Map.equal?(expected, filtered_configuration) + Map.equal?(expected, configuration) end defp check_pleroma_configuration2(info) do - configuration = info[:pleroma] - - filtered_configuration = - Map.reject(configuration, fn {key, _value} -> key not in @pleroma_configuration_keys end) - - # Tested elsewhere already - metadata = Map.fetch!(filtered_configuration, :metadata) - - filtered_metadata = - Map.reject(metadata, fn {key, _value} -> key in [:features, :federation] end) - - filtered_configuration = - %{filtered_configuration | metadata: filtered_metadata} - |> filter_render(@pleroma_configuration2_filter, %{}) + configuration = + info[:pleroma] + |> filter_render(@pleroma_configuration2_filter, :inclusive, %{}) expected = %{ metadata: %{ @@ -370,7 +346,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do } } - Map.equal?(expected, filtered_configuration) + Map.equal?(expected, configuration) end # When this fails, a new feature flag was probably added. Add it to the macros above. From ef321846d4bc4860789ce24edeeeda6384fede67 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 27 Jun 2026 22:44:59 +0200 Subject: [PATCH 181/391] InstanceView: Use attribute for pleroma_configuration filter in test --- test/pleroma/web/mastodon_api/views/instance_view_test.exs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/pleroma/web/mastodon_api/views/instance_view_test.exs b/test/pleroma/web/mastodon_api/views/instance_view_test.exs index 7bdd9a126..f7d058365 100644 --- a/test/pleroma/web/mastodon_api/views/instance_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/instance_view_test.exs @@ -36,6 +36,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do {:timelines_access, [:live_feeds, :hashtag_feeds, :trending_link_feeds]} ] + @pleroma_configuration_filter [{:metadata, [:features, :federation]}] + @pleroma_configuration2_filter [ {:metadata, [ @@ -294,7 +296,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do defp check_pleroma_configuration(info) do configuration = info[:pleroma] - |> filter_render([{:metadata, [:features, :federation]}], :discriminative, %{}) + |> filter_render(@pleroma_configuration_filter, :discriminative, %{}) expected = %{ metadata: %{ From 6e03f1d7898b8b9cc5ccfe40d81954ef91d24f5c Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 27 Jun 2026 23:34:01 +0200 Subject: [PATCH 182/391] InstanceView: Tighten show.json render test, rename show2-only funcs --- .../mastodon_api/views/instance_view_test.exs | 53 ++++++++++++++++--- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/test/pleroma/web/mastodon_api/views/instance_view_test.exs b/test/pleroma/web/mastodon_api/views/instance_view_test.exs index f7d058365..b0ef4b659 100644 --- a/test/pleroma/web/mastodon_api/views/instance_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/instance_view_test.exs @@ -79,6 +79,9 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do :pleroma ] + # Checked in other check_* functions + @show_filter @common_information_keys ++ [:contact_account, :configuration, :pleroma] + @show2_keys @common_information_keys ++ [ :domain, @@ -188,7 +191,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do Map.equal?(expected, filtered_info) end - defp check_thumbnail(info) do + defp check_thumbnail2(info) do thumbnail = info[:thumbnail] expected = %{ @@ -209,7 +212,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do Map.equal?(expected, contact) end - defp check_contact_name(info, user) do + defp check_contact_account(info, user) do contact = info[:contact_account] view = Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user, for: nil}) @@ -280,7 +283,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do Map.equal?(expected, configuration) end - defp check_registrations(info) do + defp check_registrations2(info) do registrations = info[:registrations] expected = %{ @@ -351,6 +354,43 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do Map.equal?(expected, configuration) end + defp check_show(info) do + filter = @show_filter + filtered_info = Map.reject(info, fn {key, _value} -> key in filter end) + + expected = %{ + uri: Pleroma.Web.WebFinger.host(), + description: "Pleroma: An efficient and flexible fediverse server", + short_description: "Corndog Emporium!", + email: "nonexist@example.com", + urls: %{ + streaming_api: Pleroma.Web.Endpoint.websocket_url(), + }, + stats: Pleroma.Stats.get_stats(), + thumbnail: "https://example.com/thumbnail.png", + registrations: false, + approval_required: true, + max_toot_chars: 4096, + max_media_attachments: 1, + poll_limits: %{ + max_options: 4, + max_option_chars: 120, + min_expiration: 1, + max_expiration: 2 + }, + upload_limit: 1024, + avatar_upload_limit: 1024, + background_upload_limit: 1024, + banner_upload_limit: 1024, + background_image: Pleroma.Web.Endpoint.url() <> "/media/background.png", + shout_limit: 120, + description_limit: 120, + chat_limit: 120, + } + + Map.equal?(expected, filtered_info) + end + # When this fails, a new feature flag was probably added. Add it to the macros above. describe "uncofigurable features" do setup do: Enum.each(@configurable_features_flags, &clear_config(&1, false)) @@ -545,7 +585,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do expected_keys = Enum.sort(@show_keys) assert check_common_information(output) - assert check_contact_name(output, user) + assert check_show(output) + assert check_contact_account(output, user) assert check_configuration(output) assert check_pleroma_configuration(output) assert expected_keys == Enum.sort(Map.keys(output)) @@ -556,9 +597,9 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do expected_keys = Enum.sort(@show2_keys) assert check_common_information(output) - assert check_thumbnail(output) + assert check_thumbnail2(output) assert check_configuration2(output) - assert check_registrations(output) + assert check_registrations2(output) assert check_contact(output, user) assert check_pleroma_configuration2(output) assert expected_keys == Enum.sort(Map.keys(output)) From d19a9f768c996c845222805d6d6dd9b648580d95 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 27 Jun 2026 23:39:48 +0200 Subject: [PATCH 183/391] InstanceView: Tighten show2.json --- .../mastodon_api/views/instance_view_test.exs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/pleroma/web/mastodon_api/views/instance_view_test.exs b/test/pleroma/web/mastodon_api/views/instance_view_test.exs index b0ef4b659..3f7c83968 100644 --- a/test/pleroma/web/mastodon_api/views/instance_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/instance_view_test.exs @@ -96,6 +96,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do :pleroma ] + @show2_filter @common_information_keys ++ [:thumbnail, :configuration, :registrations, :contact, :pleroma] + @features [ "pleroma_api", "mastodon_api", @@ -391,6 +393,20 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do Map.equal?(expected, filtered_info) end + defp check_show2(info) do + filter = @show2_filter + filtered_info = Map.reject(info, fn {key, _value} -> key in filter end) + + expected = %{ + domain: Pleroma.Web.WebFinger.host(), + source_url: Pleroma.Application.repository(), + description: "Corndog Emporium!", + usage: %{users: %{active_month: Pleroma.User.active_user_count()}} + } + + Map.equal?(expected, filtered_info) + end + # When this fails, a new feature flag was probably added. Add it to the macros above. describe "uncofigurable features" do setup do: Enum.each(@configurable_features_flags, &clear_config(&1, false)) @@ -597,6 +613,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do expected_keys = Enum.sort(@show2_keys) assert check_common_information(output) + assert check_show2(output) assert check_thumbnail2(output) assert check_configuration2(output) assert check_registrations2(output) From 3027052b0dbc042af45135b041754b05ffe4893d Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sun, 28 Jun 2026 00:17:28 +0200 Subject: [PATCH 184/391] InstanceView: Remove wall of changed configs in show render tests. Switched to using config defaults instead. --- .../mastodon_api/views/instance_view_test.exs | 180 +++++++----------- 1 file changed, 70 insertions(+), 110 deletions(-) diff --git a/test/pleroma/web/mastodon_api/views/instance_view_test.exs b/test/pleroma/web/mastodon_api/views/instance_view_test.exs index 3f7c83968..6acfbab99 100644 --- a/test/pleroma/web/mastodon_api/views/instance_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/instance_view_test.exs @@ -182,9 +182,9 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do filtered_info = Map.reject(info, fn {key, _value} -> key not in @common_information_keys end) expected = %{ - languages: ["en", "cs"], + languages: ["en"], rules: InstanceView.render("rules.json", %{}), - title: "test title", + title: "Pleroma", version: InstanceView.mastodon_api_level() <> " (compatible; #{Pleroma.Application.named_version()})" @@ -197,7 +197,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do thumbnail = info[:thumbnail] expected = %{ - url: URI.merge(Pleroma.Web.Endpoint.url(), "https://example.com/thumbnail.png") |> to_string + url: URI.merge(Pleroma.Web.Endpoint.url(), "http://localhost:4001/instance/thumbnail.jpeg") |> to_string } expected == thumbnail @@ -207,7 +207,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do contact = info[:contact] expected = %{ - email: "nonexist@example.com", + email: "admin@example.com", account: Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user, for: nil}) } @@ -229,19 +229,19 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do max_featured_tags: 0 }, statuses: %{ - max_characters: 4096, - max_media_attachments: 1 + max_characters: 5_000, + max_media_attachments: 1_000 }, media_attachments: %{ - image_size_limit: 1024, - video_size_limit: 1024, + image_size_limit: 16_000_000, + video_size_limit: 16_000_000, supported_mime_types: ["application/octet-stream"] }, polls: %{ - max_options: 4, - max_characters_per_option: 120, - min_expiration: 1, - max_expiration: 2 + max_options: 20, + max_characters_per_option: 200, + min_expiration: 0, + max_expiration: 365 * 24 * 60 * 60 } } @@ -256,24 +256,24 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do expected = %{ accounts: %{ max_pinned_statuses: 1, - max_profile_fields: 1, - profile_field_name_limit: 1, - profile_field_value_limit: 1 + max_profile_fields: 10, + profile_field_name_limit: 512, + profile_field_value_limit: 2048 }, statuses: %{ characters_reserved_per_url: 0 }, urls: %{ streaming: Pleroma.Web.Endpoint.websocket_url(), - status: "https://status.example.com" + status: nil }, vapid: %{ public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) }, translation: %{enabled: true}, timelines_access: %{ - live_feeds: %{local: "authenticated", remote: "authenticated"}, - hashtag_feeds: %{local: "authenticated", remote: "authenticated"}, + live_feeds: %{local: "public", remote: "public"}, + hashtag_feeds: %{local: "public", remote: "public"}, # not implemented in Pleroma trending_link_feeds: %{ local: "disabled", @@ -289,8 +289,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do registrations = info[:registrations] expected = %{ - enabled: false, - approval_required: true, + enabled: true, + approval_required: false, message: nil, url: nil } @@ -305,28 +305,31 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do expected = %{ metadata: %{ - account_activation_required: true, + account_activation_required: false, fields_limits: %{ - max_fields: 1, - max_remote_fields: 1, - name_length: 1, - value_length: 1 + max_fields: 10, + max_remote_fields: 20, + name_length: 512, + value_length: 2048 }, - post_formats: ["text/plain"], - birthday_required: true, - birthday_min_age: 1, + post_formats: [ + "text/plain", + "text/html", + "text/markdown", + "text/bbcode", + "text/x.misskeymarkdown" + ], + birthday_required: false, + birthday_min_age: 0, translation: %{ source_languages: ["en", "pl"], target_languages: ["en", "pl"] }, - base_urls: %{ - media_proxy: "https://mediaproxy.example.com", - upload: "https://upload.example.com" - }, + base_urls: %{}, markup: %{ allow_inline_images: true, - allow_headings: true, - allow_tables: true + allow_headings: false, + allow_tables: false } }, stats: %{mau: Pleroma.User.active_user_count()}, @@ -343,13 +346,13 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do expected = %{ metadata: %{ - avatar_upload_limit: 1024, - background_upload_limit: 1024, - banner_upload_limit: 1024, - background_image: Pleroma.Web.Endpoint.url() <> "/media/background.png", - chat_limit: 120, - description_limit: 120, - shout_limit: 120 + avatar_upload_limit: 2_000_000, + background_upload_limit: 4_000_000, + banner_upload_limit: 4_000_000, + background_image: Pleroma.Web.Endpoint.url() <> "/images/city.jpg", + chat_limit: 5_000, + description_limit: 5_000, + shout_limit: 5_000 } } @@ -363,31 +366,31 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do expected = %{ uri: Pleroma.Web.WebFinger.host(), description: "Pleroma: An efficient and flexible fediverse server", - short_description: "Corndog Emporium!", - email: "nonexist@example.com", + short_description: "", + email: "admin@example.com", urls: %{ streaming_api: Pleroma.Web.Endpoint.websocket_url(), }, stats: Pleroma.Stats.get_stats(), - thumbnail: "https://example.com/thumbnail.png", - registrations: false, - approval_required: true, - max_toot_chars: 4096, - max_media_attachments: 1, + thumbnail: "http://localhost:4001/instance/thumbnail.jpeg", + registrations: true, + approval_required: false, + max_toot_chars: 5_000, + max_media_attachments: 1_000, poll_limits: %{ - max_options: 4, - max_option_chars: 120, - min_expiration: 1, - max_expiration: 2 + max_options: 20, + max_option_chars: 200, + min_expiration: 0, + max_expiration: 365 * 24 * 60 * 60 }, - upload_limit: 1024, - avatar_upload_limit: 1024, - background_upload_limit: 1024, - banner_upload_limit: 1024, - background_image: Pleroma.Web.Endpoint.url() <> "/media/background.png", - shout_limit: 120, - description_limit: 120, - chat_limit: 120, + upload_limit: 16_000_000, + avatar_upload_limit: 2_000_000, + background_upload_limit: 4_000_000, + banner_upload_limit: 4_000_000, + background_image: Pleroma.Web.Endpoint.url() <> "/images/city.jpg", + shout_limit: 5_000, + description_limit: 5_000, + chat_limit: 5_000, } Map.equal?(expected, filtered_info) @@ -400,7 +403,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do expected = %{ domain: Pleroma.Web.WebFinger.host(), source_url: Pleroma.Application.repository(), - description: "Corndog Emporium!", + description: "", usage: %{users: %{active_month: Pleroma.User.active_user_count()}} } @@ -541,57 +544,14 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do user = insert(:user) - # Wall of configs for testing whether they are rendered properly + # contact in show2.json + clear_config([:instance, :contact_username], user.nickname) + + # Pleroma.Language.Translation.configured?() -> true clear_config([Pleroma.Language.Translation, :provider], TranslationMock) - clear_config([:instance, :languages], ["en", "cs"]) - clear_config([:instance, :name], "test title") - clear_config([:instance, :contact_username], user.nickname) - clear_config([:instance, :limit], 4096) - clear_config([:instance, :max_media_attachments], 1) - clear_config([:instance, :upload_limit], 1024) - clear_config([:instance, :poll_limits, :max_options], 4) - clear_config([:instance, :poll_limits, :max_option_chars], 120) - clear_config([:instance, :poll_limits, :min_expiration], 1) - clear_config([:instance, :poll_limits, :max_expiration], 2) - clear_config([:instance, :account_activation_required], true) - clear_config([:instance, :max_account_fields], 1) - clear_config([:instance, :max_remote_account_fields], 1) - clear_config([:instance, :account_field_name_length], 1) - clear_config([:instance, :account_field_value_length], 1) - clear_config([:instance, :allowed_post_formats], ["text/plain"]) - clear_config([:instance, :birthday_required], true) - clear_config([:instance, :birthday_min_age], 1) - - clear_config([:instance, :short_description], "Corndog Emporium!") - clear_config([:instance, :instance_thumbnail], "https://example.com/thumbnail.png") - clear_config([:instance, :registrations_open], false) - clear_config([:instance, :account_approval_required], true) - - clear_config([:instance, :max_pinned_statuses], 1) - clear_config([:instance, :status_page], "https://status.example.com") - - clear_config([:instance, :email], "nonexist@example.com") - - clear_config([:instance, :avatar_upload_limit], 1024) - clear_config([:instance, :background_upload_limit], 1024) - clear_config([:instance, :banner_upload_limit], 1024) - clear_config([:instance, :background_image], "/media/background.png") - clear_config([:instance, :chat_limit], 120) - clear_config([:instance, :description_limit], 120) - - clear_config([:media_proxy, :enabled], true) - clear_config([:media_proxy, :base_url], "https://mediaproxy.example.com") - - clear_config([Pleroma.Upload, :base_url], "https://upload.example.com") - - clear_config([:markup, :allow_inline_images], true) - clear_config([:markup, :allow_headings], true) - clear_config([:markup, :allow_tables], true) - - clear_config([:restrict_unauthenticated, :timelines], %{local: true, federated: true}) - - clear_config([:shout, :limit], 120) + # Deprecated, but still used in view + clear_config([:instance, :chat_limit], 5_000) %{user: user} end From dd070883778107e2dba73821ceb66aeefb3d6a82 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sun, 28 Jun 2026 00:19:38 +0200 Subject: [PATCH 185/391] InstanceView: lint tests --- .../mastodon_api/views/instance_view_test.exs | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/test/pleroma/web/mastodon_api/views/instance_view_test.exs b/test/pleroma/web/mastodon_api/views/instance_view_test.exs index 6acfbab99..8f066a6f5 100644 --- a/test/pleroma/web/mastodon_api/views/instance_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/instance_view_test.exs @@ -96,7 +96,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do :pleroma ] - @show2_filter @common_information_keys ++ [:thumbnail, :configuration, :registrations, :contact, :pleroma] + @show2_filter @common_information_keys ++ + [:thumbnail, :configuration, :registrations, :contact, :pleroma] @features [ "pleroma_api", @@ -197,7 +198,9 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do thumbnail = info[:thumbnail] expected = %{ - url: URI.merge(Pleroma.Web.Endpoint.url(), "http://localhost:4001/instance/thumbnail.jpeg") |> to_string + url: + URI.merge(Pleroma.Web.Endpoint.url(), "http://localhost:4001/instance/thumbnail.jpeg") + |> to_string } expected == thumbnail @@ -313,11 +316,11 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do value_length: 2048 }, post_formats: [ - "text/plain", - "text/html", - "text/markdown", - "text/bbcode", - "text/x.misskeymarkdown" + "text/plain", + "text/html", + "text/markdown", + "text/bbcode", + "text/x.misskeymarkdown" ], birthday_required: false, birthday_min_age: 0, @@ -369,7 +372,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do short_description: "", email: "admin@example.com", urls: %{ - streaming_api: Pleroma.Web.Endpoint.websocket_url(), + streaming_api: Pleroma.Web.Endpoint.websocket_url() }, stats: Pleroma.Stats.get_stats(), thumbnail: "http://localhost:4001/instance/thumbnail.jpeg", @@ -390,7 +393,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do background_image: Pleroma.Web.Endpoint.url() <> "/images/city.jpg", shout_limit: 5_000, description_limit: 5_000, - chat_limit: 5_000, + chat_limit: 5_000 } Map.equal?(expected, filtered_info) From 964feb1e470ba666b70c9acd3d329043a849e030 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sun, 28 Jun 2026 00:36:43 +0200 Subject: [PATCH 186/391] InstanceView: Remove unreachable guards in filter_render --- .../mastodon_api/views/instance_view_test.exs | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/test/pleroma/web/mastodon_api/views/instance_view_test.exs b/test/pleroma/web/mastodon_api/views/instance_view_test.exs index 8f066a6f5..66485be0e 100644 --- a/test/pleroma/web/mastodon_api/views/instance_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/instance_view_test.exs @@ -157,26 +157,19 @@ defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do do: Map.merge(input_rest, acc) defp filter_render(input, filter, type, acc) do - {current_filter, filter_rest} = List.pop_at(filter, 0) + {{filter_key, filters}, filter_rest} = List.pop_at(filter, 0) + {config_key_values, input_rest} = Map.pop!(input, filter_key) - case current_filter do - nil -> - filter_render(input, filter_rest, type, acc) + filtered = + case type do + :inclusive -> + Map.reject(config_key_values, fn {key, _} -> key not in filters end) - {filter_key, filters} -> - {config_key_values, input_rest} = Map.pop!(input, filter_key) + :discriminative -> + Map.reject(config_key_values, fn {key, _} -> key in filters end) + end - filtered = - case type do - :inclusive -> - Map.reject(config_key_values, fn {key, _} -> key not in filters end) - - :discriminative -> - Map.reject(config_key_values, fn {key, _} -> key in filters end) - end - - filter_render(input_rest, filter_rest, type, Map.put(acc, filter_key, filtered)) - end + filter_render(input_rest, filter_rest, type, Map.put(acc, filter_key, filtered)) end defp check_common_information(info) do From a020b8a1901720238e8749d8487ddb218b82472b Mon Sep 17 00:00:00 2001 From: Phantasm Date: Mon, 29 Jun 2026 23:47:43 +0200 Subject: [PATCH 187/391] Elixir 1.19: Ensure default primary logger configg level is debug Setting the default handler and ex_syslogger handler levels does not necessarily have to set the primary logger log level, at least in the future. Without this, the primary log level might be lower than the configured console/syslog levels, which would cause some desired logs to not be logged. --- config/config.exs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/config/config.exs b/config/config.exs index 37a2590ca..80183a3d9 100644 --- a/config/config.exs +++ b/config/config.exs @@ -133,12 +133,17 @@ config :pleroma, Pleroma.Web.Endpoint, ] # Configures Elixir's Logger +# Primary config +config :logger, level: :debug + +# Console config config :logger, :default_handler, level: :debug config :logger, :default_formatter, format: "\n$time $metadata[$level] $message\n", metadata: [:actor, :path, :type, :user] +# Syslog config config :logger, :ex_syslogger, level: :debug, ident: "pleroma", From 7d9175350d0f2ce715048ac1795ec4790d192c3b Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 30 Jun 2026 00:24:48 +0200 Subject: [PATCH 188/391] Elixir 1.19: Completely overwrite formatter for default handler :logger.update_handler_config/3 can merge configs, :logger.set_handler_config/3 instead resets the rest to defaults. --- lib/pleroma/config/deprecation_warnings.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex index e77785483..8f8786ee7 100644 --- a/lib/pleroma/config/deprecation_warnings.ex +++ b/lib/pleroma/config/deprecation_warnings.ex @@ -435,7 +435,7 @@ defmodule Pleroma.Config.DeprecationWarnings do """) if log_level, do: :logger.update_handler_config(:default, :level, log_level) - :logger.update_handler_config(:default, :formatter, formatter) + :logger.set_handler_config(:default, :formatter, formatter) end end From 7e54b0935b1b9199310467c6632c1831b0fa06a8 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 2 Jul 2026 10:20:59 +0400 Subject: [PATCH 189/391] chore(deps): add mix audit --- changelog.d/mix-audit.add | 1 + mix.exs | 1 + mix.lock | 3 +++ 3 files changed, 5 insertions(+) create mode 100644 changelog.d/mix-audit.add diff --git a/changelog.d/mix-audit.add b/changelog.d/mix-audit.add new file mode 100644 index 000000000..b87fcdc67 --- /dev/null +++ b/changelog.d/mix-audit.add @@ -0,0 +1 @@ +Add mix_audit for dependency vulnerability checks. diff --git a/mix.exs b/mix.exs index 3d525b321..f20c5d303 100644 --- a/mix.exs +++ b/mix.exs @@ -204,6 +204,7 @@ defmodule Pleroma.Mixfile do {:ex_doc, "~> 0.38", only: :dev, runtime: false}, {:ex_machina, "~> 2.8", only: :test}, {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, + {:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false}, {:mock, "~> 0.3.9", only: :test}, {:covertool, "~> 2.0", only: :test}, {:hackney, "~> 1.20.1", override: true}, diff --git a/mix.lock b/mix.lock index cde864084..961f1c6dd 100644 --- a/mix.lock +++ b/mix.lock @@ -82,6 +82,7 @@ "mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"}, "mimerl": {:hex, :mimerl, "1.5.0", "f35aca6f23242339b3666e0ac0702379e362b469d0aea167f6cc713547e777ed", [:rebar3], [], "hexpm", "db648ce065bae14ea84ca8b5dd123f42f49417cef693541110bf6f9e9be9ecc4"}, "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, + "mix_audit": {:hex, :mix_audit, "2.1.5", "c0f77cee6b4ef9d97e37772359a187a166c7a1e0e08b50edf5bf6959dfe5a016", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "87f9298e21da32f697af535475860dc1d3617a010e0b418d2ec6142bc8b42d69"}, "mochiweb": {:hex, :mochiweb, "2.18.0", "eb55f1db3e6e960fac4e6db4e2db9ec3602cc9f30b86cd1481d56545c3145d2e", [:rebar3], [], "hexpm"}, "mock": {:hex, :mock, "0.3.9", "10e44ad1f5962480c5c9b9fa779c6c63de9bd31997c8e04a853ec990a9d841af", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "9e1b244c4ca2551bb17bb8415eed89e40ee1308e0fbaed0a4fdfe3ec8a4adbd3"}, "mogrify": {:hex, :mogrify, "0.9.3", "238c782f00271dace01369ad35ae2e9dd020feee3443b9299ea5ea6bed559841", [:mix], [], "hexpm", "0189b1e1de27455f2b9ae8cf88239cefd23d38de9276eb5add7159aea51731e6"}, @@ -159,4 +160,6 @@ "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, "websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"}, + "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, + "yaml_elixir": {:hex, :yaml_elixir, "2.12.2", "9dd1330fb4cd9a36a7b0f502e5b12486eff632792ee4a5f0eba52a4d4ec32c9c", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "e7c1b10122f973e6558462d51c39026ba0e14afbc6745318e990ea82cfe9e159"}, } From fa7986a2676ed79f3590552f7679eb1bd86eaa38 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 2 Jul 2026 10:26:12 +0400 Subject: [PATCH 190/391] fix(deps): update bandit --- changelog.d/bandit-1.11.1.security | 1 + mix.exs | 2 +- mix.lock | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog.d/bandit-1.11.1.security diff --git a/changelog.d/bandit-1.11.1.security b/changelog.d/bandit-1.11.1.security new file mode 100644 index 000000000..604dc85b0 --- /dev/null +++ b/changelog.d/bandit-1.11.1.security @@ -0,0 +1 @@ +Update Bandit to address HTTP request handling vulnerabilities. diff --git a/mix.exs b/mix.exs index f20c5d303..8667a880b 100644 --- a/mix.exs +++ b/mix.exs @@ -192,7 +192,7 @@ defmodule Pleroma.Mixfile do {:elixir_make, "~> 0.7.8", override: true}, {:blurhash, "~> 0.1.0", hex: :rinpatch_blurhash}, {:exile, "~> 0.10.0"}, - {:bandit, "~> 1.10"}, + {:bandit, "~> 1.11.1"}, {:websock_adapter, "~> 0.5.8"}, {:oban_live_dashboard, "~> 0.1.1"}, {:multipart, "~> 0.4.0", optional: true}, diff --git a/mix.lock b/mix.lock index 961f1c6dd..1bee6b716 100644 --- a/mix.lock +++ b/mix.lock @@ -1,7 +1,7 @@ %{ "accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm", "11b18c220bcc2eab63b5470c038ef10eb6783bcb1fcdb11aa4137defa5ac1bb8"}, "argon2_elixir": {:hex, :argon2_elixir, "4.1.3", "4f28318286f89453364d7fbb53e03d4563fd7ed2438a60237eba5e426e97785f", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "7c295b8d8e0eaf6f43641698f962526cdf87c6feb7d14bd21e599271b510608c"}, - "bandit": {:hex, :bandit, "1.10.4", "02b9734c67c5916a008e7eb7e2ba68aaea6f8177094a5f8d95f1fb99069aac17", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "a5faf501042ac1f31d736d9d4a813b3db4ef812e634583b6a457b0928798a51d"}, + "bandit": {:hex, :bandit, "1.11.1", "1eb33123cc3c17ae0c3447874eb83399ee530f960c39711ed240342fbd4865fa", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d4401016df9abbc6dcd325c0b78b2b193e7c7c96bb68f31e576112be025d84a5"}, "base62": {:hex, :base62, "1.2.2", "85c6627eb609317b70f555294045895ffaaeb1758666ab9ef9ca38865b11e629", [:mix], [{:custom_base, "~> 0.2.1", [hex: :custom_base, repo: "hexpm", optional: false]}], "hexpm", "d41336bda8eaa5be197f1e4592400513ee60518e5b9f4dcf38f4b4dae6f377bb"}, "bbcode_pleroma": {:hex, :bbcode_pleroma, "0.2.0", "d36f5bca6e2f62261c45be30fa9b92725c0655ad45c99025cb1c3e28e25803ef", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "19851074419a5fedb4ef49e1f01b30df504bb5dbb6d6adfc135238063bebd1c3"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "2.3.1", "5114d780459a04f2b4aeef52307de23de961b69e13a5cd98a911e39fda13f420", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "42182d5f46764def15bf9af83739e3bf4ad22661b1c34fc3e88558efced07279"}, From f6cc4c144bfeb96fa0a7d074a5594e35776b343b Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 2 Jul 2026 10:27:21 +0400 Subject: [PATCH 191/391] fix(deps): update plug --- changelog.d/plug-1.20.2.security | 1 + mix.lock | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/plug-1.20.2.security diff --git a/changelog.d/plug-1.20.2.security b/changelog.d/plug-1.20.2.security new file mode 100644 index 000000000..1e0ab38f9 --- /dev/null +++ b/changelog.d/plug-1.20.2.security @@ -0,0 +1 @@ +Update Plug to address multipart header parsing denial-of-service vulnerabilities. diff --git a/mix.lock b/mix.lock index 1bee6b716..864f284a4 100644 --- a/mix.lock +++ b/mix.lock @@ -114,7 +114,7 @@ "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, "pleroma_mfm_parser": {:hex, :pleroma_mfm_parser, "0.2.1", "87a40fec4e2e035447df34e1c98e1b816b6a448d49baea73eb2273531ac5ce66", [:mix], [], "hexpm", "ec4845461da95d63ab3d592086d65114c45057a1f2d48d768f3def45b0d7696f"}, - "plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"}, + "plug": {:hex, :plug, "1.20.2", "adbee2441232412e37fbb357fd5e4cd533fdd253b29f2e1992262b0f1fb01462", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b16baf55877d60891002ffc1ce0b3ff7d6f30a38a23e02e4d4293c4ac266f136"}, "plug_cowboy": {:hex, :plug_cowboy, "2.7.4", "729c752d17cf364e2b8da5bdb34fb5804f56251e88bb602aff48ae0bd8673d11", [: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", "9b85632bd7012615bae0a5d70084deb1b25d2bcbb32cab82d1e9a1e023168aa3"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, "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"}, From dcc5a33f4d462f9080c4f100fc3062a431f73764 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 2 Jul 2026 10:31:34 +0400 Subject: [PATCH 192/391] fix(deps): update phoenix --- changelog.d/phoenix-1.8.8.security | 1 + mix.lock | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/phoenix-1.8.8.security diff --git a/changelog.d/phoenix-1.8.8.security b/changelog.d/phoenix-1.8.8.security new file mode 100644 index 000000000..f5be1eb8f --- /dev/null +++ b/changelog.d/phoenix-1.8.8.security @@ -0,0 +1 @@ +Update Phoenix to address long-poll NDJSON request denial-of-service vulnerabilities. diff --git a/mix.lock b/mix.lock index 864f284a4..468afd5e9 100644 --- a/mix.lock +++ b/mix.lock @@ -103,7 +103,7 @@ "open_api_spex": {:hex, :open_api_spex, "3.22.0", "fbf90dc82681dc042a4ee79853c8e989efbba73d9e87439085daf849bbf8bc20", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "dd751ddbdd709bb4a5313e9a24530da6e66594773c7242a0c2592cbd9f589063"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "1.2.1", "9cbe354b58121075bd20eb83076900a3832324b7dd171a6895fab57b6bb2752c", [:mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}], "hexpm", "d3b40a4a4630f0b442f19eca891fcfeeee4c40871936fed2f68e1c4faa30481f"}, - "phoenix": {:hex, :phoenix, "1.8.3", "49ac5e485083cb1495a905e47eb554277bdd9c65ccb4fc5100306b350151aa95", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, 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]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "36169f95cc2e155b78be93d9590acc3f462f1e5438db06e6248613f27c80caec"}, + "phoenix": {:hex, :phoenix, "1.8.8", "ada3d761359274178180c0e992ef0c2b536bd7c3bd75ebba94acbf39ab4347fe", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, 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]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "f0c843037bd2e7012fc1d1ec9574dfa6972b7e3d09e9b77fd23aa283af0aa994"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.5", "c4ef322acd15a574a8b1a08eff0ee0a85e73096b53ce1403b6563709f15e1cea", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "26ec3208eef407f31b748cadd044045c6fd485fbff168e35963d2f9dfff28d4b"}, "phoenix_html": {:hex, :phoenix_html, "3.3.4", "42a09fc443bbc1da37e372a5c8e6755d046f22b9b11343bf885067357da21cb3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0249d3abec3714aff3415e7ee3d9786cb325be3151e6c4b3021502c585bf53fb"}, "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [: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]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"}, From 4d1044e2d1959c0b5edfa76c1e34e29658bf7b55 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 2 Jul 2026 10:33:50 +0400 Subject: [PATCH 193/391] fix(deps): update plug_cowboy --- changelog.d/plug-cowboy-2.9.0.security | 1 + mix.lock | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/plug-cowboy-2.9.0.security diff --git a/changelog.d/plug-cowboy-2.9.0.security b/changelog.d/plug-cowboy-2.9.0.security new file mode 100644 index 000000000..aad941590 --- /dev/null +++ b/changelog.d/plug-cowboy-2.9.0.security @@ -0,0 +1 @@ +Update Plug.Cowboy to address HTTP/2 scheme atom exhaustion denial-of-service vulnerabilities. diff --git a/mix.lock b/mix.lock index 468afd5e9..d9d2b2c99 100644 --- a/mix.lock +++ b/mix.lock @@ -115,7 +115,7 @@ "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, "pleroma_mfm_parser": {:hex, :pleroma_mfm_parser, "0.2.1", "87a40fec4e2e035447df34e1c98e1b816b6a448d49baea73eb2273531ac5ce66", [:mix], [], "hexpm", "ec4845461da95d63ab3d592086d65114c45057a1f2d48d768f3def45b0d7696f"}, "plug": {:hex, :plug, "1.20.2", "adbee2441232412e37fbb357fd5e4cd533fdd253b29f2e1992262b0f1fb01462", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b16baf55877d60891002ffc1ce0b3ff7d6f30a38a23e02e4d4293c4ac266f136"}, - "plug_cowboy": {:hex, :plug_cowboy, "2.7.4", "729c752d17cf364e2b8da5bdb34fb5804f56251e88bb602aff48ae0bd8673d11", [: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", "9b85632bd7012615bae0a5d70084deb1b25d2bcbb32cab82d1e9a1e023168aa3"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.9.0", "87e21e0d9054ced99c36d128f49e3ea2cd8b745fffb97de50bff99706087af4f", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "2002bafba4f3a45b55a58e68d70211b153a7ed18d37edb1ceb6e96e7a92c422e"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, "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, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"}, From eae1311bc2f865866cac0cb4aa382256887f2fef Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 2 Jul 2026 10:35:41 +0400 Subject: [PATCH 194/391] fix(deps): update cowlib --- changelog.d/cowlib-2.17.1.security | 1 + mix.lock | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/cowlib-2.17.1.security diff --git a/changelog.d/cowlib-2.17.1.security b/changelog.d/cowlib-2.17.1.security new file mode 100644 index 000000000..6a5547f6f --- /dev/null +++ b/changelog.d/cowlib-2.17.1.security @@ -0,0 +1 @@ +Update Cowlib to address HTTP parsing resource exhaustion and injection vulnerabilities. diff --git a/mix.lock b/mix.lock index d9d2b2c99..4244c39d3 100644 --- a/mix.lock +++ b/mix.lock @@ -22,7 +22,7 @@ "covertool": {:hex, :covertool, "2.0.7", "398be995c4cf1a2861174389b3577ca97beee43b60c8f1afcf510f1b07d32408", [:rebar3], [], "hexpm", "46158ed6e1a0df7c0a912e314c7b8e053bd74daa5fc6b790614922a155b5720c"}, "cowboy": {:hex, :cowboy, "2.13.0", "09d770dd5f6a22cc60c071f432cd7cb87776164527f205c5a6b0f24ff6b38990", [:make, :rebar3], [{:cowlib, ">= 2.14.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e724d3a70995025d654c1992c7b11dbfea95205c047d86ff9bf1cda92ddc5614"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, - "cowlib": {:hex, :cowlib, "2.15.0", "3c97a318a933962d1c12b96ab7c1d728267d2c523c25a5b57b0f93392b6e9e25", [:make, :rebar3], [], "hexpm", "4f00c879a64b4fe7c8fcb42a4281925e9ffdb928820b03c3ad325a617e857532"}, + "cowlib": {:hex, :cowlib, "2.17.1", "3e6053016d1ab245730f0af688755476dcedb1c25ed8fb5751f59a2bfdc0c9af", [:make, :rebar3], [], "hexpm", "ff08bd17e6dd931445b18af77315b9b5fe052407110964ad2588c686b57b5e3f"}, "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, "crontab": {:hex, :crontab, "1.1.8", "2ce0e74777dfcadb28a1debbea707e58b879e6aa0ffbf9c9bb540887bce43617", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, "custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"}, From dc2852858866fd8f113e2d12ad195d62c1802892 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 2 Jul 2026 10:36:53 +0400 Subject: [PATCH 195/391] fix(deps): update cowboy --- changelog.d/cowboy-2.16.1.security | 1 + mix.lock | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/cowboy-2.16.1.security diff --git a/changelog.d/cowboy-2.16.1.security b/changelog.d/cowboy-2.16.1.security new file mode 100644 index 000000000..b7563b0e1 --- /dev/null +++ b/changelog.d/cowboy-2.16.1.security @@ -0,0 +1 @@ +Update Cowboy to address multipart header parsing denial-of-service vulnerabilities. diff --git a/mix.lock b/mix.lock index 4244c39d3..aa971f7ad 100644 --- a/mix.lock +++ b/mix.lock @@ -20,7 +20,7 @@ "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, "cors_plug": {:hex, :cors_plug, "2.0.3", "316f806d10316e6d10f09473f19052d20ba0a0ce2a1d910ddf57d663dac402ae", [:mix], [{:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ee4ae1418e6ce117fc42c2ba3e6cbdca4e95ecd2fe59a05ec6884ca16d469aea"}, "covertool": {:hex, :covertool, "2.0.7", "398be995c4cf1a2861174389b3577ca97beee43b60c8f1afcf510f1b07d32408", [:rebar3], [], "hexpm", "46158ed6e1a0df7c0a912e314c7b8e053bd74daa5fc6b790614922a155b5720c"}, - "cowboy": {:hex, :cowboy, "2.13.0", "09d770dd5f6a22cc60c071f432cd7cb87776164527f205c5a6b0f24ff6b38990", [:make, :rebar3], [{:cowlib, ">= 2.14.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e724d3a70995025d654c1992c7b11dbfea95205c047d86ff9bf1cda92ddc5614"}, + "cowboy": {:hex, :cowboy, "2.16.1", "fa04080b602ff25c40a7700f2dc0152dbc1ba26b42093ae0fa9bb7a337d5a242", [:make, :rebar3], [{:cowlib, ">= 2.16.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "b8ea4dd317a043e3177ec840cfa3bcb47cfb41035d3abb24d954dc7d51def399"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowlib": {:hex, :cowlib, "2.17.1", "3e6053016d1ab245730f0af688755476dcedb1c25ed8fb5751f59a2bfdc0c9af", [:make, :rebar3], [], "hexpm", "ff08bd17e6dd931445b18af77315b9b5fe052407110964ad2588c686b57b5e3f"}, "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, From d964737d75f963db80e39bf857db170c011acf7f Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 2 Jul 2026 10:39:34 +0400 Subject: [PATCH 196/391] fix(deps): update postgrex --- changelog.d/postgrex-0.22.2.security | 1 + mix.lock | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog.d/postgrex-0.22.2.security diff --git a/changelog.d/postgrex-0.22.2.security b/changelog.d/postgrex-0.22.2.security new file mode 100644 index 000000000..831afeee9 --- /dev/null +++ b/changelog.d/postgrex-0.22.2.security @@ -0,0 +1 @@ +Update Postgrex to address notification channel-name SQL injection vulnerabilities. diff --git a/mix.lock b/mix.lock index aa971f7ad..0f74f5e12 100644 --- a/mix.lock +++ b/mix.lock @@ -26,7 +26,7 @@ "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, "crontab": {:hex, :crontab, "1.1.8", "2ce0e74777dfcadb28a1debbea707e58b879e6aa0ffbf9c9bb540887bce43617", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, "custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"}, - "db_connection": {:hex, :db_connection, "2.8.0", "64fd82cfa6d8e25ec6660cea73e92a4cbc6a18b31343910427b702838c4b33b2", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "008399dae5eee1bf5caa6e86d204dcb44242c82b1ed5e22c881f2c34da201b15"}, + "db_connection": {:hex, :db_connection, "2.10.1", "d5465f6bcc125c1b8981c1dbf23c193ca16f446ec0b25832dc174f74f18be510", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "18ed94c6e627b4bf452dbd4df61b69a35a1e768525140bc1917b7a685026a6a3"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, "dialyxir": {:hex, :dialyxir, "1.4.6", "7cca478334bf8307e968664343cbdb432ee95b4b68a9cba95bdabb0ad5bdfd9a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "8cf5615c5cd4c2da6c501faae642839c8405b49f8aa057ad4ae401cb808ef64d"}, @@ -120,7 +120,7 @@ "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, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, - "postgrex": {:hex, :postgrex, "0.21.1", "2c5cc830ec11e7a0067dd4d623c049b3ef807e9507a424985b8dcf921224cd88", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "27d8d21c103c3cc68851b533ff99eef353e6a0ff98dc444ea751de43eb48bdac"}, + "postgrex": {:hex, :postgrex, "0.22.2", "4aec14df2a72722aee92492566edbeeb44e233ecb86b1915d03136297ef1385d", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "8946382ddb06294f56026ac4278b3cc212bac8a2c82ed68b4087819ed1abc53b"}, "pot": {:hex, :pot, "1.0.2", "13abb849139fdc04ab8154986abbcb63bdee5de6ed2ba7e1713527e33df923dd", [:rebar3], [], "hexpm", "78fe127f5a4f5f919d6ea5a2a671827bd53eb9d37e5b4128c0ad3df99856c2e0"}, "prom_ex": {:hex, :prom_ex, "1.9.0", "63e6dda6c05cdeec1f26c48443dcc38ffd2118b3665ae8d2bd0e5b79f2aea03e", [:mix], [{:absinthe, ">= 1.6.0", [hex: :absinthe, repo: "hexpm", optional: true]}, {:broadway, ">= 1.0.2", [hex: :broadway, repo: "hexpm", optional: true]}, {:ecto, ">= 3.5.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, ">= 2.4.0", [hex: :oban, repo: "hexpm", optional: true]}, {:octo_fetch, "~> 0.3", [hex: :octo_fetch, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.5.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, ">= 0.14.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, ">= 1.12.1", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 2.5", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:telemetry, ">= 1.0.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.0", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.0", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "01f3d4f69ec93068219e686cc65e58a29c42bea5429a8ff4e2121f19db178ee6"}, "prometheus": {:hex, :prometheus, "4.10.0", "792adbf0130ff61b5fa8826f013772af24b6e57b984445c8d602c8a0355704a1", [:mix, :rebar3], [{:quantile_estimator, "~> 0.2.1", [hex: :quantile_estimator, repo: "hexpm", optional: false]}], "hexpm", "2a99bb6dce85e238c7236fde6b0064f9834dc420ddbd962aac4ea2a3c3d59384"}, From 604e2ad8496c4253d5a60f2a33c76f598f072fc3 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 2 Jul 2026 10:40:53 +0400 Subject: [PATCH 197/391] fix(deps): remove unused quic lock --- changelog.d/quic-unused.security | 1 + mix.lock | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 changelog.d/quic-unused.security diff --git a/changelog.d/quic-unused.security b/changelog.d/quic-unused.security new file mode 100644 index 000000000..dc47464cf --- /dev/null +++ b/changelog.d/quic-unused.security @@ -0,0 +1 @@ +Remove unused QUIC lockfile entry with vulnerable TLS verification behavior. diff --git a/mix.lock b/mix.lock index 0f74f5e12..7516b628d 100644 --- a/mix.lock +++ b/mix.lock @@ -130,7 +130,6 @@ "prometheus_phx": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/prometheus-phx.git", "9cd8f248c9381ffedc799905050abce194a97514", [branch: "no-logging"]}, "prometheus_plugs": {:hex, :prometheus_plugs, "1.1.5", "25933d48f8af3a5941dd7b621c889749894d8a1082a6ff7c67cc99dec26377c5", [:mix], [{:accept, "~> 0.1", [hex: :accept, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}, {:prometheus_process_collector, "~> 1.1", [hex: :prometheus_process_collector, repo: "hexpm", optional: true]}], "hexpm", "0273a6483ccb936d79ca19b0ab629aef0dba958697c94782bb728b920dfc6a79"}, "quantile_estimator": {:hex, :quantile_estimator, "0.2.1", "ef50a361f11b5f26b5f16d0696e46a9e4661756492c981f7b2229ef42ff1cd15", [:rebar3], [], "hexpm", "282a8a323ca2a845c9e6f787d166348f776c1d4a41ede63046d72d422e3da946"}, - "quic": {:hex, :quic, "0.10.2", "4b390507a85f65ce47808f3df1a864e0baf9adb7a1b4ea9f4dcd66fe9d0cb166", [:rebar3], [], "hexpm", "7c196a66973c877a59768a5687f0a0610ff11817254d0a4e45cc4e3a16b1d00b"}, "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"}, "recon": {:hex, :recon, "2.5.6", "9052588e83bfedfd9b72e1034532aee2a5369d9d9343b61aeb7fbce761010741", [:mix, :rebar3], [], "hexpm", "96c6799792d735cc0f0fd0f86267e9d351e63339cbe03df9d162010cefc26bb0"}, "remote_ip": {:hex, :remote_ip, "1.2.0", "fb078e12a44414f4cef5a75963c33008fe169b806572ccd17257c208a7bc760f", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "2ff91de19c48149ce19ed230a81d377186e4412552a597d6a5137373e5877cb7"}, From ab49f0009012b33beb316ea5cf12cc661ed12994 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 2 Jul 2026 10:48:06 +0400 Subject: [PATCH 198/391] chore(deps): update jason --- changelog.d/jason-1.4.5.change | 1 + mix.lock | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/jason-1.4.5.change diff --git a/changelog.d/jason-1.4.5.change b/changelog.d/jason-1.4.5.change new file mode 100644 index 000000000..7fc87688f --- /dev/null +++ b/changelog.d/jason-1.4.5.change @@ -0,0 +1 @@ +Update Jason to support Decimal 3 dependency resolution. diff --git a/mix.lock b/mix.lock index 7516b628d..282836372 100644 --- a/mix.lock +++ b/mix.lock @@ -66,7 +66,7 @@ "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "inet_cidr": {:hex, :inet_cidr, "1.0.9", "e0ef72a2942529da78c8e4147d53f2ef5f6f5293335c3637b0fdf83c012cc816", [:mix], [], "hexpm", "172da15ff7cf635b1feaf14f5818be28c811b37cc5fb7c5f7c01058c1c1066cc"}, - "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"}, "joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"}, "jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"}, "jumper": {:hex, :jumper, "1.0.2", "68cdcd84472a00ac596b4e6459a41b3062d4427cbd4f1e8c8793c5b54f1406a7", [:mix], [], "hexpm", "9b7782409021e01ab3c08270e26f36eb62976a38c1aa64b2eaf6348422f165e1"}, From 28d7ac3a4384c504d130393ee6076245ee712673 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 2 Jul 2026 10:50:20 +0400 Subject: [PATCH 199/391] fix(deps): update decimal --- changelog.d/decimal-3.1.1.security | 1 + mix.lock | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 changelog.d/decimal-3.1.1.security diff --git a/changelog.d/decimal-3.1.1.security b/changelog.d/decimal-3.1.1.security new file mode 100644 index 000000000..65d885553 --- /dev/null +++ b/changelog.d/decimal-3.1.1.security @@ -0,0 +1 @@ +Update Decimal and compatible dependencies to address a rounding vulnerability. diff --git a/mix.lock b/mix.lock index 282836372..49822048a 100644 --- a/mix.lock +++ b/mix.lock @@ -27,16 +27,16 @@ "crontab": {:hex, :crontab, "1.1.8", "2ce0e74777dfcadb28a1debbea707e58b879e6aa0ffbf9c9bb540887bce43617", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, "custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"}, "db_connection": {:hex, :db_connection, "2.10.1", "d5465f6bcc125c1b8981c1dbf23c193ca16f446ec0b25832dc174f74f18be510", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "18ed94c6e627b4bf452dbd4df61b69a35a1e768525140bc1917b7a685026a6a3"}, - "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, + "decimal": {:hex, :decimal, "3.1.1", "430d87b04011ce6cbd4fd205be758311a81f87d552d40904abd00f015935b1d0", [:mix], [], "hexpm", "c5f25f2ced74a0587d03e6023f595db8e924c9d3922c8c8ffd9edfc4498cf1f6"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, "dialyxir": {:hex, :dialyxir, "1.4.6", "7cca478334bf8307e968664343cbdb432ee95b4b68a9cba95bdabb0ad5bdfd9a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "8cf5615c5cd4c2da6c501faae642839c8405b49f8aa057ad4ae401cb808ef64d"}, "earmark": {:hex, :earmark, "1.4.46", "8c7287bd3137e99d26ae4643e5b7ef2129a260e3dcf41f251750cb4563c8fb81", [:mix], [], "hexpm", "798d86db3d79964e759ddc0c077d5eb254968ed426399fbf5a62de2b5ff8910a"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, "eblurhash": {:git, "https://github.com/zotonic/eblurhash.git", "bc37ceb426ef021ee9927fb249bb93f7059194ab", [ref: "bc37ceb426ef021ee9927fb249bb93f7059194ab"]}, - "ecto": {:hex, :ecto, "3.13.2", "7d0c0863f3fc8d71d17fc3ad3b9424beae13f02712ad84191a826c7169484f01", [:mix], [{:decimal, "~> 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", "669d9291370513ff56e7b7e7081b7af3283d02e046cf3d403053c557894a0b3e"}, + "ecto": {:hex, :ecto, "3.14.0", "2fa64521eebfcb2670d907a86e4ad947290e9933706bb315e6fb5c21b172cb26", [:mix], [{:decimal, "~> 3.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", "130d69ffb4285f9ce4792b65dfbb994fd13ea4cbc3cbea2524b199aa3de84af3"}, "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.8.8", "aa02529c97f69aed5722899f5dc6360128735a92dd169f23c5d50b1f7fdede08", [:mix], [{:ecto_sql, "~> 3.7", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "> 0.16.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1 or ~> 4.0", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "04c63d92b141723ad6fed2e60a4b461ca00b3594d16df47bbc48f1f4534f2c49"}, - "ecto_sql": {:hex, :ecto_sql, "3.13.2", "a07d2461d84107b3d037097c822ffdd36ed69d1cf7c0f70e12a3d1decf04e2e1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 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", "539274ab0ecf1a0078a6a72ef3465629e4d6018a3028095dc90f60a19c371717"}, + "ecto_sql": {:hex, :ecto_sql, "3.14.0", "06446ab8410d2f85bfbb80857ee224ab3b693700cbb38f6535d507449a627b2e", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.14.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.8", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 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", "f4d8d36faf294c9417b5a37ec7ac8217ee2abdef5fcf197ba690f361548d3949"}, "eimp": {:hex, :eimp, "1.0.14", "fc297f0c7e2700457a95a60c7010a5f1dcb768a083b6d53f49cd94ab95a28f22", [:rebar3], [{:p1_utils, "1.0.18", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "501133f3112079b92d9e22da8b88bf4f0e13d4d67ae9c15c42c30bd25ceb83b6"}, "elixir_make": {:hex, :elixir_make, "0.7.8", "505026f266552ee5aabca0b9f9c229cbb496c689537c9f922f3eb5431157efc7", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "7a71945b913d37ea89b06966e1342c85cfe549b15e6d6d081e8081c493062c07"}, "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, @@ -100,7 +100,7 @@ "oban_plugins_lazarus": {:hex, :oban_plugins_lazarus, "0.1.1", "a5141d569e5b9f3bec8f4a958231d2c538af097d3c1ad06274f096fc06956821", [:mix], [{:oban, "< 3.0.0", [hex: :oban, repo: "hexpm", optional: false]}], "hexpm", "7e9c51bc44d33f71c0a52cf0cd5c8d4c70380ede065c1042013f5123f2fc9729"}, "oban_web": {:hex, :oban_web, "2.11.6", "53933cb4253c4d9f1098ee311c06f07935259f0e564dcf2d66bae4cc98e317fe", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, "~> 2.19", [hex: :oban, repo: "hexpm", optional: false]}, {:oban_met, "~> 1.0", [hex: :oban_met, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}], "hexpm", "576d94b705688c313694c2c114ca21aa0f8f2ad1b9ca45c052c5ba316d3e8d10"}, "octo_fetch": {:hex, :octo_fetch, "0.4.0", "074b5ecbc08be10b05b27e9db08bc20a3060142769436242702931c418695b19", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "cf8be6f40cd519d7000bb4e84adcf661c32e59369ca2827c4e20042eda7a7fc6"}, - "open_api_spex": {:hex, :open_api_spex, "3.22.0", "fbf90dc82681dc042a4ee79853c8e989efbba73d9e87439085daf849bbf8bc20", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "dd751ddbdd709bb4a5313e9a24530da6e66594773c7242a0c2592cbd9f589063"}, + "open_api_spex": {:hex, :open_api_spex, "3.22.3", "0e383bf23cc3a060bffaebbcd09fc06bfc908d948c00e518aed36bbf8a2fe473", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "5f74f1878fdc38f8e961b0b943ac7af88dcf3a82a0c0ef6680ddfd3d161aecbd"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "1.2.1", "9cbe354b58121075bd20eb83076900a3832324b7dd171a6895fab57b6bb2752c", [:mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}], "hexpm", "d3b40a4a4630f0b442f19eca891fcfeeee4c40871936fed2f68e1c4faa30481f"}, "phoenix": {:hex, :phoenix, "1.8.8", "ada3d761359274178180c0e992ef0c2b536bd7c3bd75ebba94acbf39ab4347fe", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, 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]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "f0c843037bd2e7012fc1d1ec9574dfa6972b7e3d09e9b77fd23aa283af0aa994"}, From a96fb31a89919a2801e06b887a3fb5b15d3252bf Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 2 Jul 2026 10:57:12 +0400 Subject: [PATCH 200/391] fix(deps): update hackney --- changelog.d/hackney-4.0.3.security | 1 + mix.exs | 3 ++- mix.lock | 10 ++++++---- 3 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 changelog.d/hackney-4.0.3.security diff --git a/changelog.d/hackney-4.0.3.security b/changelog.d/hackney-4.0.3.security new file mode 100644 index 000000000..2cbf211b3 --- /dev/null +++ b/changelog.d/hackney-4.0.3.security @@ -0,0 +1 @@ +Update Hackney and its HTTPoison integration to address SSRF, timeout, and injection vulnerabilities. diff --git a/mix.exs b/mix.exs index 8667a880b..5cfc99f20 100644 --- a/mix.exs +++ b/mix.exs @@ -160,6 +160,7 @@ defmodule Pleroma.Mixfile do {:pleroma_mfm_parser, "~> 0.2.1"}, {:cors_plug, "~> 2.0"}, {:web_push_encryption, "~> 0.3.1"}, + {:httpoison, "~> 3.0", override: true}, {:swoosh, "~> 1.16.12"}, {:phoenix_swoosh, "~> 1.2"}, {:gen_smtp, "~> 0.15"}, @@ -207,7 +208,7 @@ defmodule Pleroma.Mixfile do {:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false}, {:mock, "~> 0.3.9", only: :test}, {:covertool, "~> 2.0", only: :test}, - {:hackney, "~> 1.20.1", override: true}, + {:hackney, "~> 4.0.1", override: true}, {:mox, "~> 1.2", only: :test}, {:websockex, "~> 0.4.3", only: :test}, {:benchee, "~> 1.4", only: :benchmark}, diff --git a/mix.lock b/mix.lock index 49822048a..8ea793fea 100644 --- a/mix.lock +++ b/mix.lock @@ -13,7 +13,7 @@ "captcha": {:hex, :pleroma_captcha, "1.0.3", "6cc1440e91a092653fe909f028cc4c5d83ea858b1439e3b9a85e446382e2b9a3", [:make, :mix], [], "hexpm", "477f62c1a845a9458c546658223295f958f98136acef89c05beb278b1b6f4a14"}, "castore": {:hex, :castore, "1.0.15", "8aa930c890fe18b6fe0a0cff27b27d0d4d231867897bd23ea772dee561f032a3", [:mix], [], "hexpm", "96ce4c69d7d5d7a0761420ef743e2f4096253931a3ba69e5ff8ef1844fe446d3"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, - "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, + "certifi": {:hex, :certifi, "2.16.0", "a4edfc1d2da3424d478a3271133bf28e0ec5e6fd8c009aab5a4ae980cb165ce9", [:rebar3], [], "hexpm", "8a64f6669d85e9cc0e5086fcf29a5b13de57a13efa23d3582874b9a19303f184"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, "comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"}, "concurrent_limiter": {:hex, :concurrent_limiter, "0.1.1", "43ae1dc23edda1ab03dd66febc739c4ff710d047bb4d735754909f9a474ae01c", [:mix], [{:telemetry, "~> 0.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "53968ff238c0fbb4d7ed76ddb1af0be6f3b2f77909f6796e249e737c505a16eb"}, @@ -59,12 +59,13 @@ "gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"}, "gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"}, "gun": {:hex, :gun, "2.2.0", "b8f6b7d417e277d4c2b0dc3c07dfdf892447b087f1cc1caff9c0f556b884e33d", [:make, :rebar3], [{:cowlib, ">= 2.15.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "76022700c64287feb4df93a1795cff6741b83fb37415c40c34c38d2a4645261a"}, - "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, + "h2": {:hex, :h2, "0.6.1", "16a8cf76dc017aba1e47ea20bc928ae309892d36a59ca0cb43640bcabb2a270f", [:rebar3], [], "hexpm", "27fb7482341431dc9e1d96d2063c23664a52ec0ce70ba2fdaddb945356a51388"}, + "hackney": {:hex, :hackney, "4.0.3", "bbc2d133c405d7d049c07898aa5a622b667be841f8cd63fd24fa97afd3e36fa1", [:rebar3], [{:certifi, "~> 2.16.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:h2, "0.6.1", [hex: :h2, repo: "hexpm", optional: false]}, {:idna, "~> 7.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:quic, "1.4.5", [hex: :quic, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "6b9e755646b563228986acc86a95f7d8d26b58b82ce57d8ce1b71d22b81bbf92"}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, "http_signatures": {:hex, :http_signatures, "0.1.3", "19f26aee35b4684e37efdce3ac4638605e6e41a04368186bd39d2b6138a60ea9", [:mix], [{:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "20313a65516db88006f85b090f6f76cc5b04e9609b45943657e6781eb91174f4"}, - "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"}, - "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, + "httpoison": {:hex, :httpoison, "3.0.0", "8566a933bb9175236d1ec335978445b67cd1f5b5d3ead6ca4b80be469d41f5d9", [:mix], [{:hackney, "~> 4.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "9130197b7658901c493d6fcfb842fb9676300fa8a6c8ed058c8889cf1a77f3c2"}, + "idna": {:hex, :idna, "7.1.0", "1067a13043538129602d2f2ce6899d8713125c7d19734aa557ce2e3ea55bd4f1", [:rebar3], [], "hexpm", "6ae959a025bf36df61a8cab8508d9654891b5426a84c44d82deaffd6ddf8c71f"}, "inet_cidr": {:hex, :inet_cidr, "1.0.9", "e0ef72a2942529da78c8e4147d53f2ef5f6f5293335c3637b0fdf83c012cc816", [:mix], [], "hexpm", "172da15ff7cf635b1feaf14f5818be28c811b37cc5fb7c5f7c01058c1c1066cc"}, "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"}, "joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"}, @@ -130,6 +131,7 @@ "prometheus_phx": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/prometheus-phx.git", "9cd8f248c9381ffedc799905050abce194a97514", [branch: "no-logging"]}, "prometheus_plugs": {:hex, :prometheus_plugs, "1.1.5", "25933d48f8af3a5941dd7b621c889749894d8a1082a6ff7c67cc99dec26377c5", [:mix], [{:accept, "~> 0.1", [hex: :accept, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}, {:prometheus_process_collector, "~> 1.1", [hex: :prometheus_process_collector, repo: "hexpm", optional: true]}], "hexpm", "0273a6483ccb936d79ca19b0ab629aef0dba958697c94782bb728b920dfc6a79"}, "quantile_estimator": {:hex, :quantile_estimator, "0.2.1", "ef50a361f11b5f26b5f16d0696e46a9e4661756492c981f7b2229ef42ff1cd15", [:rebar3], [], "hexpm", "282a8a323ca2a845c9e6f787d166348f776c1d4a41ede63046d72d422e3da946"}, + "quic": {:hex, :quic, "1.4.5", "95882c15623fb4cc005d05590089b72506e304447e7e6a96919580d8dfaadd5e", [:rebar3], [], "hexpm", "581596797c858f3d15fe283398c988c9999334877eff816c6e3fa7e83fa21c1a"}, "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"}, "recon": {:hex, :recon, "2.5.6", "9052588e83bfedfd9b72e1034532aee2a5369d9d9343b61aeb7fbce761010741", [:mix, :rebar3], [], "hexpm", "96c6799792d735cc0f0fd0f86267e9d351e63339cbe03df9d162010cefc26bb0"}, "remote_ip": {:hex, :remote_ip, "1.2.0", "fb078e12a44414f4cef5a75963c33008fe169b806572ccd17257c208a7bc760f", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "2ff91de19c48149ce19ed230a81d377186e4412552a597d6a5137373e5877cb7"}, From 9154f6fe3c54d63e36d7dac274966d34ed60ccb7 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 2 Jul 2026 11:31:54 +0400 Subject: [PATCH 201/391] fix(deps): replace earmark with mdex --- changelog.d/earmark-mdex.security | 1 + lib/pleroma/formatter.ex | 43 ++++++++++++++++++++-- mix.exs | 3 +- mix.lock | 6 ++- test/pleroma/web/common_api/utils_test.exs | 9 +++++ 5 files changed, 55 insertions(+), 7 deletions(-) create mode 100644 changelog.d/earmark-mdex.security diff --git a/changelog.d/earmark-mdex.security b/changelog.d/earmark-mdex.security new file mode 100644 index 000000000..b03da28ab --- /dev/null +++ b/changelog.d/earmark-mdex.security @@ -0,0 +1 @@ +Replace retired Earmark markdown rendering with MDEx to address stored XSS in link attributes. diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex index 4bf2f6b95..a0f477a16 100644 --- a/lib/pleroma/formatter.ex +++ b/lib/pleroma/formatter.ex @@ -124,14 +124,49 @@ defmodule Pleroma.Formatter do end def markdown_to_html(text) do - Earmark.as_html!(text, %Earmark.Options{compact_output: true, smartypants: false}) + markdown_to_html(text, %{}) end def markdown_to_html(text, opts) do - Earmark.as_html!( - text, - %Earmark.Options{compact_output: true, smartypants: false} |> Map.merge(opts) + breaks = Map.get(opts, :breaks, false) + text = normalize_markdown_quirks(text) + + text + |> MDEx.to_html!( + extension: [autolink: true, strikethrough: true], + parse: [smart: false], + render: [hardbreaks: breaks, unsafe: true] ) + |> compact_markdown_html(text) + end + + defp compact_markdown_html(html, markdown) do + html + |> String.trim_trailing("\n") + |> String.replace(~r/>\n\s*<") + |> String.replace(~r/\n<\/code><\/pre>/, "") + |> String.replace(~r/(?)/, ~s()) + |> String.replace("
", "
") + |> unwrap_raw_html_paragraph(markdown) + end + + defp normalize_markdown_quirks(text) do + String.replace(text, ~r/~~([^~\n]+)~~~/, "~~\\1~~\\\\~") + end + + defp unwrap_raw_html_paragraph(html, markdown) do + if raw_html_only?(markdown) do + String.replace(html, ~r/\A

(.*)<\/p>\z/s, "\\1") + else + html + end + end + + defp raw_html_only?(markdown) do + markdown = String.trim(markdown) + + String.match?(markdown, ~r/\A(?: From 775ae5d50493540d2b16f20428a9bed416400eff Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 24 Sep 2026 20:01:06 +0200 Subject: [PATCH 350/391] Docs New Elixir/Erlang versions install: Add maintenance chapter --- .../installing_newer_elixir_erlang.md | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/installation/installing_newer_elixir_erlang.md b/docs/installation/installing_newer_elixir_erlang.md index fd5623785..6b7e145ed 100644 --- a/docs/installation/installing_newer_elixir_erlang.md +++ b/docs/installation/installing_newer_elixir_erlang.md @@ -281,8 +281,28 @@ ExecStart=/var/lib/pleroma/pleroma.sh You can now start the Pleroma service. If you are installing Pleroma, continue with creating your first user according to the install guide. +## Maintenance + +Since Elixir and Erlang is no longer managed by the system package manager after following this guide, it is recommended to periodically check their respective websites/repositories for newer versions with security bug fixes. +When updating Elixir, Erlang or both, stop Pleroma before proceeding with the update. + +Pleroma will likely need to be recompiled after the update by running the following command in the Pleroma source code directory as the Pleroma user: +```sh +MIX_ENV=prod mix compile +``` + +If Pleroma fails to compile or start after upgrading Elixir/Erlang, run the following as a troubleshooting step in the Pleroma source code directory as the Pleroma user: +```sh +mix clean +mix deps.clean --all +mix deps.get +MIX_ENV=prod mix compile +``` + +Then try to start Pleroma again. If that doesn't help, join the Matrix room or IRC channel mentioned below, or make an issue on [git.pleroma.social/pleroma/pleroma](https://git.pleroma.social/pleroma/pleroma/issues/new/choose) explaining the problem. + ## Questions -Questions about the installation or didn’t it work as it should be, ask in [#pleroma:libera.chat](https://matrix.to/#/#pleroma:libera.chat) via Matrix or **#pleroma** on **libera.chat** via IRC. +Questions about the guide or it didn’t work as it should, ask in [#pleroma:libera.chat](https://matrix.to/#/#pleroma:libera.chat) via Matrix or **#pleroma** on **libera.chat** via IRC. From 0061f62355145e764ec539424541a2f2386aac5d Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 24 Sep 2026 20:06:42 +0200 Subject: [PATCH 351/391] Docs Newer Elixir/Erlang guide: Recommend upgrading OS release instead --- docs/installation/installing_newer_elixir_erlang.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/installation/installing_newer_elixir_erlang.md b/docs/installation/installing_newer_elixir_erlang.md index 6b7e145ed..8c475e513 100644 --- a/docs/installation/installing_newer_elixir_erlang.md +++ b/docs/installation/installing_newer_elixir_erlang.md @@ -3,6 +3,7 @@ This guide is for installing newer versions of Elixir and/or Erlang on your system when installing Pleroma from source and your operating system repositories don't meet the minimum requirements listed below. This guide assumes you are using Debian or Ubuntu. **Using the system repositories is recommended if possible.** +**If a newer release of your operating system would meet the requirements, consider upgrading instead.** Shell commands prepended with `#` in this page are supposed to be ran with root privileges, commands prepended with `$` are supposed to be ran as the Pleroma user unless noted otherwise. From 62b6a24d5dcc9a371bb4881d88e7d2b636e79c22 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 24 Sep 2026 20:08:58 +0200 Subject: [PATCH 352/391] Docs: move paragraph --- docs/installation/installing_newer_elixir_erlang.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation/installing_newer_elixir_erlang.md b/docs/installation/installing_newer_elixir_erlang.md index 8c475e513..4771f7507 100644 --- a/docs/installation/installing_newer_elixir_erlang.md +++ b/docs/installation/installing_newer_elixir_erlang.md @@ -19,10 +19,10 @@ Shell commands prepended with `#` in this page are supposed to be ran with root {! backend/installation/erlang_elixir_requirements.include !} -Choose one of the following install methods: - ## Install methods +Choose one of the following install methods: + * User level: * mise (Erlang and Elixir install methods are built-in; downloads binaries both for Elixir and Erlang) * asdf (plugin-based, plugins are shells scripts; builds Erlang from source) From 4515f9139c64d04eef57ece6783b24f8547d97e6 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 29 Sep 2026 10:27:11 +0200 Subject: [PATCH 353/391] Docs OpenBSD: Bump recommended datasize login.conf limit Under OpenBSD datasize not only includes allocated memory using malloc, but also anonymous memory mapped via mmap. --- changelog.d/openbsd-datasize-bump.change | 1 + docs/installation/openbsd_en.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/openbsd-datasize-bump.change diff --git a/changelog.d/openbsd-datasize-bump.change b/changelog.d/openbsd-datasize-bump.change new file mode 100644 index 000000000..9085cc43a --- /dev/null +++ b/changelog.d/openbsd-datasize-bump.change @@ -0,0 +1 @@ +Docs OpenBSD: Increased recommended `datasize` from 1536M to 4096M to avoid Pleroma crashes diff --git a/docs/installation/openbsd_en.md b/docs/installation/openbsd_en.md index 1de016cdd..4e2f43f85 100644 --- a/docs/installation/openbsd_en.md +++ b/docs/installation/openbsd_en.md @@ -67,7 +67,7 @@ Pleroma will be run by a dedicated \_pleroma user. Before creating it, insert th ``` pleroma:\ - :datasize=1536M:\ + :datasize=4096M:\ :openfiles-max=4096:\ :openfiles-cur=1024:\ :setenv=LC_ALL=en_US.UTF-8,VIX_COMPILATION_MODE=PLATFORM_PROVIDED_LIBVIPS,MIX_ENV=prod:\ From 62d4258d7994e8ebdffb8befea290cc71082906d Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 29 Sep 2026 10:41:13 +0200 Subject: [PATCH 354/391] Docs OpenBSD: Mention running cap_mkdb after making new login class --- docs/installation/openbsd_en.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/installation/openbsd_en.md b/docs/installation/openbsd_en.md index 4e2f43f85..82d137e96 100644 --- a/docs/installation/openbsd_en.md +++ b/docs/installation/openbsd_en.md @@ -74,6 +74,12 @@ pleroma:\ :tc=daemon: ``` +Then run: + +``` +# cap_mkdb /etc/login.conf +``` + This creates a "pleroma" login class and sets higher values than default for datasize and openfiles (see [login.conf(5)](https://man.openbsd.org/login.conf)), this is required to avoid having Pleroma crash some time after starting. Create the \_pleroma user, assign it the pleroma login class and create its home directory (/home/\_pleroma/): From 9c76e0686a017ba465a5b61ce18447a04f517dc9 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 30 Sep 2026 14:42:10 +0200 Subject: [PATCH 355/391] Deps: Bump captcha version Fixes build on OpenBSD and FreeBSD --- changelog.d/captcha-ver-bump.fix | 1 + mix.exs | 2 +- mix.lock | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog.d/captcha-ver-bump.fix diff --git a/changelog.d/captcha-ver-bump.fix b/changelog.d/captcha-ver-bump.fix new file mode 100644 index 000000000..8e5ea0e9c --- /dev/null +++ b/changelog.d/captcha-ver-bump.fix @@ -0,0 +1 @@ +Updated the captcha package to fix compilation failure on BSD-based systems diff --git a/mix.exs b/mix.exs index 140aafb98..3598a4f53 100644 --- a/mix.exs +++ b/mix.exs @@ -198,7 +198,7 @@ defmodule Pleroma.Mixfile do {:concurrent_limiter, "~> 0.1.1"}, {:remote_ip, "~> 1.2.0"}, {:inet_cidr, "~> 1.0"}, - {:captcha, "~> 1.0.3", hex: :pleroma_captcha}, + {:captcha, "~> 1.0.4", hex: :pleroma_captcha}, {:restarter, path: "./restarter"}, {:majic, "~> 1.2"}, {:open_api_spex, "~> 3.22"}, diff --git a/mix.lock b/mix.lock index bca6f777b..369365cdc 100644 --- a/mix.lock +++ b/mix.lock @@ -10,7 +10,7 @@ "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "cachex": {:hex, :cachex, "3.6.0", "14a1bfbeee060dd9bec25a5b6f4e4691e3670ebda28c8ba2884b12fe30b36bf8", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "ebf24e373883bc8e0c8d894a63bbe102ae13d918f790121f5cfe6e485cc8e2e2"}, "calendar": {:hex, :calendar, "1.0.0", "f52073a708528482ec33d0a171954ca610fe2bd28f1e871f247dc7f1565fa807", [:mix], [{:tzdata, "~> 0.1.201603 or ~> 0.5.20 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "990e9581920c82912a5ee50e62ff5ef96da6b15949a2ee4734f935fdef0f0a6f"}, - "captcha": {:hex, :pleroma_captcha, "1.0.3", "6cc1440e91a092653fe909f028cc4c5d83ea858b1439e3b9a85e446382e2b9a3", [:make, :mix], [], "hexpm", "477f62c1a845a9458c546658223295f958f98136acef89c05beb278b1b6f4a14"}, + "captcha": {:hex, :pleroma_captcha, "1.0.4", "6e28ef98660981daeb4aee5043551d921b0094ec5bdeb4daf2f01858e14e9d2d", [:make, :mix], [], "hexpm", "aee86d4da3522ef39ba814e6954c4ebe692e72664efcb8fa5819b1335999b577"}, "castore": {:hex, :castore, "1.0.15", "8aa930c890fe18b6fe0a0cff27b27d0d4d231867897bd23ea772dee561f032a3", [:mix], [], "hexpm", "96ce4c69d7d5d7a0761420ef743e2f4096253931a3ba69e5ff8ef1844fe446d3"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, From 1659fd99d34ff55d51bdbdaa3645ce36cf377478 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 3 Oct 2026 23:40:07 +0200 Subject: [PATCH 356/391] Docs: Add missing curl dependency for asdf --- changelog.d/fixup-new-elixir-erlang-docs.skip | 0 docs/installation/installing_newer_elixir_erlang.md | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 changelog.d/fixup-new-elixir-erlang-docs.skip diff --git a/changelog.d/fixup-new-elixir-erlang-docs.skip b/changelog.d/fixup-new-elixir-erlang-docs.skip new file mode 100644 index 000000000..e69de29bb diff --git a/docs/installation/installing_newer_elixir_erlang.md b/docs/installation/installing_newer_elixir_erlang.md index 4771f7507..0d06d2fba 100644 --- a/docs/installation/installing_newer_elixir_erlang.md +++ b/docs/installation/installing_newer_elixir_erlang.md @@ -117,7 +117,7 @@ Then follow the install and configure steps from asdf documentation and install Install dependencies: ``` -# apt install build-essential libssl-dev libncurses-dev git unzip +# apt install build-essential libssl-dev libncurses-dev git unzip curl ``` Add Erlang autotools configure options to Pleroma user's shell config (`.bashrc` for bash): From 0421e8b2e373f5a5fc40f889c18af2a7e45a02ec Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sun, 4 Oct 2026 20:23:16 +0200 Subject: [PATCH 357/391] Docs New Elixir/Erlang: Clarify that mise builds Erlang on Debian --- .../installing_newer_elixir_erlang.md | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/docs/installation/installing_newer_elixir_erlang.md b/docs/installation/installing_newer_elixir_erlang.md index 0d06d2fba..0220996d8 100644 --- a/docs/installation/installing_newer_elixir_erlang.md +++ b/docs/installation/installing_newer_elixir_erlang.md @@ -24,14 +24,16 @@ Shell commands prepended with `#` in this page are supposed to be ran with root Choose one of the following install methods: * User level: - * mise (Erlang and Elixir install methods are built-in; downloads binaries both for Elixir and Erlang) + * mise (Erlang and Elixir install methods are built-in[^1]) * asdf (plugin-based, plugins are shells scripts; builds Erlang from source) * pkgsrc (for advanced users, harder to install and maintain; builds Elixir and Erlang from source) * System-wide: * pkgsrc (for advanced users, harder to install and maintain; builds Elixir and Erlang from source) +[^1]: On Ubuntu downloads binaries both for Elixir and Erlang. On Debian builds Erlang from source and downloads binaries for Elixir. + !!!warning - **Ignore if using mise.** + **Ignore if using mise on Ubuntu.** You are about to compile Erlang on your server. If you are planning to run Pleroma on a VPS virtual machine, make sure you have enough disk space and system resources available. At least 2GB of RAM and 8GB of extra free disk space is required to continue in this guide. @@ -44,8 +46,30 @@ Choose one of the following install methods: ## mise -To start, create the Pleroma user according to the [source install guide](./debian_based_en.md#install-pleromabe) and switch to the user using `sudo -Hu pleroma /bin/bash`. -Then follow the install steps from mise documentation and install it for the user Pleroma: [https://mise.jdx.dev/getting-started.html](https://mise.jdx.dev/getting-started.html) +To start, create the Pleroma user according to the [source install guide](./debian_based_en.md#install-pleromabe). + +### Debian-only preparation + +If you are using Ubuntu, skip to the [next section](./installing_newer_elixir_erlang.md#elixirerlang-installation). + +Install dependencies: +``` +# apt install build-essential libssl-dev libncurses-dev git unzip +``` + +Add Erlang autotools configure options to Pleroma user's shell config (`.bashrc` for bash): +```sh +export KERL_CONFIGURE_OPTIONS="--without-javac --without-odbc --without-wx --without-ssh" +``` + +### Elixir/Erlang installation + +Install curl (for the mise installer): +``` +# apt install curl +``` + +Switch to the Pleroma user using `sudo -Hu pleroma /bin/bash` and follow the install steps from mise documentation and install it for the user Pleroma: [https://mise.jdx.dev/getting-started.html](https://mise.jdx.dev/getting-started.html) Add the following to activate mise in the Pleroma user's shell configuration (`.bashrc` for bash): ```sh From bde6bc91a4f9952e63b9e495952488d6f8fdbcf3 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 20 Oct 2026 23:42:28 +0200 Subject: [PATCH 358/391] Sec: Bump castore to version 1.0.22 eMudhra Intermediate CA Compromise --- changelog.d/castore-1.0.22.security | 1 + mix.lock | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/castore-1.0.22.security diff --git a/changelog.d/castore-1.0.22.security b/changelog.d/castore-1.0.22.security new file mode 100644 index 000000000..8d702f382 --- /dev/null +++ b/changelog.d/castore-1.0.22.security @@ -0,0 +1 @@ +Updated CAStore to version 1.0.22 with updated Mozilla certificate bundle after eMudhra Intermediate CA Compromise diff --git a/mix.lock b/mix.lock index 165374c59..838b56a5e 100644 --- a/mix.lock +++ b/mix.lock @@ -11,7 +11,7 @@ "cachex": {:hex, :cachex, "3.6.0", "14a1bfbeee060dd9bec25a5b6f4e4691e3670ebda28c8ba2884b12fe30b36bf8", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "ebf24e373883bc8e0c8d894a63bbe102ae13d918f790121f5cfe6e485cc8e2e2"}, "calendar": {:hex, :calendar, "1.0.0", "f52073a708528482ec33d0a171954ca610fe2bd28f1e871f247dc7f1565fa807", [:mix], [{:tzdata, "~> 0.1.201603 or ~> 0.5.20 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "990e9581920c82912a5ee50e62ff5ef96da6b15949a2ee4734f935fdef0f0a6f"}, "captcha": {:hex, :pleroma_captcha, "1.0.3", "6cc1440e91a092653fe909f028cc4c5d83ea858b1439e3b9a85e446382e2b9a3", [:make, :mix], [], "hexpm", "477f62c1a845a9458c546658223295f958f98136acef89c05beb278b1b6f4a14"}, - "castore": {:hex, :castore, "1.0.21", "0a0e8330dc267a40a3b7ad86d39302764bb71758172904e6a59d5ad6443ce307", [:mix], [], "hexpm", "e42e22723e25dbd46876d056a03f685513d6e98f6b5e555dc551321decd76c5c"}, + "castore": {:hex, :castore, "1.0.22", "a01978e6a5d42fa3a34143630588f92036e7d90e2309ecdd466722d1279a2213", [:mix], [], "hexpm", "c2a4d42723b4affbc1c3e15d8f6df3f7d8f1f627c84de643383634cce58493d5"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, "certifi": {:hex, :certifi, "2.17.0", "835748414307e15e05b17d0e518190228ce648b08d569a5cc93a85a40f3e5c9b", [:rebar3], [], "hexpm", "8122798a17f0293c80daada25d0f81c7f4d708c73fef782c7c9b1950e26e4d21"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, From 6ac6cff5b5de4ed1a917902ad20b043fb4cb40cc Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 20 Oct 2026 23:42:28 +0200 Subject: [PATCH 359/391] Sec: Bump castore to version 1.0.22 eMudhra Intermediate CA Compromise --- changelog.d/castore-1.0.22.security | 1 + mix.lock | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/castore-1.0.22.security diff --git a/changelog.d/castore-1.0.22.security b/changelog.d/castore-1.0.22.security new file mode 100644 index 000000000..8d702f382 --- /dev/null +++ b/changelog.d/castore-1.0.22.security @@ -0,0 +1 @@ +Updated CAStore to version 1.0.22 with updated Mozilla certificate bundle after eMudhra Intermediate CA Compromise diff --git a/mix.lock b/mix.lock index bca6f777b..58b5a1f58 100644 --- a/mix.lock +++ b/mix.lock @@ -11,7 +11,7 @@ "cachex": {:hex, :cachex, "3.6.0", "14a1bfbeee060dd9bec25a5b6f4e4691e3670ebda28c8ba2884b12fe30b36bf8", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "ebf24e373883bc8e0c8d894a63bbe102ae13d918f790121f5cfe6e485cc8e2e2"}, "calendar": {:hex, :calendar, "1.0.0", "f52073a708528482ec33d0a171954ca610fe2bd28f1e871f247dc7f1565fa807", [:mix], [{:tzdata, "~> 0.1.201603 or ~> 0.5.20 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "990e9581920c82912a5ee50e62ff5ef96da6b15949a2ee4734f935fdef0f0a6f"}, "captcha": {:hex, :pleroma_captcha, "1.0.3", "6cc1440e91a092653fe909f028cc4c5d83ea858b1439e3b9a85e446382e2b9a3", [:make, :mix], [], "hexpm", "477f62c1a845a9458c546658223295f958f98136acef89c05beb278b1b6f4a14"}, - "castore": {:hex, :castore, "1.0.15", "8aa930c890fe18b6fe0a0cff27b27d0d4d231867897bd23ea772dee561f032a3", [:mix], [], "hexpm", "96ce4c69d7d5d7a0761420ef743e2f4096253931a3ba69e5ff8ef1844fe446d3"}, + "castore": {:hex, :castore, "1.0.22", "a01978e6a5d42fa3a34143630588f92036e7d90e2309ecdd466722d1279a2213", [:mix], [], "hexpm", "c2a4d42723b4affbc1c3e15d8f6df3f7d8f1f627c84de643383634cce58493d5"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, From d8c880e4946afa2710ae7e897082db982245b095 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Wed, 21 Oct 2026 08:21:43 +0400 Subject: [PATCH 360/391] fix(http): make Hackney trust the CAStore bundle Hackney trusts its own certifi bundle, which still has the roots of the compromised eMudhra intermediate CAs. Pass explicit ssl_options with the CAStore bundle. Hackney drops all its TLS defaults when ssl_options are given, so keep peer and hostname checks in the options. --- changelog.d/hackney-castore.security | 1 + lib/pleroma/http/adapter_helper/hackney.ex | 1 + lib/pleroma/http/tls.ex | 35 ++++++++++++++ lib/pleroma/reverse_proxy/client/hackney.ex | 6 +-- .../http/adapter_helper/hackney_test.exs | 15 ++++++ test/pleroma/http/tls_test.exs | 47 +++++++++++++++++++ 6 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 changelog.d/hackney-castore.security create mode 100644 lib/pleroma/http/tls.ex create mode 100644 test/pleroma/http/tls_test.exs diff --git a/changelog.d/hackney-castore.security b/changelog.d/hackney-castore.security new file mode 100644 index 000000000..d30a53f80 --- /dev/null +++ b/changelog.d/hackney-castore.security @@ -0,0 +1 @@ +Hackney now trusts only the CAStore certificate bundle instead of its outdated certifi bundle, which still trusted the compromised eMudhra intermediate CAs diff --git a/lib/pleroma/http/adapter_helper/hackney.ex b/lib/pleroma/http/adapter_helper/hackney.ex index 2b1502001..fe4df1e1b 100644 --- a/lib/pleroma/http/adapter_helper/hackney.ex +++ b/lib/pleroma/http/adapter_helper/hackney.ex @@ -28,6 +28,7 @@ defmodule Pleroma.HTTP.AdapterHelper.Hackney do |> add_scheme_opts(uri) |> maybe_add_with_body() |> Pleroma.HTTP.AdapterHelper.maybe_add_proxy(proxy) + |> Pleroma.HTTP.TLS.put_hackney_ssl_options() end defp add_scheme_opts(opts, _), do: opts diff --git a/lib/pleroma/http/tls.ex b/lib/pleroma/http/tls.ex new file mode 100644 index 000000000..22718835b --- /dev/null +++ b/lib/pleroma/http/tls.ex @@ -0,0 +1,35 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HTTP.TLS do + @moduledoc """ + TLS client options that trust the CAStore bundle. + + Hackney otherwise trusts its own certifi bundle, and it drops all of its + default TLS options, including certificate checks, as soon as any + `ssl_options` are given. + """ + + @spec ssl_options(keyword()) :: keyword() + def ssl_options(overrides \\ []) do + Keyword.merge( + [ + verify: :verify_peer, + cacertfile: CAStore.file_path(), + depth: 20, + customize_hostname_check: [match_fun: :public_key.pkix_verify_hostname_match_fun(:https)] + ], + overrides + ) + end + + @spec put_hackney_ssl_options(keyword()) :: keyword() + def put_hackney_ssl_options(opts) do + if opts[:insecure] do + opts + else + Keyword.put(opts, :ssl_options, ssl_options(Keyword.get(opts, :ssl_options, []))) + end + end +end diff --git a/lib/pleroma/reverse_proxy/client/hackney.ex b/lib/pleroma/reverse_proxy/client/hackney.ex index 4c81a6225..9026b4586 100644 --- a/lib/pleroma/reverse_proxy/client/hackney.ex +++ b/lib/pleroma/reverse_proxy/client/hackney.ex @@ -28,9 +28,9 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do @impl true def request(method, url, headers, body, opts \\ []) do opts = - Keyword.put_new(opts, :path_encode_fun, fn path -> - path - end) + opts + |> Keyword.put_new(:path_encode_fun, fn path -> path end) + |> Pleroma.HTTP.TLS.put_hackney_ssl_options() if opts[:follow_redirect] != false do {_state, req_opts} = Access.get_and_update(opts, :follow_redirect, fn a -> {a, false} end) diff --git a/test/pleroma/http/adapter_helper/hackney_test.exs b/test/pleroma/http/adapter_helper/hackney_test.exs index 343bdb800..5bf13e25c 100644 --- a/test/pleroma/http/adapter_helper/hackney_test.exs +++ b/test/pleroma/http/adapter_helper/hackney_test.exs @@ -39,5 +39,20 @@ defmodule Pleroma.HTTP.AdapterHelper.HackneyTest do assert opts[:b] == 1 refute Keyword.has_key?(opts, :proxy) end + + test "trusts the CAStore bundle", %{uri: uri} do + opts = Hackney.options([], uri) + + assert opts[:ssl_options] == Pleroma.HTTP.TLS.ssl_options() + end + + test "keeps the CAStore bundle with configured ssl_options", %{uri: uri} do + clear_config([:http, :adapter], ssl_options: [versions: [:"tlsv1.2"]]) + + opts = Hackney.options([], uri) + + assert opts[:ssl_options][:versions] == [:"tlsv1.2"] + assert opts[:ssl_options][:cacertfile] == CAStore.file_path() + end end end diff --git a/test/pleroma/http/tls_test.exs b/test/pleroma/http/tls_test.exs new file mode 100644 index 000000000..7e9a85edd --- /dev/null +++ b/test/pleroma/http/tls_test.exs @@ -0,0 +1,47 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HTTP.TLSTest do + use ExUnit.Case, async: true + + alias Pleroma.HTTP.TLS + + describe "ssl_options/1" do + test "verifies peers against the CAStore bundle" do + opts = TLS.ssl_options() + + assert opts[:verify] == :verify_peer + assert opts[:cacertfile] == CAStore.file_path() + assert [match_fun: match_fun] = opts[:customize_hostname_check] + assert is_function(match_fun, 2) + end + + test "keeps the CA bundle when other options are given" do + opts = TLS.ssl_options(versions: [:"tlsv1.2"]) + + assert opts[:versions] == [:"tlsv1.2"] + assert opts[:cacertfile] == CAStore.file_path() + assert opts[:verify] == :verify_peer + end + end + + describe "put_hackney_ssl_options/1" do + test "adds the TLS options" do + opts = TLS.put_hackney_ssl_options(pool: :media) + + assert opts[:pool] == :media + assert opts[:ssl_options] == TLS.ssl_options() + end + + test "merges given ssl_options into the TLS options" do + opts = TLS.put_hackney_ssl_options(ssl_options: [versions: [:"tlsv1.2"]]) + + assert opts[:ssl_options] == TLS.ssl_options(versions: [:"tlsv1.2"]) + end + + test "leaves insecure requests alone" do + assert TLS.put_hackney_ssl_options(insecure: true) == [insecure: true] + end + end +end From 830455d197957b2b00d92e4f4cbdfc94f92d05fe Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Wed, 21 Oct 2026 08:21:43 +0400 Subject: [PATCH 361/391] fix(mailer): keep certificate checks for Swoosh HTTP adapters The TLS version override replaced all Hackney TLS defaults. On OTP 26+ this fails with cacerts: :undefined; on older OTP it skipped certificate checks. Closes #7962 on the public tracker. --- changelog.d/mailer-tls-options.fix | 1 + lib/pleroma/emails/mailer.ex | 4 +++- test/pleroma/emails/mailer_test.exs | 9 +++++++++ test/pleroma/user_test.exs | 4 +++- .../admin_api/controllers/admin_api_controller_test.exs | 4 +++- 5 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 changelog.d/mailer-tls-options.fix diff --git a/changelog.d/mailer-tls-options.fix b/changelog.d/mailer-tls-options.fix new file mode 100644 index 000000000..fe17eea6d --- /dev/null +++ b/changelog.d/mailer-tls-options.fix @@ -0,0 +1 @@ +Fix Swoosh HTTP mail adapters failing with `cacerts: :undefined` on OTP 26+ diff --git a/lib/pleroma/emails/mailer.ex b/lib/pleroma/emails/mailer.ex index 2a80f8547..58ef7de94 100644 --- a/lib/pleroma/emails/mailer.ex +++ b/lib/pleroma/emails/mailer.ex @@ -39,7 +39,9 @@ defmodule Pleroma.Emails.Mailer do # temporary hackney fix until hackney max_connections bug is fixed # https://git.pleroma.social/pleroma/pleroma/-/issues/2101 email = - Swoosh.Email.put_private(email, :hackney_options, ssl_options: [versions: [:"tlsv1.2"]]) + Swoosh.Email.put_private(email, :hackney_options, + ssl_options: Pleroma.HTTP.TLS.ssl_options(versions: [:"tlsv1.2"]) + ) case enabled?() do true -> Swoosh.Mailer.deliver(email, parse_config(config)) diff --git a/test/pleroma/emails/mailer_test.exs b/test/pleroma/emails/mailer_test.exs index 7a718c653..52b9c0687 100644 --- a/test/pleroma/emails/mailer_test.exs +++ b/test/pleroma/emails/mailer_test.exs @@ -52,4 +52,13 @@ defmodule Pleroma.Emails.MailerTest do subject: "Pleroma test email" ) end + + test "keeps certificate checks for HTTP API adapters" do + Mailer.deliver(@email) + + assert_received {:email, %Swoosh.Email{private: %{hackney_options: hackney_options}}} + + assert hackney_options[:ssl_options] == + Pleroma.HTTP.TLS.ssl_options(versions: [:"tlsv1.2"]) + end end diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs index 63c0c2bb8..f5e6e20fe 100644 --- a/test/pleroma/user_test.exs +++ b/test/pleroma/user_test.exs @@ -533,7 +533,9 @@ defmodule Pleroma.UserTest do Pleroma.Emails.UserEmail.account_confirmation_email(registered_user) # temporary hackney fix until hackney max_connections bug is fixed # https://git.pleroma.social/pleroma/pleroma/-/issues/2101 - |> Swoosh.Email.put_private(:hackney_options, ssl_options: [versions: [:"tlsv1.2"]]) + |> Swoosh.Email.put_private(:hackney_options, + ssl_options: Pleroma.HTTP.TLS.ssl_options(versions: [:"tlsv1.2"]) + ) |> assert_email_sent() end diff --git a/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs b/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs index fc578d828..07288caa8 100644 --- a/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs @@ -1044,7 +1044,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do Pleroma.Emails.UserEmail.account_confirmation_email(first_user) # temporary hackney fix until hackney max_connections bug is fixed # https://git.pleroma.social/pleroma/pleroma/-/issues/2101 - |> Swoosh.Email.put_private(:hackney_options, ssl_options: [versions: [:"tlsv1.2"]]) + |> Swoosh.Email.put_private(:hackney_options, + ssl_options: Pleroma.HTTP.TLS.ssl_options(versions: [:"tlsv1.2"]) + ) |> assert_email_sent() end From f4ff7cb1eaa23ecffaeb11e8b31417a6db0c4b78 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Wed, 21 Oct 2026 08:21:43 +0400 Subject: [PATCH 362/391] fix(http): make Finch trust the CAStore bundle Mint trusts the OTP CA store, which comes from the OS. Load the CAStore bundle into it when Finch starts, so Finch trusts the same roots as the other adapters. --- changelog.d/hackney-castore.security | 2 +- lib/pleroma/application.ex | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/changelog.d/hackney-castore.security b/changelog.d/hackney-castore.security index d30a53f80..44cb90ce6 100644 --- a/changelog.d/hackney-castore.security +++ b/changelog.d/hackney-castore.security @@ -1 +1 @@ -Hackney now trusts only the CAStore certificate bundle instead of its outdated certifi bundle, which still trusted the compromised eMudhra intermediate CAs +Hackney and Finch now trust only the CAStore certificate bundle, which rejects the compromised eMudhra intermediate CAs diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 353e91f72..adc054f3b 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -66,6 +66,7 @@ defmodule Pleroma.Application do if match?({Tesla.Adapter.Finch, _}, adapter) do Logger.info("Starting Finch") + use_castore_for_otp_cacerts() Finch.start_link(name: MyFinch) end @@ -304,6 +305,15 @@ defmodule Pleroma.Application do end end + # Mint trusts the OTP CA store, which comes from the OS. Make it the CAStore + # bundle that the other adapters trust. OTP 24 has no such store and Mint + # uses CAStore there anyway. + defp use_castore_for_otp_cacerts do + if Code.ensure_loaded?(:public_key) and function_exported?(:public_key, :cacerts_load, 1) do + :ok = :public_key.cacerts_load(CAStore.file_path()) + end + end + defp http_children_hackney do pools = [:federation, :media] From 235435fc9b8d8300b76e5b71e4c215fb465383fa Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 21 Oct 2026 11:16:24 +0200 Subject: [PATCH 363/391] Sec: Bump gun to version 2.7.0 and cowlib to version 2.21.0 Fixes: cowlib 2.20.0 - EEF-CVE-2026-43966 (MEDIUM) aka: CVE-2026-43966, GHSA-w4f7-4cxr-rv3c HTTP Response Splitting via Non-VCHAR Bytes in cow_http_struct_hd:escape_string/2 https://osv.dev/vulnerability/EEF-CVE-2026-43966 cowlib 2.20.0 - EEF-CVE-2026-43969 (LOW) aka: CVE-2026-43969, GHSA-g2wm-735q-3f56 Cookie Request Header Injection via Unvalidated Encoder in cow_cookie:cookie/1 https://osv.dev/vulnerability/EEF-CVE-2026-43969 --- changelog.d/cowlib-2.21.0.security | 1 + mix.lock | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog.d/cowlib-2.21.0.security diff --git a/changelog.d/cowlib-2.21.0.security b/changelog.d/cowlib-2.21.0.security new file mode 100644 index 000000000..b45bde712 --- /dev/null +++ b/changelog.d/cowlib-2.21.0.security @@ -0,0 +1 @@ +Update gun to version 2.7.0 and cowlib to version 2.21.0 resolving a header smuggling vulnerability diff --git a/mix.lock b/mix.lock index 838b56a5e..9abe2eb6f 100644 --- a/mix.lock +++ b/mix.lock @@ -22,7 +22,7 @@ "covertool": {:hex, :covertool, "2.0.7", "398be995c4cf1a2861174389b3577ca97beee43b60c8f1afcf510f1b07d32408", [:rebar3], [], "hexpm", "46158ed6e1a0df7c0a912e314c7b8e053bd74daa5fc6b790614922a155b5720c"}, "cowboy": {:hex, :cowboy, "2.19.0", "78b9d92d25a23e56d7341040b67946c04067cd77de966e2372865f673a8e6f59", [:make, :rebar3], [{:cowlib, ">= 2.20.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "986dae81f99fcb78ef2d8efc21d738cee189410840eeaf32eca84ada81dcf6d4"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, - "cowlib": {:hex, :cowlib, "2.20.0", "bb525377ba634cd6d68bac7bff5d98571f738806139d335daca827717c5dc172", [:make, :rebar3], [], "hexpm", "7d41a0dd2c093041ff3779ac5fe8a1585a68ec7cb2dd1de0536bdd2452fd7ba1"}, + "cowlib": {:hex, :cowlib, "2.21.0", "3ea757c79784bdbdebac7857ce3adf2290af6b8680d4248c75e40ced2e98c89e", [:make, :rebar3], [], "hexpm", "0960248773762a5319b3c17285028bc76f54f58ef0f9451367401bdf67b46274"}, "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, "crontab": {:hex, :crontab, "1.1.8", "2ce0e74777dfcadb28a1debbea707e58b879e6aa0ffbf9c9bb540887bce43617", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, "custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"}, @@ -57,7 +57,7 @@ "floki": {:hex, :floki, "0.38.0", "62b642386fa3f2f90713f6e231da0fa3256e41ef1089f83b6ceac7a3fd3abf33", [:mix], [], "hexpm", "a5943ee91e93fb2d635b612caf5508e36d37548e84928463ef9dd986f0d1abd9"}, "gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"}, "gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"}, - "gun": {:hex, :gun, "2.6.0", "4672fe3d2e056d9314a4639db288b7e9ce0fe6be8f2f4dd4adba7d2b56f7927c", [:make, :rebar3], [{:cowlib, ">= 2.20.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "69bb957f4bd6c475b5714460fd65c26fc7eb85760aa64b74754de36ce5cced7d"}, + "gun": {:hex, :gun, "2.7.0", "8dfc20ff1b70f3c78f07122f7d82ac6950c40d33a8a9b35da0134f49fb35b334", [:make, :rebar3], [{:cowlib, ">= 2.21.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "2a90d073069e72654b6c9a91fae365b746ff5ce385a4a5bbf71ef53a8bf95d82"}, "h2": {:hex, :h2, "0.11.0", "b8c9f7c86f400582c5cbed59f79c0efd6cf49ad6478c210bede91d2d15019b14", [:rebar3], [], "hexpm", "c61f24361d0dc76375eaf35c2bf0e01235d81b7685538df571db036467fac358"}, "hackney": {:hex, :hackney, "4.7.3", "42feec1a4986000411cb5d389bbc9fb61d9dff84c5eef42b11fc40ea327e6f9a", [:rebar3], [{:certifi, "~> 2.17.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:h2, "~> 0.11.0", [hex: :h2, repo: "hexpm", optional: false]}, {:idna, "~> 7.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.5", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.2", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:webtransport, "~> 0.4.4", [hex: :webtransport, repo: "hexpm", optional: false]}], "hexpm", "3f460e01d5a1abba39c32d06e9a733bb76e35d1ab87a83868b6bb29d646fe25f"}, "hpax": {:hex, :hpax, "1.0.4", "777de5d433b0fbdc7c418159c8055910faa8047ffdb3d6b31098d2a46cd7685c", [:mix], [], "hexpm", "afc7cb142ebcc2d01ce7816190b98ce5dd49e799111b24249f3443d730f377ca"}, From b4a3e8436dd762bbdb4f0c2b821921b907ae9fbd Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 21 Oct 2026 11:21:52 +0200 Subject: [PATCH 364/391] Sec: Update bandit to version 1.13.1 Fixes: bandit 1.12.5 - EEF-CVE-2026-105659 (HIGH) aka: CVE-2026-105659, GHSA-69m6-wcv4-rwcf HTTP/2 blocked-send deadlines reset on every WINDOW_UPDATE, letting a trickle pin stream processes indefinitely in Bandit https://osv.dev/vulnerability/EEF-CVE-2026-105659 bandit 1.12.5 - EEF-CVE-2026-105660 (MEDIUM) aka: CVE-2026-105660, GHSA-v53v-9hvg-37jf Unvalidated HTTP/1 request-target authority lets a client choose conn.host and conn.port in Bandit https://osv.dev/vulnerability/EEF-CVE-2026-105660 --- changelog.d/bandit-1.13.1.security | 1 + mix.lock | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 changelog.d/bandit-1.13.1.security diff --git a/changelog.d/bandit-1.13.1.security b/changelog.d/bandit-1.13.1.security new file mode 100644 index 000000000..3926b1772 --- /dev/null +++ b/changelog.d/bandit-1.13.1.security @@ -0,0 +1 @@ +Update bandit to version 1.13.1 resolving a stuck process and unvalidated HTTP/1 headers vulnerabilities diff --git a/mix.lock b/mix.lock index 9abe2eb6f..f8c0d48ac 100644 --- a/mix.lock +++ b/mix.lock @@ -1,7 +1,7 @@ %{ "accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm", "11b18c220bcc2eab63b5470c038ef10eb6783bcb1fcdb11aa4137defa5ac1bb8"}, "argon2_elixir": {:hex, :argon2_elixir, "4.1.3", "4f28318286f89453364d7fbb53e03d4563fd7ed2438a60237eba5e426e97785f", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "7c295b8d8e0eaf6f43641698f962526cdf87c6feb7d14bd21e599271b510608c"}, - "bandit": {:hex, :bandit, "1.12.5", "af205a8e550f304caae09a97d29fd3c79a7f337526ea7cd772d2ff11d2f7c800", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.5", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "c5684ca062fa407cac115aec3256383f3e2ec9fdced7904d59cf5a7bb7ed6181"}, + "bandit": {:hex, :bandit, "1.13.1", "1ce25a7beb750ed3fa2e905962f1f9a679cb585f1da0ae45637d06e385ce2eae", [:mix], [{:hpax, "~> 1.1", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.6", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "39028f0b5a21404661410a6c19275060a9822c9dcca70b7c3e4e4fa50b11da58"}, "base62": {:hex, :base62, "1.2.2", "85c6627eb609317b70f555294045895ffaaeb1758666ab9ef9ca38865b11e629", [:mix], [{:custom_base, "~> 0.2.1", [hex: :custom_base, repo: "hexpm", optional: false]}], "hexpm", "d41336bda8eaa5be197f1e4592400513ee60518e5b9f4dcf38f4b4dae6f377bb"}, "bbcode_pleroma": {:hex, :bbcode_pleroma, "0.2.0", "d36f5bca6e2f62261c45be30fa9b92725c0655ad45c99025cb1c3e28e25803ef", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "19851074419a5fedb4ef49e1f01b30df504bb5dbb6d6adfc135238063bebd1c3"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "2.3.1", "5114d780459a04f2b4aeef52307de23de961b69e13a5cd98a911e39fda13f420", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "42182d5f46764def15bf9af83739e3bf4ad22661b1c34fc3e88558efced07279"}, @@ -60,7 +60,7 @@ "gun": {:hex, :gun, "2.7.0", "8dfc20ff1b70f3c78f07122f7d82ac6950c40d33a8a9b35da0134f49fb35b334", [:make, :rebar3], [{:cowlib, ">= 2.21.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "2a90d073069e72654b6c9a91fae365b746ff5ce385a4a5bbf71ef53a8bf95d82"}, "h2": {:hex, :h2, "0.11.0", "b8c9f7c86f400582c5cbed59f79c0efd6cf49ad6478c210bede91d2d15019b14", [:rebar3], [], "hexpm", "c61f24361d0dc76375eaf35c2bf0e01235d81b7685538df571db036467fac358"}, "hackney": {:hex, :hackney, "4.7.3", "42feec1a4986000411cb5d389bbc9fb61d9dff84c5eef42b11fc40ea327e6f9a", [:rebar3], [{:certifi, "~> 2.17.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:h2, "~> 0.11.0", [hex: :h2, repo: "hexpm", optional: false]}, {:idna, "~> 7.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.5", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.2", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:webtransport, "~> 0.4.4", [hex: :webtransport, repo: "hexpm", optional: false]}], "hexpm", "3f460e01d5a1abba39c32d06e9a733bb76e35d1ab87a83868b6bb29d646fe25f"}, - "hpax": {:hex, :hpax, "1.0.4", "777de5d433b0fbdc7c418159c8055910faa8047ffdb3d6b31098d2a46cd7685c", [:mix], [], "hexpm", "afc7cb142ebcc2d01ce7816190b98ce5dd49e799111b24249f3443d730f377ca"}, + "hpax": {:hex, :hpax, "1.1.0", "782931867cc23217c68fb5f68fe1a11f5e7544c7fda82c8a7019a5df5a4a1cdf", [:mix], [], "hexpm", "0b8d0f05832f55571d65ac720f79bf8994138ffbb133209dc4685eae0ad456a8"}, "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, "http_signatures": {:hex, :http_signatures, "0.1.3", "19f26aee35b4684e37efdce3ac4638605e6e41a04368186bd39d2b6138a60ea9", [:mix], [{:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "20313a65516db88006f85b090f6f76cc5b04e9609b45943657e6781eb91174f4"}, "httpoison": {:hex, :httpoison, "3.0.0", "8566a933bb9175236d1ec335978445b67cd1f5b5d3ead6ca4b80be469d41f5d9", [:mix], [{:hackney, "~> 4.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "9130197b7658901c493d6fcfb842fb9676300fa8a6c8ed058c8889cf1a77f3c2"}, @@ -146,7 +146,7 @@ "telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.2.1", "c9755987d7b959b557084e6990990cb96a50d6482c683fb9622a63837f3cd3d8", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "5e2c599da4983c4f88a33e9571f1458bf98b0cf6ba930f1dc3a6e8cf45d5afb6"}, "telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"}, "tesla": {:hex, :tesla, "1.20.0", "922b504e186da2ee82990d44e7422435f41b67403908a00340c3a0b23a17aa90", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21 or >= 4.0.2 and < 5.0.0-0", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [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]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:opentelemetry_semantic_conventions, "~> 1.27", [hex: :opentelemetry_semantic_conventions, 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", "3ecb41cb458772332752c3acdfe983e23abb991f5a43cfd69a64e9ea3f4b0061"}, - "thousand_island": {:hex, :thousand_island, "1.5.0", "f50a213cac97262b6d5ebb85745aa2c00fec1413191e6e66834788d45425cecb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "708923d40523e43cf99041ab37a0d4b0ec426ac6438fa3716ab23d919eaeb412"}, + "thousand_island": {:hex, :thousand_island, "1.6.0", "d6ad7c2220c96a270cc8ca1586ec4d8bde648a4a3a53f003ce093f41beec0288", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4d4720470f091fc21a1311777c2ac66916c43621bffc068590c9f9bb3cd057a3"}, "timex": {:hex, :timex, "3.7.7", "3ed093cae596a410759104d878ad7b38e78b7c2151c6190340835515d4a46b8a", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "0ec4b09f25fe311321f9fc04144a7e3affe48eb29481d7a5583849b6c4dfa0a7"}, "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.0.5", "69f1ee029a49afa04ad77801febaf69385f3d3e3d1e4b56b9469025677b89a28", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "55519aa2a99e5d2095c1e61cc74c9be69688f8ab75c27da724eb8279ff402a5a"}, From e7b88cd65241dff9b13f822d024825e7714b395d Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 21 Oct 2026 11:24:36 +0200 Subject: [PATCH 365/391] Sec: Update mint to version 1.11.0 Fixes: mint 1.10.0 - EEF-CVE-2026-92103 (MEDIUM) aka: CVE-2026-92103, GHSA-q95c-ccq6-j5j6 Mint HTTP/2 client buffers oversized frames up to 16 MiB before enforcing max_frame_size https://osv.dev/vulnerability/EEF-CVE-2026-92103 mint 1.10.0 - EEF-CVE-2026-82672 (MEDIUM) aka: CVE-2026-82672, GHSA-rj5m-69wp-cxq9 Unvalidated chunk-size line tail in Mint HTTP/1 client enables response smuggling against strict intermediaries on pooled connections https://osv.dev/vulnerability/EEF-CVE-2026-82672 mint 1.10.0 - EEF-CVE-2026-91043 (HIGH) aka: CVE-2026-91043, GHSA-9x8p-qrf4-jq7g HPACK-indexed cookie fields in Mint HTTP/2 responses bypass max_header_list_size and exhaust client memory https://osv.dev/vulnerability/EEF-CVE-2026-91043 mint 1.10.0 - EEF-CVE-2026-94194 (MEDIUM) aka: CVE-2026-94194, GHSA-gvrc-75rc-7gj9 Mint HTTP/1 client applies chunked framing when chunked is not the final transfer coding, enabling response smuggling through intermediaries https://osv.dev/vulnerability/EEF-CVE-2026-94194 --- changelog.d/mint-1.11.0.security | 1 + mix.lock | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/mint-1.11.0.security diff --git a/changelog.d/mint-1.11.0.security b/changelog.d/mint-1.11.0.security new file mode 100644 index 000000000..78098c0e3 --- /dev/null +++ b/changelog.d/mint-1.11.0.security @@ -0,0 +1 @@ +Update mint to version 1.11.0 resolving multiple vulnerabilities diff --git a/mix.lock b/mix.lock index f8c0d48ac..3552eb8cd 100644 --- a/mix.lock +++ b/mix.lock @@ -81,7 +81,7 @@ "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"}, "mimerl": {:hex, :mimerl, "1.5.0", "f35aca6f23242339b3666e0ac0702379e362b469d0aea167f6cc713547e777ed", [:rebar3], [], "hexpm", "db648ce065bae14ea84ca8b5dd123f42f49417cef693541110bf6f9e9be9ecc4"}, - "mint": {:hex, :mint, "1.10.0", "85af3353bfc504f5bdfe494bd92b8490f87a306dc659ee1ad0af435107e898dc", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "8b16fb72aaa7531d206a1f05e4cc85509ba531ccec7a17a22736c9c95cbb24d1"}, + "mint": {:hex, :mint, "1.11.0", "a713551624815c0435237b93d90ea8b9b14254690c66f732d0ef8930f76ff1d9", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "c6279ba2d6aa3a383a1d4cfbe7b59f42e6efd400f58d8e2acfeac48a438693ab"}, "mix_audit": {:hex, :mix_audit, "2.1.5", "c0f77cee6b4ef9d97e37772359a187a166c7a1e0e08b50edf5bf6959dfe5a016", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "87f9298e21da32f697af535475860dc1d3617a010e0b418d2ec6142bc8b42d69"}, "mochiweb": {:hex, :mochiweb, "2.18.0", "eb55f1db3e6e960fac4e6db4e2db9ec3602cc9f30b86cd1481d56545c3145d2e", [:rebar3], [], "hexpm"}, "mock": {:hex, :mock, "0.3.9", "10e44ad1f5962480c5c9b9fa779c6c63de9bd31997c8e04a853ec990a9d841af", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "9e1b244c4ca2551bb17bb8415eed89e40ee1308e0fbaed0a4fdfe3ec8a4adbd3"}, From 5810ecd16ed54c62a7599c5d5e494f34aa32bd38 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 21 Oct 2026 11:26:46 +0200 Subject: [PATCH 366/391] Sec: Update Decimal to version 3.2.0 Fixes: decimal 3.1.1 - EEF-CVE-2026-97853 (MEDIUM) aka: CVE-2026-97853, GHSA-6c27-994x-c52f Unbounded allocation in decimal Decimal.round/3 driven by the places argument enables DoS https://osv.dev/vulnerability/EEF-CVE-2026-97853 --- changelog.d/decimal-3.2.0.security | 1 + mix.lock | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/decimal-3.2.0.security diff --git a/changelog.d/decimal-3.2.0.security b/changelog.d/decimal-3.2.0.security new file mode 100644 index 000000000..fc20b3f50 --- /dev/null +++ b/changelog.d/decimal-3.2.0.security @@ -0,0 +1 @@ +Update Decimal to version 3.2.0 resolving an unbounded allocation vulnerability diff --git a/mix.lock b/mix.lock index 3552eb8cd..9b3b1714a 100644 --- a/mix.lock +++ b/mix.lock @@ -27,7 +27,7 @@ "crontab": {:hex, :crontab, "1.1.8", "2ce0e74777dfcadb28a1debbea707e58b879e6aa0ffbf9c9bb540887bce43617", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, "custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"}, "db_connection": {:hex, :db_connection, "2.10.1", "d5465f6bcc125c1b8981c1dbf23c193ca16f446ec0b25832dc174f74f18be510", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "18ed94c6e627b4bf452dbd4df61b69a35a1e768525140bc1917b7a685026a6a3"}, - "decimal": {:hex, :decimal, "3.1.1", "430d87b04011ce6cbd4fd205be758311a81f87d552d40904abd00f015935b1d0", [:mix], [], "hexpm", "c5f25f2ced74a0587d03e6023f595db8e924c9d3922c8c8ffd9edfc4498cf1f6"}, + "decimal": {:hex, :decimal, "3.2.0", "f1235f05d841a211fda78a08fdb9777f2ff51a71f18455233fae186ff76187c6", [:mix], [], "hexpm", "190bca80580ce61aa69bb0eacadd7677710205c770225c2c2c07e32a5c31b234"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, "dialyxir": {:hex, :dialyxir, "1.4.6", "7cca478334bf8307e968664343cbdb432ee95b4b68a9cba95bdabb0ad5bdfd9a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "8cf5615c5cd4c2da6c501faae642839c8405b49f8aa057ad4ae401cb808ef64d"}, "earmark_parser": {:hex, :earmark_parser, "1.4.46", "67607a0532e810c6f630a515c548d0b24949643f168cc556303bee4cf96105c7", [:mix], [], "hexpm", "9c44636e8a1c68c62f526b2dcd85d941dbbcee7ab82cf64ba06ce28bef8e89f5"}, From 92a112cf15dc73a7ac88737c4d407745e47db89d Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 22 Oct 2026 08:48:56 +0400 Subject: [PATCH 367/391] fix(mailer): drop the TLS 1.2 pin The pin worked around a TLS 1.3 problem with Hackney on OTP 23 (swoosh/swoosh#478). Pleroma no longer supports OTP 23. --- lib/pleroma/emails/mailer.ex | 5 ++--- test/pleroma/emails/mailer_test.exs | 2 +- test/pleroma/user_test.exs | 4 +--- .../web/admin_api/controllers/admin_api_controller_test.exs | 4 +--- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/lib/pleroma/emails/mailer.ex b/lib/pleroma/emails/mailer.ex index 58ef7de94..fa6fd8020 100644 --- a/lib/pleroma/emails/mailer.ex +++ b/lib/pleroma/emails/mailer.ex @@ -36,11 +36,10 @@ defmodule Pleroma.Emails.Mailer do def deliver(email, config \\ []) def deliver(email, config) do - # temporary hackney fix until hackney max_connections bug is fixed - # https://git.pleroma.social/pleroma/pleroma/-/issues/2101 + # Swoosh HTTP adapters use Hackney, which otherwise trusts its certifi bundle email = Swoosh.Email.put_private(email, :hackney_options, - ssl_options: Pleroma.HTTP.TLS.ssl_options(versions: [:"tlsv1.2"]) + ssl_options: Pleroma.HTTP.TLS.ssl_options() ) case enabled?() do diff --git a/test/pleroma/emails/mailer_test.exs b/test/pleroma/emails/mailer_test.exs index 52b9c0687..2c2b5a4af 100644 --- a/test/pleroma/emails/mailer_test.exs +++ b/test/pleroma/emails/mailer_test.exs @@ -59,6 +59,6 @@ defmodule Pleroma.Emails.MailerTest do assert_received {:email, %Swoosh.Email{private: %{hackney_options: hackney_options}}} assert hackney_options[:ssl_options] == - Pleroma.HTTP.TLS.ssl_options(versions: [:"tlsv1.2"]) + Pleroma.HTTP.TLS.ssl_options() end end diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs index f5e6e20fe..ec5a2a12a 100644 --- a/test/pleroma/user_test.exs +++ b/test/pleroma/user_test.exs @@ -531,10 +531,8 @@ defmodule Pleroma.UserTest do ObanHelpers.perform_all() Pleroma.Emails.UserEmail.account_confirmation_email(registered_user) - # temporary hackney fix until hackney max_connections bug is fixed - # https://git.pleroma.social/pleroma/pleroma/-/issues/2101 |> Swoosh.Email.put_private(:hackney_options, - ssl_options: Pleroma.HTTP.TLS.ssl_options(versions: [:"tlsv1.2"]) + ssl_options: Pleroma.HTTP.TLS.ssl_options() ) |> assert_email_sent() end diff --git a/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs b/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs index 07288caa8..aa38a7454 100644 --- a/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs @@ -1042,10 +1042,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do ObanHelpers.perform_all() Pleroma.Emails.UserEmail.account_confirmation_email(first_user) - # temporary hackney fix until hackney max_connections bug is fixed - # https://git.pleroma.social/pleroma/pleroma/-/issues/2101 |> Swoosh.Email.put_private(:hackney_options, - ssl_options: Pleroma.HTTP.TLS.ssl_options(versions: [:"tlsv1.2"]) + ssl_options: Pleroma.HTTP.TLS.ssl_options() ) |> assert_email_sent() end From 65076aa2f88a9ba25adc974a586b7d6556135d45 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 22 Oct 2026 10:05:12 +0400 Subject: [PATCH 368/391] fix(http): reject Hackney's certifi partial chains Hackney 4 merges given ssl_options into its defaults and keeps its own partial_chain, which trusts any certificate in the peer chain that is in certifi. A peer could send the emSign ECC Root CA - G3 root in its chain and pass. Set partial_chain so certifi is never a trust anchor. --- lib/pleroma/http/tls.ex | 12 +++++++++--- test/pleroma/http/tls_test.exs | 6 ++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/http/tls.ex b/lib/pleroma/http/tls.ex index 22718835b..e1fbc7caa 100644 --- a/lib/pleroma/http/tls.ex +++ b/lib/pleroma/http/tls.ex @@ -6,9 +6,11 @@ defmodule Pleroma.HTTP.TLS do @moduledoc """ TLS client options that trust the CAStore bundle. - Hackney otherwise trusts its own certifi bundle, and it drops all of its + Hackney otherwise trusts its own certifi bundle. Hackney 1 drops all of its default TLS options, including certificate checks, as soon as any - `ssl_options` are given. + `ssl_options` are given. Hackney 4 merges them, so `partial_chain` must be + set too: Hackney's own one trusts any certificate in the peer chain that is + in certifi. """ @spec ssl_options(keyword()) :: keyword() @@ -18,12 +20,16 @@ defmodule Pleroma.HTTP.TLS do verify: :verify_peer, cacertfile: CAStore.file_path(), depth: 20, - customize_hostname_check: [match_fun: :public_key.pkix_verify_hostname_match_fun(:https)] + customize_hostname_check: [match_fun: :public_key.pkix_verify_hostname_match_fun(:https)], + partial_chain: &__MODULE__.reject_partial_chain/1 ], overrides ) end + @doc false + def reject_partial_chain(_certs), do: :unknown_ca + @spec put_hackney_ssl_options(keyword()) :: keyword() def put_hackney_ssl_options(opts) do if opts[:insecure] do diff --git a/test/pleroma/http/tls_test.exs b/test/pleroma/http/tls_test.exs index 7e9a85edd..490950b50 100644 --- a/test/pleroma/http/tls_test.exs +++ b/test/pleroma/http/tls_test.exs @@ -17,6 +17,12 @@ defmodule Pleroma.HTTP.TLSTest do assert is_function(match_fun, 2) end + test "does not accept certificates from the peer chain as trust anchors" do + partial_chain = TLS.ssl_options()[:partial_chain] + + assert partial_chain.([:certificate]) == :unknown_ca + end + test "keeps the CA bundle when other options are given" do opts = TLS.ssl_options(versions: [:"tlsv1.2"]) From d698b9657914d9557f6c382ec5657e702ae1c5cf Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 22 Oct 2026 11:21:00 +0400 Subject: [PATCH 369/391] fix(search): make the Qdrant clients trust the CAStore bundle The Qdrant and OpenAI clients do not go through Pleroma.HTTP, so on Hackney they trusted certifi. Both send API keys. Give them the configured adapter with the same TLS options. --- lib/pleroma/http/adapter_helper.ex | 18 ++++++++++ lib/pleroma/search/qdrant_search.ex | 4 +-- .../adapter_helper/tesla_adapter_test.exs | 34 +++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 test/pleroma/http/adapter_helper/tesla_adapter_test.exs diff --git a/lib/pleroma/http/adapter_helper.ex b/lib/pleroma/http/adapter_helper.ex index 32c1080f7..4f0e23f20 100644 --- a/lib/pleroma/http/adapter_helper.ex +++ b/lib/pleroma/http/adapter_helper.ex @@ -46,6 +46,24 @@ defmodule Pleroma.HTTP.AdapterHelper do |> adapter_helper().options(uri) end + @doc """ + The configured Tesla adapter with TLS options, for clients that do not go + through `Pleroma.HTTP`. + """ + @spec tesla_adapter() :: module() | {module(), keyword()} + def tesla_adapter do + case adapter() do + Tesla.Adapter.Hackney -> + {Tesla.Adapter.Hackney, Pleroma.HTTP.TLS.put_hackney_ssl_options([])} + + Tesla.Adapter.Gun -> + {Tesla.Adapter.Gun, certificates_verification: true} + + adapter -> + adapter + end + end + defp adapter, do: Application.get_env(:tesla, :adapter) defp adapter_helper do diff --git a/lib/pleroma/search/qdrant_search.ex b/lib/pleroma/search/qdrant_search.ex index 4d57cfa88..a8c5a3eb9 100644 --- a/lib/pleroma/search/qdrant_search.ex +++ b/lib/pleroma/search/qdrant_search.ex @@ -160,7 +160,7 @@ defmodule Pleroma.Search.QdrantSearch.OpenAIClient do end defp client do - Tesla.client(middleware()) + Tesla.client(middleware(), Pleroma.HTTP.AdapterHelper.tesla_adapter()) end defp middleware do @@ -191,7 +191,7 @@ defmodule Pleroma.Search.QdrantSearch.QdrantClient do end defp client do - Tesla.client(middleware()) + Tesla.client(middleware(), Pleroma.HTTP.AdapterHelper.tesla_adapter()) end defp middleware do diff --git a/test/pleroma/http/adapter_helper/tesla_adapter_test.exs b/test/pleroma/http/adapter_helper/tesla_adapter_test.exs new file mode 100644 index 000000000..1ee2f13ba --- /dev/null +++ b/test/pleroma/http/adapter_helper/tesla_adapter_test.exs @@ -0,0 +1,34 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HTTP.AdapterHelper.TeslaAdapterTest do + use ExUnit.Case, async: false + + alias Pleroma.HTTP.AdapterHelper + alias Pleroma.HTTP.TLS + + setup do + adapter = Application.get_env(:tesla, :adapter) + on_exit(fn -> Application.put_env(:tesla, :adapter, adapter) end) + end + + test "adds the TLS options to Hackney" do + Application.put_env(:tesla, :adapter, Tesla.Adapter.Hackney) + + assert AdapterHelper.tesla_adapter() == + {Tesla.Adapter.Hackney, ssl_options: TLS.ssl_options()} + end + + test "turns on certificate checks for Gun" do + Application.put_env(:tesla, :adapter, Tesla.Adapter.Gun) + + assert AdapterHelper.tesla_adapter() == {Tesla.Adapter.Gun, certificates_verification: true} + end + + test "keeps other adapters" do + Application.put_env(:tesla, :adapter, {Tesla.Adapter.Finch, name: MyFinch}) + + assert AdapterHelper.tesla_adapter() == {Tesla.Adapter.Finch, name: MyFinch} + end +end From accadd99f38fb9b15d67a85db79e4f4550e97ef4 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 22 Oct 2026 11:21:50 +0400 Subject: [PATCH 370/391] fix(mailer): make Mua trust the CAStore bundle Mua (the SMTP adapter in the docs) trusts the OS CA store unless it gets a CA. Pass the CAStore bundle, but keep a CA that the admin set. --- changelog.d/hackney-castore.security | 2 +- lib/pleroma/emails/mailer.ex | 17 +++++++++++++++- test/pleroma/emails/mailer_test.exs | 29 ++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/changelog.d/hackney-castore.security b/changelog.d/hackney-castore.security index 44cb90ce6..8d1a0cf4a 100644 --- a/changelog.d/hackney-castore.security +++ b/changelog.d/hackney-castore.security @@ -1 +1 @@ -Hackney and Finch now trust only the CAStore certificate bundle, which rejects the compromised eMudhra intermediate CAs +Hackney, Finch, Mua and the Swoosh HTTP mail adapters now trust only the CAStore certificate bundle, which rejects the compromised eMudhra intermediate CAs diff --git a/lib/pleroma/emails/mailer.ex b/lib/pleroma/emails/mailer.ex index fa6fd8020..e6abf4456 100644 --- a/lib/pleroma/emails/mailer.ex +++ b/lib/pleroma/emails/mailer.ex @@ -68,6 +68,21 @@ defmodule Pleroma.Emails.Mailer do end defp parse_config(config) do - Swoosh.Mailer.parse_config(@otp_app, __MODULE__, @mailer_config, config) + @otp_app + |> Swoosh.Mailer.parse_config(__MODULE__, @mailer_config, config) + |> put_mua_cacertfile() + end + + @doc false + # Mua otherwise trusts the OS CA store. Keep a CA that the admin set. + def put_mua_cacertfile(config) do + ssl = Keyword.get(config, :ssl, []) + + if config[:adapter] == Swoosh.Adapters.Mua and + not Keyword.has_key?(ssl, :cacertfile) and not Keyword.has_key?(ssl, :cacerts) do + Keyword.put(config, :ssl, [{:cacertfile, CAStore.file_path()} | ssl]) + else + config + end end end diff --git a/test/pleroma/emails/mailer_test.exs b/test/pleroma/emails/mailer_test.exs index 2c2b5a4af..d42e8b723 100644 --- a/test/pleroma/emails/mailer_test.exs +++ b/test/pleroma/emails/mailer_test.exs @@ -61,4 +61,33 @@ defmodule Pleroma.Emails.MailerTest do assert hackney_options[:ssl_options] == Pleroma.HTTP.TLS.ssl_options() end + + describe "put_mua_cacertfile/1" do + test "makes Mua trust the CAStore bundle" do + config = Mailer.put_mua_cacertfile(adapter: Swoosh.Adapters.Mua, relay: "mail.example.com") + + assert config[:ssl] == [cacertfile: CAStore.file_path()] + assert config[:relay] == "mail.example.com" + end + + test "keeps other ssl options" do + config = Mailer.put_mua_cacertfile(adapter: Swoosh.Adapters.Mua, ssl: [depth: 3]) + + assert config[:ssl] == [cacertfile: CAStore.file_path(), depth: 3] + end + + test "keeps a CA that the admin set" do + config = [adapter: Swoosh.Adapters.Mua, ssl: [cacertfile: "/etc/ssl/smtp.pem"]] + assert Mailer.put_mua_cacertfile(config) == config + + config = [adapter: Swoosh.Adapters.Mua, ssl: [cacerts: []]] + assert Mailer.put_mua_cacertfile(config) == config + end + + test "leaves other adapters alone" do + config = [adapter: Swoosh.Adapters.SMTP, relay: "mail.example.com"] + + assert Mailer.put_mua_cacertfile(config) == config + end + end end From f4a3bc230068088f38a71e58df7f4fe27e75e938 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 22 Oct 2026 11:23:51 +0400 Subject: [PATCH 371/391] Static: Update bundled frontend to 2.11.4 --- priv/static/index.html | 41 +- priv/static/static/css/about.BPcl3NxB.css | 1 + .../static/css/account_actions.DjGDDSEq.css | 1 + .../css/announcements_page.DvITSMCQ.css | 1 + .../static/css/attributes_helper.DsgIU8rn.css | 1 + .../static/css/basic_user_card.CXE-NVII.css | 1 + .../css/bookmark_folder_edit.BYhGGKc2.css | 1 + .../static/css/bookmark_folders.CitandGk.css | 1 + priv/static/static/css/chat.6N-tF5xA.css | 1 + priv/static/static/css/chat_list.WHtUK6wd.css | 1 + .../static/static/css/chat_title.B1JBlEsZ.css | 1 + priv/static/static/css/checkbox.DY0WQ--N.css | 1 + .../static/css/confirm_modal.CxXYsWAn.css | 1 + .../static/css/dialog_modal.DC5hTqSH.css | 1 + priv/static/static/css/drafts.CvPb_YQj.css | 1 + .../static/css/edit_status_modal.egG_jIir.css | 1 + .../static/css/emoji_picker.CJFAWWOw.css | 1 + .../static/css/error_modal.D1oWQ5nz.css | 1 + priv/static/static/css/flash.BYNCHsIH.css | 1 + .../static/css/folder_card.CTk_063i.css | 1 + .../static/css/follow_card.jWN765qC.css | 1 + .../static/css/follow_requests.5W1jgYR0.css | 1 + .../static/css/image_cropper.iV4LurEO.css | 1 + .../interface_language_switcher.DYf3Vv_u.css | 1 + priv/static/static/css/list.Cau2FUIa.css | 1 + priv/static/static/css/lists.R2bOz6oY.css | 1 + .../static/static/css/lists_edit.D3Eu2cDo.css | 1 + priv/static/static/css/main.0Iw0byjp.css | 1 + priv/static/static/css/main.CF7uYwoC.css | 1 - .../static/css/media_modal.Bc6XAjzi.css | 1 + priv/static/static/css/modal.Dv3V-p-W.css | 1 + .../static/css/moderation_tools.DoZVYsfl.css | 1 + .../static/css/mute_confirm.p-qSlYOr.css | 1 + .../static/css/notifications.CW0-QQtF.css | 1 + .../static/css/panel_loading.DQYYmPMZ.css | 1 + .../static/css/password_reset.BLv39QBH.css | 1 + priv/static/static/css/poll_form.s0YHqxUy.css | 1 + priv/static/static/css/popover.BlbX_Uwp.css | 1 + .../static/css/post_status_modal.C4svyRW2.css | 1 + priv/static/static/css/quote.DAX2X1-h.css | 1 + .../static/static/css/quote_form.ix5T3mDK.css | 1 + .../static/css/registration.BpinXGMn.css | 1 + .../static/css/remote_follow.BrTyl75C.css | 1 + priv/static/static/css/search.BMRqSZdz.css | 1 + priv/static/static/css/select.BF2ygbkJ.css | 1 + .../static/css/settings_modal._8lsImIr.css | 1 + .../static/css/settings_modal.dQ5YFZGx.css | 1 - .../settings_modal_admin_content.CKTEQiM8.css | 1 + .../settings_modal_admin_content.d1pRGdhX.css | 1 - .../settings_modal_user_content.7KpKOwxU.css | 1 - .../settings_modal_user_content.DUcdr5Gz.css | 1 + .../static/css/shout_panel.BHBsqket.css | 1 + .../static/css/side_drawer.2CnDtAJj.css | 1 + .../css/status_history_modal.CZzNzXP5.css | 1 + .../static/css/sticker_picker.BzJVB0Ta.css | 1 - .../static/css/sticker_picker.C2lbaygI.css | 1 + .../static/css/string_setting.CtukauKH.css | 1 - .../static/css/tab_switcher.CEadvXLu.css | 1 + .../css/terms_of_service_panel.ClR_b2MD.css | 1 + .../css/update_notification.BJ1CAGCw.css | 1 - .../css/update_notification.Co8luhDO.css | 1 + priv/static/static/css/user_card.C2QiPTUj.css | 1 + priv/static/static/css/user_note.A3UYQeEG.css | 1 + .../static/css/user_profile.CC-U32e-.css | 1 + .../css/user_reporting_modal.CVdwO3Sc.css | 1 + .../css/user_timed_filter_modal.ClNyvF9i.css | 1 + .../css/vertical_tab_switcher.DU3QPNwr.css | 1 + .../css/who_to_follow_panel.BP23-WSI.css | 1 + .../js/_plugin-vue_export-helper.By3-ojQa.js | 2 + .../_plugin-vue_export-helper.By3-ojQa.js.map | 1 + priv/static/static/js/about.Origtmpb.js | 2 + priv/static/static/js/about.Origtmpb.js.map | 1 + .../static/js/account_actions.iKah8h7y.js | 3 + .../static/js/account_actions.iKah8h7y.js.map | 1 + priv/static/static/js/admin.Dy10Wzep.js | 2 + priv/static/static/js/admin.Dy10Wzep.js.map | 1 + .../static/js/announcements_page.5InQkWqQ.js | 2 + .../js/announcements_page.5InQkWqQ.js.map | 1 + .../js/attributes_helper.service.Ku0p8pb2.js | 3 + .../attributes_helper.service.Ku0p8pb2.js.map | 1 + .../static/js/basic_user_card.DrNxeTyc.js | 2 + .../static/js/basic_user_card.DrNxeTyc.js.map | 1 + .../js/bookmark_folder_edit.BAEqHhNo.js | 2 + .../js/bookmark_folder_edit.BAEqHhNo.js.map | 1 + .../static/js/bookmark_folders.BTJUGZLW.js | 2 + .../js/bookmark_folders.BTJUGZLW.js.map | 1 + priv/static/static/js/chat.DdKwqQWN.js | 2 + priv/static/static/js/chat.DdKwqQWN.js.map | 1 + priv/static/static/js/chat_list.CXgF_qIq.js | 2 + .../static/js/chat_list.CXgF_qIq.js.map | 1 + priv/static/static/js/chat_title.C29jtIsU.js | 2 + .../static/js/chat_title.C29jtIsU.js.map | 1 + priv/static/static/js/checkbox.BfKZDdOO.js | 2 + .../static/static/js/checkbox.BfKZDdOO.js.map | 1 + priv/static/static/js/chunk.aKtaBQYM.js | 1 + .../static/js/confirm_modal.f5dYyJF2.js | 2 + .../static/js/confirm_modal.f5dYyJF2.js.map | 1 + .../static/static/js/dialog_modal.CxsP9W3P.js | 2 + .../static/js/dialog_modal.CxsP9W3P.js.map | 1 + priv/static/static/js/drafts.Ba6rmZP1.js | 3 + priv/static/static/js/drafts.Ba6rmZP1.js.map | 1 + .../static/js/edit_status_form.FufJ0RMz.js | 2 + .../js/edit_status_form.FufJ0RMz.js.map | 1 + .../static/js/edit_status_modal._P4rGdPa.js | 3 + .../js/edit_status_modal._P4rGdPa.js.map | 1 + .../js/emoji-annotations/ar.B8mhZA6U.js | 2 - .../js/emoji-annotations/ar.B8mhZA6U.js.map | 1 - .../js/emoji-annotations/ar.C0WdH5xy.js | 2 + .../js/emoji-annotations/ar.C0WdH5xy.js.map | 1 + .../js/emoji-annotations/ca.BNs7Pavf.js | 2 - .../js/emoji-annotations/ca.BNs7Pavf.js.map | 1 - .../js/emoji-annotations/ca.CNeNqe3P.js | 2 + .../js/emoji-annotations/ca.CNeNqe3P.js.map | 1 + .../js/emoji-annotations/cs.BO8W9-jj.js | 2 + .../js/emoji-annotations/cs.BO8W9-jj.js.map | 1 + .../js/emoji-annotations/cs.CdaRuLZ2.js | 2 - .../js/emoji-annotations/cs.CdaRuLZ2.js.map | 1 - .../js/emoji-annotations/de.D2VnBbFt.js | 2 + .../js/emoji-annotations/de.D2VnBbFt.js.map | 1 + .../js/emoji-annotations/de.i-ePE-E2.js | 2 - .../js/emoji-annotations/de.i-ePE-E2.js.map | 1 - .../js/emoji-annotations/en.D_KNv6Yw.js | 2 + .../js/emoji-annotations/en.D_KNv6Yw.js.map | 1 + .../js/emoji-annotations/en.UxzeFsjk.js | 2 - .../js/emoji-annotations/en.UxzeFsjk.js.map | 1 - .../js/emoji-annotations/es.BgprEtDA.js | 2 - .../js/emoji-annotations/es.BgprEtDA.js.map | 1 - .../js/emoji-annotations/es.C46MzXjW.js | 2 + .../js/emoji-annotations/es.C46MzXjW.js.map | 1 + .../js/emoji-annotations/et.Det91k5K.js | 2 + .../js/emoji-annotations/et.Det91k5K.js.map | 1 + .../js/emoji-annotations/et.RsBIeRrK.js | 2 - .../js/emoji-annotations/et.RsBIeRrK.js.map | 1 - .../js/emoji-annotations/eu.Cvp9Y63c.js | 2 + .../js/emoji-annotations/eu.Cvp9Y63c.js.map | 1 + .../js/emoji-annotations/eu.DflrXXOX.js | 2 - .../js/emoji-annotations/eu.DflrXXOX.js.map | 1 - .../js/emoji-annotations/fi.BQTjfiqL.js | 2 - .../js/emoji-annotations/fi.BQTjfiqL.js.map | 1 - .../js/emoji-annotations/fi.BxxnAYpU.js | 2 + .../js/emoji-annotations/fi.BxxnAYpU.js.map | 1 + .../js/emoji-annotations/fr.6uuBHV2n.js | 2 - .../js/emoji-annotations/fr.6uuBHV2n.js.map | 1 - .../js/emoji-annotations/fr.BS41U5JJ.js | 2 + .../js/emoji-annotations/fr.BS41U5JJ.js.map | 1 + .../js/emoji-annotations/ga.D-L92xRm.js | 2 - .../js/emoji-annotations/ga.D-L92xRm.js.map | 1 - .../js/emoji-annotations/ga.ESuEHZKl.js | 2 + .../js/emoji-annotations/ga.ESuEHZKl.js.map | 1 + .../js/emoji-annotations/he.CfoS-gG4.js | 2 + .../js/emoji-annotations/he.CfoS-gG4.js.map | 1 + .../js/emoji-annotations/he.Ql7ZB6oR.js | 2 - .../js/emoji-annotations/he.Ql7ZB6oR.js.map | 1 - .../js/emoji-annotations/hu.BXFnVUck.js | 2 + .../js/emoji-annotations/hu.BXFnVUck.js.map | 1 + .../js/emoji-annotations/hu.CfFMlYJN.js | 2 - .../js/emoji-annotations/hu.CfFMlYJN.js.map | 1 - .../js/emoji-annotations/it.CjUsZ0f4.js | 2 - .../js/emoji-annotations/it.CjUsZ0f4.js.map | 1 - .../js/emoji-annotations/it.V9d10lLp.js | 2 + .../js/emoji-annotations/it.V9d10lLp.js.map | 1 + .../js/emoji-annotations/ja.Cp7hRpKj.js | 2 - .../js/emoji-annotations/ja.Cp7hRpKj.js.map | 1 - .../js/emoji-annotations/ja.Dt-m5rD-.js | 2 + .../js/emoji-annotations/ja.Dt-m5rD-.js.map | 1 + .../js/emoji-annotations/ko.DGjjBGsA.js | 2 - .../js/emoji-annotations/ko.DGjjBGsA.js.map | 1 - .../js/emoji-annotations/ko.qCDFL19U.js | 2 + .../js/emoji-annotations/ko.qCDFL19U.js.map | 1 + .../js/emoji-annotations/nl.B2IenlFE.js | 2 + .../js/emoji-annotations/nl.B2IenlFE.js.map | 1 + .../js/emoji-annotations/nl.BCmP3XAV.js | 2 - .../js/emoji-annotations/nl.BCmP3XAV.js.map | 1 - .../js/emoji-annotations/pl.C1i62U33.js | 2 - .../js/emoji-annotations/pl.C1i62U33.js.map | 1 - .../js/emoji-annotations/pl.DxxjBECj.js | 2 + .../js/emoji-annotations/pl.DxxjBECj.js.map | 1 + .../js/emoji-annotations/pt.BmciJSYs.js | 2 + .../js/emoji-annotations/pt.BmciJSYs.js.map | 1 + .../js/emoji-annotations/pt.qcD2jbqr.js | 2 - .../js/emoji-annotations/pt.qcD2jbqr.js.map | 1 - .../js/emoji-annotations/ro.Q0r94aGX.js | 2 - .../js/emoji-annotations/ro.Q0r94aGX.js.map | 1 - .../js/emoji-annotations/ro.yzctWu4x.js | 2 + .../js/emoji-annotations/ro.yzctWu4x.js.map | 1 + .../js/emoji-annotations/ru.BLE9DyAq.js | 2 + .../js/emoji-annotations/ru.BLE9DyAq.js.map | 1 + .../js/emoji-annotations/ru.BsjC3M1d.js | 2 - .../js/emoji-annotations/ru.BsjC3M1d.js.map | 1 - .../js/emoji-annotations/sk.BrZ6jXEe.js | 2 - .../js/emoji-annotations/sk.BrZ6jXEe.js.map | 1 - .../js/emoji-annotations/sk.Dy63vbs6.js | 2 + .../js/emoji-annotations/sk.Dy63vbs6.js.map | 1 + .../js/emoji-annotations/te.3rQpXN4b.js | 2 - .../js/emoji-annotations/te.3rQpXN4b.js.map | 1 - .../js/emoji-annotations/te.BE2qiFNg.js | 2 + .../js/emoji-annotations/te.BE2qiFNg.js.map | 1 + .../js/emoji-annotations/uk.C7RXZT0o.js | 2 - .../js/emoji-annotations/uk.C7RXZT0o.js.map | 1 - .../js/emoji-annotations/uk.DYbkLgKB.js | 2 + .../js/emoji-annotations/uk.DYbkLgKB.js.map | 1 + .../js/emoji-annotations/zh.By94GFYU.js | 2 + .../js/emoji-annotations/zh.By94GFYU.js.map | 1 + .../js/emoji-annotations/zh.DXJ_1nIQ.js | 2 - .../js/emoji-annotations/zh.DXJ_1nIQ.js.map | 1 - .../js/emoji-annotations/zh_Hant.D6xeB4Z8.js | 2 + .../emoji-annotations/zh_Hant.D6xeB4Z8.js.map | 1 + .../js/emoji-annotations/zh_Hant.ccXyIXaI.js | 2 - .../emoji-annotations/zh_Hant.ccXyIXaI.js.map | 1 - priv/static/static/js/emoji.C5uX9ZLF.js | 2 + priv/static/static/js/emoji.C5uX9ZLF.js.map | 1 + priv/static/static/js/emoji.CF2IDMqM.js | 2 - priv/static/static/js/emoji.CF2IDMqM.js.map | 1 - priv/static/static/js/emoji.CKB_ooIp.js | 4 + priv/static/static/js/emoji.CKB_ooIp.js.map | 1 + .../static/static/js/emoji_picker.Ccx2u99H.js | 3 + .../static/js/emoji_picker.Ccx2u99H.js.map | 1 + priv/static/static/js/error_modal.BRfbOgFr.js | 2 + .../static/js/error_modal.BRfbOgFr.js.map | 1 + .../static/js/export_import.DxS26Hs4.js | 2 + .../static/js/export_import.DxS26Hs4.js.map | 1 + priv/static/static/js/flash.euQ77IAL.js | 2 + priv/static/static/js/flash.euQ77IAL.js.map | 1 + priv/static/static/js/folder_card.DRaMmZN4.js | 2 + .../static/js/folder_card.DRaMmZN4.js.map | 1 + priv/static/static/js/follow_card.C3eWENM0.js | 3 + .../static/js/follow_card.C3eWENM0.js.map | 1 + .../static/js/follow_requests.BTnExoOV.js | 3 + .../static/js/follow_requests.BTnExoOV.js.map | 1 + .../js/free-regular-svg-icons.BGRGA4e_.js | 2 + .../js/free-regular-svg-icons.BGRGA4e_.js.map | 1 + .../js/free-solid-svg-icons.vn_kySUQ.js | 558 ++++++++++++++ .../js/free-solid-svg-icons.vn_kySUQ.js.map | 1 + priv/static/static/js/helpers.DO6M90bq.js | 2 + priv/static/static/js/helpers.DO6M90bq.js.map | 1 + priv/static/static/js/i18n/ar.Cr-3kZBh.js | 3 - priv/static/static/js/i18n/ar.Cr-3kZBh.js.map | 1 - priv/static/static/js/i18n/ar.PF4_zGG8.js | 3 + priv/static/static/js/i18n/ar.PF4_zGG8.js.map | 1 + priv/static/static/js/i18n/ca.Bv4d2Muy.js.map | 1 - .../i18n/{ca.Bv4d2Muy.js => ca.DN5rtM_y.js} | 6 +- priv/static/static/js/i18n/ca.DN5rtM_y.js.map | 1 + priv/static/static/js/i18n/cs.Ch8ryXjb.js | 5 + priv/static/static/js/i18n/cs.Ch8ryXjb.js.map | 1 + priv/static/static/js/i18n/cs.EyNXpVXW.js | 5 - priv/static/static/js/i18n/cs.EyNXpVXW.js.map | 1 - priv/static/static/js/i18n/de.CvXWIhiF.js.map | 1 - .../i18n/{de.CvXWIhiF.js => de.sg00fJvg.js} | 8 +- priv/static/static/js/i18n/de.sg00fJvg.js.map | 1 + priv/static/static/js/i18n/eo.C9h09SOv.js | 5 - priv/static/static/js/i18n/eo.C9h09SOv.js.map | 1 - priv/static/static/js/i18n/eo.siiZsXXQ.js | 5 + priv/static/static/js/i18n/eo.siiZsXXQ.js.map | 1 + priv/static/static/js/i18n/es.BFMlvXsf.js.map | 1 - .../i18n/{es.BFMlvXsf.js => es.BfXuHyuJ.js} | 8 +- priv/static/static/js/i18n/es.BfXuHyuJ.js.map | 1 + priv/static/static/js/i18n/et.BrU7KoHa.js | 2 + priv/static/static/js/i18n/et.BrU7KoHa.js.map | 1 + priv/static/static/js/i18n/et.fhuMKZXQ.js | 2 - priv/static/static/js/i18n/et.fhuMKZXQ.js.map | 1 - priv/static/static/js/i18n/eu.C1UxLEb6.js.map | 1 - .../i18n/{eu.C1UxLEb6.js => eu.CcYtkR8l.js} | 8 +- priv/static/static/js/i18n/eu.CcYtkR8l.js.map | 1 + priv/static/static/js/i18n/fa.BxVyTR8q.js | 2 - priv/static/static/js/i18n/fa.BxVyTR8q.js.map | 1 - priv/static/static/js/i18n/fa.Ct0q0avS.js | 2 + priv/static/static/js/i18n/fa.Ct0q0avS.js.map | 1 + priv/static/static/js/i18n/fi.Brm9lhAA.js.map | 1 - .../i18n/{fi.Brm9lhAA.js => fi.PwO5kZWe.js} | 6 +- priv/static/static/js/i18n/fi.PwO5kZWe.js.map | 1 + priv/static/static/js/i18n/fr.C55JqQ_m.js | 5 + priv/static/static/js/i18n/fr.C55JqQ_m.js.map | 1 + priv/static/static/js/i18n/fr.DzBG0KvL.js | 5 - priv/static/static/js/i18n/fr.DzBG0KvL.js.map | 1 - priv/static/static/js/i18n/ga.C1ShdXcl.js | 2 + priv/static/static/js/i18n/ga.C1ShdXcl.js.map | 1 + priv/static/static/js/i18n/ga.CPJnGxWv.js | 2 - priv/static/static/js/i18n/ga.CPJnGxWv.js.map | 1 - priv/static/static/js/i18n/he.BllNuLoC.js | 4 - priv/static/static/js/i18n/he.BllNuLoC.js.map | 1 - priv/static/static/js/i18n/he.Bs9RetYt.js | 4 + priv/static/static/js/i18n/he.Bs9RetYt.js.map | 1 + priv/static/static/js/i18n/hu.CljyAvQa.js | 2 - priv/static/static/js/i18n/hu.CljyAvQa.js.map | 1 - priv/static/static/js/i18n/hu.UfPuJQT0.js | 2 + priv/static/static/js/i18n/hu.UfPuJQT0.js.map | 1 + priv/static/static/js/i18n/id.CLCh_oXi.js | 5 + priv/static/static/js/i18n/id.CLCh_oXi.js.map | 1 + priv/static/static/js/i18n/id.DLjgbCu5.js | 5 - priv/static/static/js/i18n/id.DLjgbCu5.js.map | 1 - priv/static/static/js/i18n/it.DfxTv4P6.js | 5 + priv/static/static/js/i18n/it.DfxTv4P6.js.map | 1 + priv/static/static/js/i18n/it.ERg696CD.js | 5 - priv/static/static/js/i18n/it.ERg696CD.js.map | 1 - .../static/js/i18n/ja_easy.PsomvmuF.js.map | 1 - ...a_easy.PsomvmuF.js => ja_easy.r4RI35B2.js} | 8 +- .../static/js/i18n/ja_easy.r4RI35B2.js.map | 1 + .../static/js/i18n/ja_pedantic.Cy1uUP6w.js | 5 + .../js/i18n/ja_pedantic.Cy1uUP6w.js.map | 1 + .../static/js/i18n/ja_pedantic.VWLd9bHt.js | 5 - .../js/i18n/ja_pedantic.VWLd9bHt.js.map | 1 - .../i18n/{ko.b1sv0xRp.js => ko.BB_3jPc2.js} | 8 +- priv/static/static/js/i18n/ko.BB_3jPc2.js.map | 1 + priv/static/static/js/i18n/ko.b1sv0xRp.js.map | 1 - .../static/js/i18n/nan-TW.Ci98X1S7.js.map | 1 - ...{nan-TW.Ci98X1S7.js => nan-TW.DniT5teG.js} | 8 +- .../static/js/i18n/nan-TW.DniT5teG.js.map | 1 + .../i18n/{nb.DCCmz1gh.js => nb.CSJp4rKA.js} | 6 +- priv/static/static/js/i18n/nb.CSJp4rKA.js.map | 1 + priv/static/static/js/i18n/nb.DCCmz1gh.js.map | 1 - priv/static/static/js/i18n/nl.D6wYIlyz.js.map | 1 - .../i18n/{nl.D6wYIlyz.js => nl.DA-1A4I-.js} | 6 +- priv/static/static/js/i18n/nl.DA-1A4I-.js.map | 1 + priv/static/static/js/i18n/oc.C4DhJxhs.js.map | 1 - .../i18n/{oc.C4DhJxhs.js => oc.CXSjczlM.js} | 6 +- priv/static/static/js/i18n/oc.CXSjczlM.js.map | 1 + priv/static/static/js/i18n/pdc.DOwCrGXH.js | 2 + .../static/static/js/i18n/pdc.DOwCrGXH.js.map | 1 + priv/static/static/js/i18n/pdc.FmFgRqLi.js | 2 - .../static/static/js/i18n/pdc.FmFgRqLi.js.map | 1 - priv/static/static/js/i18n/pl.CPLK-gPs.js.map | 1 - .../i18n/{pl.CPLK-gPs.js => pl.Cmnd-Fcg.js} | 8 +- priv/static/static/js/i18n/pl.Cmnd-Fcg.js.map | 1 + .../i18n/{pt.BzQabObA.js => pt.B0CD21EJ.js} | 8 +- priv/static/static/js/i18n/pt.B0CD21EJ.js.map | 1 + priv/static/static/js/i18n/pt.BzQabObA.js.map | 1 - priv/static/static/js/i18n/ro.BA9koejw.js | 2 - priv/static/static/js/i18n/ro.BA9koejw.js.map | 1 - priv/static/static/js/i18n/ro.BDDUzm6K.js | 2 + priv/static/static/js/i18n/ro.BDDUzm6K.js.map | 1 + priv/static/static/js/i18n/ru.CYi-PMDA.js.map | 1 - .../i18n/{ru.CYi-PMDA.js => ru.CZj8h1eY.js} | 8 +- priv/static/static/js/i18n/ru.CZj8h1eY.js.map | 1 + priv/static/static/js/i18n/sk.BHTmHTrO.js | 5 + priv/static/static/js/i18n/sk.BHTmHTrO.js.map | 1 + priv/static/static/js/i18n/sk.fGdLyyK_.js | 5 - priv/static/static/js/i18n/sk.fGdLyyK_.js.map | 1 - priv/static/static/js/i18n/te.CikZ4jPW.js | 4 - priv/static/static/js/i18n/te.CikZ4jPW.js.map | 1 - priv/static/static/js/i18n/te.DCldbst0.js | 4 + priv/static/static/js/i18n/te.DCldbst0.js.map | 1 + priv/static/static/js/i18n/uk.A8RhHFvE.js | 4 + priv/static/static/js/i18n/uk.A8RhHFvE.js.map | 1 + priv/static/static/js/i18n/uk.CkmtgYNq.js | 4 - priv/static/static/js/i18n/uk.CkmtgYNq.js.map | 1 - priv/static/static/js/i18n/vi.BsGK9mjD.js.map | 1 - .../i18n/{vi.BsGK9mjD.js => vi.QgsLBEhg.js} | 8 +- priv/static/static/js/i18n/vi.QgsLBEhg.js.map | 1 + priv/static/static/js/i18n/zh.BZkJuR5j.js.map | 1 - .../i18n/{zh.BZkJuR5j.js => zh.Dbd2AYo_.js} | 8 +- priv/static/static/js/i18n/zh.Dbd2AYo_.js.map | 1 + .../static/static/js/i18n/zh_Hant.BFQs14ie.js | 5 + .../static/js/i18n/zh_Hant.BFQs14ie.js.map | 1 + .../static/static/js/i18n/zh_Hant.dSMADUQ9.js | 5 - .../static/js/i18n/zh_Hant.dSMADUQ9.js.map | 1 - .../static/js/image_cropper.CSuez09N.js | 2 + .../static/js/image_cropper.CSuez09N.js.map | 1 + .../js/instance_capabilities.B6a7_omF.js | 2 + .../js/instance_capabilities.B6a7_omF.js.map | 1 + .../static/static/js/interactions.Lac6uCf8.js | 2 + .../static/js/interactions.Lac6uCf8.js.map | 1 + .../interface_language_switcher.cmRBgy4K.js | 2 + ...nterface_language_switcher.cmRBgy4K.js.map | 1 + priv/static/static/js/languages.q6q0orhR.js | 2 + .../static/js/languages.q6q0orhR.js.map | 1 + priv/static/static/js/list.sBmZfIXM.js | 2 + priv/static/static/js/list.sBmZfIXM.js.map | 1 + priv/static/static/js/lists.BBz5d_Ug.js | 2 + priv/static/static/js/lists.BBz5d_Ug.js.map | 1 + priv/static/static/js/lists_edit.BFmgx9sZ.js | 2 + .../static/js/lists_edit.BFmgx9sZ.js.map | 1 + .../static/js/lists_timeline.D0jKFKd9.js | 2 + .../static/js/lists_timeline.D0jKFKd9.js.map | 1 + .../static/js/locale.service.ol8gNUGd.js | 5 + .../static/js/locale.service.ol8gNUGd.js.map | 1 + priv/static/static/js/lodash.daI9UflD.js | 21 + priv/static/static/js/lodash.daI9UflD.js.map | 1 + priv/static/static/js/main.CPF6dYCY.js | 15 + priv/static/static/js/main.CPF6dYCY.js.map | 1 + priv/static/static/js/main.qxCxIqDs.js | 696 ------------------ priv/static/static/js/main.qxCxIqDs.js.map | 1 - priv/static/static/js/media_modal.Bn3GxKv7.js | 3 + .../static/js/media_modal.Bn3GxKv7.js.map | 1 + .../static/static/js/media_viewer.B2Dx6m_D.js | 2 + .../static/js/media_viewer.B2Dx6m_D.js.map | 1 + priv/static/static/js/modal.BLgTTi-o.js | 2 + priv/static/static/js/modal.BLgTTi-o.js.map | 1 + .../static/js/moderation_tools.C6XQ3bpM.js | 2 + .../js/moderation_tools.C6XQ3bpM.js.map | 1 + .../static/static/js/mute_confirm.DfJoacGH.js | 3 + .../static/js/mute_confirm.DfJoacGH.js.map | 1 + .../static/js/notifications.WzZPeGzx.js | 3 + .../static/js/notifications.WzZPeGzx.js.map | 1 + priv/static/static/js/oauth.C4JLKGAu.js | 29 + priv/static/static/js/oauth.C4JLKGAu.js.map | 1 + .../static/js/oauth_callback.B2cngiTu.js | 2 + .../static/js/oauth_callback.B2cngiTu.js.map | 1 + .../static/js/panel_loading.0JmxtEZa.js | 2 + .../static/js/panel_loading.0JmxtEZa.js.map | 1 + .../static/js/password_reset.HOi7zEwD.js | 2 + .../static/js/password_reset.HOi7zEwD.js.map | 1 + priv/static/static/js/pinch_zoom.Djntym73.js | 2 + .../static/js/pinch_zoom.Djntym73.js.map | 1 + priv/static/static/js/poll_form.D-st5E13.js | 2 + .../static/js/poll_form.D-st5E13.js.map | 1 + priv/static/static/js/popover.94nM-RDc.js | 2 + priv/static/static/js/popover.94nM-RDc.js.map | 1 + priv/static/static/js/post_status.BF7jtp5r.js | 2 + .../static/js/post_status.BF7jtp5r.js.map | 1 + .../static/js/post_status_modal.Cjqh58_u.js | 2 + .../js/post_status_modal.Cjqh58_u.js.map | 1 + .../static/js/preload-helper.zJ_50EbN.js | 1 + .../static/js/progress_button.B2rAp_zx.js | 2 + .../static/js/progress_button.B2rAp_zx.js.map | 1 + .../static/js/promise_interval.BgbE32Aa.js | 2 + .../js/promise_interval.BgbE32Aa.js.map | 1 + priv/static/static/js/quote.D9tjkbDR.js | 2 + priv/static/static/js/quote.D9tjkbDR.js.map | 1 + priv/static/static/js/quote_form.DhluIGdz.js | 2 + .../static/js/quote_form.DhluIGdz.js.map | 1 + .../static/static/js/registration.C-t5q0zl.js | 3 + .../static/js/registration.C-t5q0zl.js.map | 1 + .../static/js/remote_follow.DpK1hmxn.js | 2 + .../static/js/remote_follow.DpK1hmxn.js.map | 1 + .../js/runtime-dom.esm-bundler.DzcUlaDE.js | 2 + .../runtime-dom.esm-bundler.DzcUlaDE.js.map | 1 + priv/static/static/js/search.CeRVMFFx.js | 2 + priv/static/static/js/search.CeRVMFFx.js.map | 1 + priv/static/static/js/select.BU31Em0g.js | 2 + priv/static/static/js/select.BU31Em0g.js.map | 1 + .../static/js/settings_modal.B74APXc5.js | 3 - .../static/js/settings_modal.B74APXc5.js.map | 1 - .../static/js/settings_modal.BDTk2X9h.js | 3 + .../static/js/settings_modal.BDTk2X9h.js.map | 1 + .../settings_modal_admin_content.CBR-3uLX.js | 3 + ...ttings_modal_admin_content.CBR-3uLX.js.map | 1 + .../settings_modal_admin_content.DWMrR-Cb.js | 2 - ...ttings_modal_admin_content.DWMrR-Cb.js.map | 1 - .../settings_modal_user_content.C6A-ej51.js | 37 + ...ettings_modal_user_content.C6A-ej51.js.map | 1 + .../settings_modal_user_content.D-dohGjw.js | 41 -- ...ettings_modal_user_content.D-dohGjw.js.map | 1 - priv/static/static/js/shout_panel.D2n32sK6.js | 2 + .../static/js/shout_panel.D2n32sK6.js.map | 1 + priv/static/static/js/side_drawer.Cq9nZmqN.js | 2 + .../static/js/side_drawer.Cq9nZmqN.js.map | 1 + .../js/status_history_modal.LzEdYEdX.js | 2 + .../js/status_history_modal.LzEdYEdX.js.map | 1 + .../static/js/sticker_picker.B0j2YsOB.js | 2 - .../static/js/sticker_picker.B0j2YsOB.js.map | 1 - .../static/js/sticker_picker.aQ7A7kiL.js | 2 + .../static/js/sticker_picker.aQ7A7kiL.js.map | 1 + .../static/js/string_setting.QmUmmIFY.js | 2 - .../static/js/string_setting.QmUmmIFY.js.map | 1 - priv/static/static/js/swipe_click.DFwOeCbK.js | 2 + .../static/js/swipe_click.DFwOeCbK.js.map | 1 + .../static/static/js/tab_switcher.BMrlDICt.js | 2 + .../static/js/tab_switcher.BMrlDICt.js.map | 1 + .../js/terms_of_service_panel.rRFEmiHL.js | 2 + .../js/terms_of_service_panel.rRFEmiHL.js.map | 1 + .../js/unicode_domain_indicator.BnLqD48H.js | 2 + .../unicode_domain_indicator.BnLqD48H.js.map | 1 + .../static/js/update_notification.D5whTpWL.js | 2 + .../js/update_notification.D5whTpWL.js.map | 1 + .../static/js/update_notification.DdDhzBg0.js | 2 - .../js/update_notification.DdDhzBg0.js.map | 1 - priv/static/static/js/user_card.DVbLIASH.js | 3 + .../static/js/user_card.DVbLIASH.js.map | 1 + priv/static/static/js/user_link.CzPI9p_B.js | 2 + .../static/js/user_link.CzPI9p_B.js.map | 1 + priv/static/static/js/user_note.CL52aTOE.js | 2 + .../static/js/user_note.CL52aTOE.js.map | 1 + .../static/static/js/user_profile.CZtkyu5S.js | 2 + .../static/js/user_profile.CZtkyu5S.js.map | 1 + .../js/user_profile_admin_view.BVpUbjXr.js | 2 + .../user_profile_admin_view.BVpUbjXr.js.map | 1 + .../user_profile_link_generator.WrdnW5sg.js | 2 + ...ser_profile_link_generator.WrdnW5sg.js.map | 1 + .../js/user_reporting_modal.Drcvbv7N.js | 2 + .../js/user_reporting_modal.Drcvbv7N.js.map | 1 + .../js/user_timed_filter_modal.Njj7Gcae.js | 3 + .../user_timed_filter_modal.Njj7Gcae.js.map | 1 + .../js/vertical_tab_switcher.CgkOofcV.js | 2 + .../js/vertical_tab_switcher.CgkOofcV.js.map | 1 + .../static/js/video_attachment.DeOyHdhJ.js | 2 + .../js/video_attachment.DeOyHdhJ.js.map | 1 + .../static/js/vuex.esm-bundler.CWMfctgB.js | 2 + .../js/vuex.esm-bundler.CWMfctgB.js.map | 1 + .../static/js/who_to_follow.I-3jjy5y.js | 2 + .../static/js/who_to_follow.I-3jjy5y.js.map | 1 + .../static/js/who_to_follow_panel.CeJ6wsZ3.js | 2 + .../js/who_to_follow_panel.CeJ6wsZ3.js.map | 1 + priv/static/static/palettes/index.json | 72 +- priv/static/static/splash.css | 21 +- priv/static/sw-pleroma.js | 46 +- priv/static/sw-pleroma.js.map | 2 +- 496 files changed, 1382 insertions(+), 1046 deletions(-) create mode 100644 priv/static/static/css/about.BPcl3NxB.css create mode 100644 priv/static/static/css/account_actions.DjGDDSEq.css create mode 100644 priv/static/static/css/announcements_page.DvITSMCQ.css create mode 100644 priv/static/static/css/attributes_helper.DsgIU8rn.css create mode 100644 priv/static/static/css/basic_user_card.CXE-NVII.css create mode 100644 priv/static/static/css/bookmark_folder_edit.BYhGGKc2.css create mode 100644 priv/static/static/css/bookmark_folders.CitandGk.css create mode 100644 priv/static/static/css/chat.6N-tF5xA.css create mode 100644 priv/static/static/css/chat_list.WHtUK6wd.css create mode 100644 priv/static/static/css/chat_title.B1JBlEsZ.css create mode 100644 priv/static/static/css/checkbox.DY0WQ--N.css create mode 100644 priv/static/static/css/confirm_modal.CxXYsWAn.css create mode 100644 priv/static/static/css/dialog_modal.DC5hTqSH.css create mode 100644 priv/static/static/css/drafts.CvPb_YQj.css create mode 100644 priv/static/static/css/edit_status_modal.egG_jIir.css create mode 100644 priv/static/static/css/emoji_picker.CJFAWWOw.css create mode 100644 priv/static/static/css/error_modal.D1oWQ5nz.css create mode 100644 priv/static/static/css/flash.BYNCHsIH.css create mode 100644 priv/static/static/css/folder_card.CTk_063i.css create mode 100644 priv/static/static/css/follow_card.jWN765qC.css create mode 100644 priv/static/static/css/follow_requests.5W1jgYR0.css create mode 100644 priv/static/static/css/image_cropper.iV4LurEO.css create mode 100644 priv/static/static/css/interface_language_switcher.DYf3Vv_u.css create mode 100644 priv/static/static/css/list.Cau2FUIa.css create mode 100644 priv/static/static/css/lists.R2bOz6oY.css create mode 100644 priv/static/static/css/lists_edit.D3Eu2cDo.css create mode 100644 priv/static/static/css/main.0Iw0byjp.css delete mode 100644 priv/static/static/css/main.CF7uYwoC.css create mode 100644 priv/static/static/css/media_modal.Bc6XAjzi.css create mode 100644 priv/static/static/css/modal.Dv3V-p-W.css create mode 100644 priv/static/static/css/moderation_tools.DoZVYsfl.css create mode 100644 priv/static/static/css/mute_confirm.p-qSlYOr.css create mode 100644 priv/static/static/css/notifications.CW0-QQtF.css create mode 100644 priv/static/static/css/panel_loading.DQYYmPMZ.css create mode 100644 priv/static/static/css/password_reset.BLv39QBH.css create mode 100644 priv/static/static/css/poll_form.s0YHqxUy.css create mode 100644 priv/static/static/css/popover.BlbX_Uwp.css create mode 100644 priv/static/static/css/post_status_modal.C4svyRW2.css create mode 100644 priv/static/static/css/quote.DAX2X1-h.css create mode 100644 priv/static/static/css/quote_form.ix5T3mDK.css create mode 100644 priv/static/static/css/registration.BpinXGMn.css create mode 100644 priv/static/static/css/remote_follow.BrTyl75C.css create mode 100644 priv/static/static/css/search.BMRqSZdz.css create mode 100644 priv/static/static/css/select.BF2ygbkJ.css create mode 100644 priv/static/static/css/settings_modal._8lsImIr.css delete mode 100644 priv/static/static/css/settings_modal.dQ5YFZGx.css create mode 100644 priv/static/static/css/settings_modal_admin_content.CKTEQiM8.css delete mode 100644 priv/static/static/css/settings_modal_admin_content.d1pRGdhX.css delete mode 100644 priv/static/static/css/settings_modal_user_content.7KpKOwxU.css create mode 100644 priv/static/static/css/settings_modal_user_content.DUcdr5Gz.css create mode 100644 priv/static/static/css/shout_panel.BHBsqket.css create mode 100644 priv/static/static/css/side_drawer.2CnDtAJj.css create mode 100644 priv/static/static/css/status_history_modal.CZzNzXP5.css delete mode 100644 priv/static/static/css/sticker_picker.BzJVB0Ta.css create mode 100644 priv/static/static/css/sticker_picker.C2lbaygI.css delete mode 100644 priv/static/static/css/string_setting.CtukauKH.css create mode 100644 priv/static/static/css/tab_switcher.CEadvXLu.css create mode 100644 priv/static/static/css/terms_of_service_panel.ClR_b2MD.css delete mode 100644 priv/static/static/css/update_notification.BJ1CAGCw.css create mode 100644 priv/static/static/css/update_notification.Co8luhDO.css create mode 100644 priv/static/static/css/user_card.C2QiPTUj.css create mode 100644 priv/static/static/css/user_note.A3UYQeEG.css create mode 100644 priv/static/static/css/user_profile.CC-U32e-.css create mode 100644 priv/static/static/css/user_reporting_modal.CVdwO3Sc.css create mode 100644 priv/static/static/css/user_timed_filter_modal.ClNyvF9i.css create mode 100644 priv/static/static/css/vertical_tab_switcher.DU3QPNwr.css create mode 100644 priv/static/static/css/who_to_follow_panel.BP23-WSI.css create mode 100644 priv/static/static/js/_plugin-vue_export-helper.By3-ojQa.js create mode 100644 priv/static/static/js/_plugin-vue_export-helper.By3-ojQa.js.map create mode 100644 priv/static/static/js/about.Origtmpb.js create mode 100644 priv/static/static/js/about.Origtmpb.js.map create mode 100644 priv/static/static/js/account_actions.iKah8h7y.js create mode 100644 priv/static/static/js/account_actions.iKah8h7y.js.map create mode 100644 priv/static/static/js/admin.Dy10Wzep.js create mode 100644 priv/static/static/js/admin.Dy10Wzep.js.map create mode 100644 priv/static/static/js/announcements_page.5InQkWqQ.js create mode 100644 priv/static/static/js/announcements_page.5InQkWqQ.js.map create mode 100644 priv/static/static/js/attributes_helper.service.Ku0p8pb2.js create mode 100644 priv/static/static/js/attributes_helper.service.Ku0p8pb2.js.map create mode 100644 priv/static/static/js/basic_user_card.DrNxeTyc.js create mode 100644 priv/static/static/js/basic_user_card.DrNxeTyc.js.map create mode 100644 priv/static/static/js/bookmark_folder_edit.BAEqHhNo.js create mode 100644 priv/static/static/js/bookmark_folder_edit.BAEqHhNo.js.map create mode 100644 priv/static/static/js/bookmark_folders.BTJUGZLW.js create mode 100644 priv/static/static/js/bookmark_folders.BTJUGZLW.js.map create mode 100644 priv/static/static/js/chat.DdKwqQWN.js create mode 100644 priv/static/static/js/chat.DdKwqQWN.js.map create mode 100644 priv/static/static/js/chat_list.CXgF_qIq.js create mode 100644 priv/static/static/js/chat_list.CXgF_qIq.js.map create mode 100644 priv/static/static/js/chat_title.C29jtIsU.js create mode 100644 priv/static/static/js/chat_title.C29jtIsU.js.map create mode 100644 priv/static/static/js/checkbox.BfKZDdOO.js create mode 100644 priv/static/static/js/checkbox.BfKZDdOO.js.map create mode 100644 priv/static/static/js/chunk.aKtaBQYM.js create mode 100644 priv/static/static/js/confirm_modal.f5dYyJF2.js create mode 100644 priv/static/static/js/confirm_modal.f5dYyJF2.js.map create mode 100644 priv/static/static/js/dialog_modal.CxsP9W3P.js create mode 100644 priv/static/static/js/dialog_modal.CxsP9W3P.js.map create mode 100644 priv/static/static/js/drafts.Ba6rmZP1.js create mode 100644 priv/static/static/js/drafts.Ba6rmZP1.js.map create mode 100644 priv/static/static/js/edit_status_form.FufJ0RMz.js create mode 100644 priv/static/static/js/edit_status_form.FufJ0RMz.js.map create mode 100644 priv/static/static/js/edit_status_modal._P4rGdPa.js create mode 100644 priv/static/static/js/edit_status_modal._P4rGdPa.js.map delete mode 100644 priv/static/static/js/emoji-annotations/ar.B8mhZA6U.js delete mode 100644 priv/static/static/js/emoji-annotations/ar.B8mhZA6U.js.map create mode 100644 priv/static/static/js/emoji-annotations/ar.C0WdH5xy.js create mode 100644 priv/static/static/js/emoji-annotations/ar.C0WdH5xy.js.map delete mode 100644 priv/static/static/js/emoji-annotations/ca.BNs7Pavf.js delete mode 100644 priv/static/static/js/emoji-annotations/ca.BNs7Pavf.js.map create mode 100644 priv/static/static/js/emoji-annotations/ca.CNeNqe3P.js create mode 100644 priv/static/static/js/emoji-annotations/ca.CNeNqe3P.js.map create mode 100644 priv/static/static/js/emoji-annotations/cs.BO8W9-jj.js create mode 100644 priv/static/static/js/emoji-annotations/cs.BO8W9-jj.js.map delete mode 100644 priv/static/static/js/emoji-annotations/cs.CdaRuLZ2.js delete mode 100644 priv/static/static/js/emoji-annotations/cs.CdaRuLZ2.js.map create mode 100644 priv/static/static/js/emoji-annotations/de.D2VnBbFt.js create mode 100644 priv/static/static/js/emoji-annotations/de.D2VnBbFt.js.map delete mode 100644 priv/static/static/js/emoji-annotations/de.i-ePE-E2.js delete mode 100644 priv/static/static/js/emoji-annotations/de.i-ePE-E2.js.map create mode 100644 priv/static/static/js/emoji-annotations/en.D_KNv6Yw.js create mode 100644 priv/static/static/js/emoji-annotations/en.D_KNv6Yw.js.map delete mode 100644 priv/static/static/js/emoji-annotations/en.UxzeFsjk.js delete mode 100644 priv/static/static/js/emoji-annotations/en.UxzeFsjk.js.map delete mode 100644 priv/static/static/js/emoji-annotations/es.BgprEtDA.js delete mode 100644 priv/static/static/js/emoji-annotations/es.BgprEtDA.js.map create mode 100644 priv/static/static/js/emoji-annotations/es.C46MzXjW.js create mode 100644 priv/static/static/js/emoji-annotations/es.C46MzXjW.js.map create mode 100644 priv/static/static/js/emoji-annotations/et.Det91k5K.js create mode 100644 priv/static/static/js/emoji-annotations/et.Det91k5K.js.map delete mode 100644 priv/static/static/js/emoji-annotations/et.RsBIeRrK.js delete mode 100644 priv/static/static/js/emoji-annotations/et.RsBIeRrK.js.map create mode 100644 priv/static/static/js/emoji-annotations/eu.Cvp9Y63c.js create mode 100644 priv/static/static/js/emoji-annotations/eu.Cvp9Y63c.js.map delete mode 100644 priv/static/static/js/emoji-annotations/eu.DflrXXOX.js delete mode 100644 priv/static/static/js/emoji-annotations/eu.DflrXXOX.js.map delete mode 100644 priv/static/static/js/emoji-annotations/fi.BQTjfiqL.js delete mode 100644 priv/static/static/js/emoji-annotations/fi.BQTjfiqL.js.map create mode 100644 priv/static/static/js/emoji-annotations/fi.BxxnAYpU.js create mode 100644 priv/static/static/js/emoji-annotations/fi.BxxnAYpU.js.map delete mode 100644 priv/static/static/js/emoji-annotations/fr.6uuBHV2n.js delete mode 100644 priv/static/static/js/emoji-annotations/fr.6uuBHV2n.js.map create mode 100644 priv/static/static/js/emoji-annotations/fr.BS41U5JJ.js create mode 100644 priv/static/static/js/emoji-annotations/fr.BS41U5JJ.js.map delete mode 100644 priv/static/static/js/emoji-annotations/ga.D-L92xRm.js delete mode 100644 priv/static/static/js/emoji-annotations/ga.D-L92xRm.js.map create mode 100644 priv/static/static/js/emoji-annotations/ga.ESuEHZKl.js create mode 100644 priv/static/static/js/emoji-annotations/ga.ESuEHZKl.js.map create mode 100644 priv/static/static/js/emoji-annotations/he.CfoS-gG4.js create mode 100644 priv/static/static/js/emoji-annotations/he.CfoS-gG4.js.map delete mode 100644 priv/static/static/js/emoji-annotations/he.Ql7ZB6oR.js delete mode 100644 priv/static/static/js/emoji-annotations/he.Ql7ZB6oR.js.map create mode 100644 priv/static/static/js/emoji-annotations/hu.BXFnVUck.js create mode 100644 priv/static/static/js/emoji-annotations/hu.BXFnVUck.js.map delete mode 100644 priv/static/static/js/emoji-annotations/hu.CfFMlYJN.js delete mode 100644 priv/static/static/js/emoji-annotations/hu.CfFMlYJN.js.map delete mode 100644 priv/static/static/js/emoji-annotations/it.CjUsZ0f4.js delete mode 100644 priv/static/static/js/emoji-annotations/it.CjUsZ0f4.js.map create mode 100644 priv/static/static/js/emoji-annotations/it.V9d10lLp.js create mode 100644 priv/static/static/js/emoji-annotations/it.V9d10lLp.js.map delete mode 100644 priv/static/static/js/emoji-annotations/ja.Cp7hRpKj.js delete mode 100644 priv/static/static/js/emoji-annotations/ja.Cp7hRpKj.js.map create mode 100644 priv/static/static/js/emoji-annotations/ja.Dt-m5rD-.js create mode 100644 priv/static/static/js/emoji-annotations/ja.Dt-m5rD-.js.map delete mode 100644 priv/static/static/js/emoji-annotations/ko.DGjjBGsA.js delete mode 100644 priv/static/static/js/emoji-annotations/ko.DGjjBGsA.js.map create mode 100644 priv/static/static/js/emoji-annotations/ko.qCDFL19U.js create mode 100644 priv/static/static/js/emoji-annotations/ko.qCDFL19U.js.map create mode 100644 priv/static/static/js/emoji-annotations/nl.B2IenlFE.js create mode 100644 priv/static/static/js/emoji-annotations/nl.B2IenlFE.js.map delete mode 100644 priv/static/static/js/emoji-annotations/nl.BCmP3XAV.js delete mode 100644 priv/static/static/js/emoji-annotations/nl.BCmP3XAV.js.map delete mode 100644 priv/static/static/js/emoji-annotations/pl.C1i62U33.js delete mode 100644 priv/static/static/js/emoji-annotations/pl.C1i62U33.js.map create mode 100644 priv/static/static/js/emoji-annotations/pl.DxxjBECj.js create mode 100644 priv/static/static/js/emoji-annotations/pl.DxxjBECj.js.map create mode 100644 priv/static/static/js/emoji-annotations/pt.BmciJSYs.js create mode 100644 priv/static/static/js/emoji-annotations/pt.BmciJSYs.js.map delete mode 100644 priv/static/static/js/emoji-annotations/pt.qcD2jbqr.js delete mode 100644 priv/static/static/js/emoji-annotations/pt.qcD2jbqr.js.map delete mode 100644 priv/static/static/js/emoji-annotations/ro.Q0r94aGX.js delete mode 100644 priv/static/static/js/emoji-annotations/ro.Q0r94aGX.js.map create mode 100644 priv/static/static/js/emoji-annotations/ro.yzctWu4x.js create mode 100644 priv/static/static/js/emoji-annotations/ro.yzctWu4x.js.map create mode 100644 priv/static/static/js/emoji-annotations/ru.BLE9DyAq.js create mode 100644 priv/static/static/js/emoji-annotations/ru.BLE9DyAq.js.map delete mode 100644 priv/static/static/js/emoji-annotations/ru.BsjC3M1d.js delete mode 100644 priv/static/static/js/emoji-annotations/ru.BsjC3M1d.js.map delete mode 100644 priv/static/static/js/emoji-annotations/sk.BrZ6jXEe.js delete mode 100644 priv/static/static/js/emoji-annotations/sk.BrZ6jXEe.js.map create mode 100644 priv/static/static/js/emoji-annotations/sk.Dy63vbs6.js create mode 100644 priv/static/static/js/emoji-annotations/sk.Dy63vbs6.js.map delete mode 100644 priv/static/static/js/emoji-annotations/te.3rQpXN4b.js delete mode 100644 priv/static/static/js/emoji-annotations/te.3rQpXN4b.js.map create mode 100644 priv/static/static/js/emoji-annotations/te.BE2qiFNg.js create mode 100644 priv/static/static/js/emoji-annotations/te.BE2qiFNg.js.map delete mode 100644 priv/static/static/js/emoji-annotations/uk.C7RXZT0o.js delete mode 100644 priv/static/static/js/emoji-annotations/uk.C7RXZT0o.js.map create mode 100644 priv/static/static/js/emoji-annotations/uk.DYbkLgKB.js create mode 100644 priv/static/static/js/emoji-annotations/uk.DYbkLgKB.js.map create mode 100644 priv/static/static/js/emoji-annotations/zh.By94GFYU.js create mode 100644 priv/static/static/js/emoji-annotations/zh.By94GFYU.js.map delete mode 100644 priv/static/static/js/emoji-annotations/zh.DXJ_1nIQ.js delete mode 100644 priv/static/static/js/emoji-annotations/zh.DXJ_1nIQ.js.map create mode 100644 priv/static/static/js/emoji-annotations/zh_Hant.D6xeB4Z8.js create mode 100644 priv/static/static/js/emoji-annotations/zh_Hant.D6xeB4Z8.js.map delete mode 100644 priv/static/static/js/emoji-annotations/zh_Hant.ccXyIXaI.js delete mode 100644 priv/static/static/js/emoji-annotations/zh_Hant.ccXyIXaI.js.map create mode 100644 priv/static/static/js/emoji.C5uX9ZLF.js create mode 100644 priv/static/static/js/emoji.C5uX9ZLF.js.map delete mode 100644 priv/static/static/js/emoji.CF2IDMqM.js delete mode 100644 priv/static/static/js/emoji.CF2IDMqM.js.map create mode 100644 priv/static/static/js/emoji.CKB_ooIp.js create mode 100644 priv/static/static/js/emoji.CKB_ooIp.js.map create mode 100644 priv/static/static/js/emoji_picker.Ccx2u99H.js create mode 100644 priv/static/static/js/emoji_picker.Ccx2u99H.js.map create mode 100644 priv/static/static/js/error_modal.BRfbOgFr.js create mode 100644 priv/static/static/js/error_modal.BRfbOgFr.js.map create mode 100644 priv/static/static/js/export_import.DxS26Hs4.js create mode 100644 priv/static/static/js/export_import.DxS26Hs4.js.map create mode 100644 priv/static/static/js/flash.euQ77IAL.js create mode 100644 priv/static/static/js/flash.euQ77IAL.js.map create mode 100644 priv/static/static/js/folder_card.DRaMmZN4.js create mode 100644 priv/static/static/js/folder_card.DRaMmZN4.js.map create mode 100644 priv/static/static/js/follow_card.C3eWENM0.js create mode 100644 priv/static/static/js/follow_card.C3eWENM0.js.map create mode 100644 priv/static/static/js/follow_requests.BTnExoOV.js create mode 100644 priv/static/static/js/follow_requests.BTnExoOV.js.map create mode 100644 priv/static/static/js/free-regular-svg-icons.BGRGA4e_.js create mode 100644 priv/static/static/js/free-regular-svg-icons.BGRGA4e_.js.map create mode 100644 priv/static/static/js/free-solid-svg-icons.vn_kySUQ.js create mode 100644 priv/static/static/js/free-solid-svg-icons.vn_kySUQ.js.map create mode 100644 priv/static/static/js/helpers.DO6M90bq.js create mode 100644 priv/static/static/js/helpers.DO6M90bq.js.map delete mode 100644 priv/static/static/js/i18n/ar.Cr-3kZBh.js delete mode 100644 priv/static/static/js/i18n/ar.Cr-3kZBh.js.map create mode 100644 priv/static/static/js/i18n/ar.PF4_zGG8.js create mode 100644 priv/static/static/js/i18n/ar.PF4_zGG8.js.map delete mode 100644 priv/static/static/js/i18n/ca.Bv4d2Muy.js.map rename priv/static/static/js/i18n/{ca.Bv4d2Muy.js => ca.DN5rtM_y.js} (56%) create mode 100644 priv/static/static/js/i18n/ca.DN5rtM_y.js.map create mode 100644 priv/static/static/js/i18n/cs.Ch8ryXjb.js create mode 100644 priv/static/static/js/i18n/cs.Ch8ryXjb.js.map delete mode 100644 priv/static/static/js/i18n/cs.EyNXpVXW.js delete mode 100644 priv/static/static/js/i18n/cs.EyNXpVXW.js.map delete mode 100644 priv/static/static/js/i18n/de.CvXWIhiF.js.map rename priv/static/static/js/i18n/{de.CvXWIhiF.js => de.sg00fJvg.js} (56%) create mode 100644 priv/static/static/js/i18n/de.sg00fJvg.js.map delete mode 100644 priv/static/static/js/i18n/eo.C9h09SOv.js delete mode 100644 priv/static/static/js/i18n/eo.C9h09SOv.js.map create mode 100644 priv/static/static/js/i18n/eo.siiZsXXQ.js create mode 100644 priv/static/static/js/i18n/eo.siiZsXXQ.js.map delete mode 100644 priv/static/static/js/i18n/es.BFMlvXsf.js.map rename priv/static/static/js/i18n/{es.BFMlvXsf.js => es.BfXuHyuJ.js} (57%) create mode 100644 priv/static/static/js/i18n/es.BfXuHyuJ.js.map create mode 100644 priv/static/static/js/i18n/et.BrU7KoHa.js create mode 100644 priv/static/static/js/i18n/et.BrU7KoHa.js.map delete mode 100644 priv/static/static/js/i18n/et.fhuMKZXQ.js delete mode 100644 priv/static/static/js/i18n/et.fhuMKZXQ.js.map delete mode 100644 priv/static/static/js/i18n/eu.C1UxLEb6.js.map rename priv/static/static/js/i18n/{eu.C1UxLEb6.js => eu.CcYtkR8l.js} (50%) create mode 100644 priv/static/static/js/i18n/eu.CcYtkR8l.js.map delete mode 100644 priv/static/static/js/i18n/fa.BxVyTR8q.js delete mode 100644 priv/static/static/js/i18n/fa.BxVyTR8q.js.map create mode 100644 priv/static/static/js/i18n/fa.Ct0q0avS.js create mode 100644 priv/static/static/js/i18n/fa.Ct0q0avS.js.map delete mode 100644 priv/static/static/js/i18n/fi.Brm9lhAA.js.map rename priv/static/static/js/i18n/{fi.Brm9lhAA.js => fi.PwO5kZWe.js} (54%) create mode 100644 priv/static/static/js/i18n/fi.PwO5kZWe.js.map create mode 100644 priv/static/static/js/i18n/fr.C55JqQ_m.js create mode 100644 priv/static/static/js/i18n/fr.C55JqQ_m.js.map delete mode 100644 priv/static/static/js/i18n/fr.DzBG0KvL.js delete mode 100644 priv/static/static/js/i18n/fr.DzBG0KvL.js.map create mode 100644 priv/static/static/js/i18n/ga.C1ShdXcl.js create mode 100644 priv/static/static/js/i18n/ga.C1ShdXcl.js.map delete mode 100644 priv/static/static/js/i18n/ga.CPJnGxWv.js delete mode 100644 priv/static/static/js/i18n/ga.CPJnGxWv.js.map delete mode 100644 priv/static/static/js/i18n/he.BllNuLoC.js delete mode 100644 priv/static/static/js/i18n/he.BllNuLoC.js.map create mode 100644 priv/static/static/js/i18n/he.Bs9RetYt.js create mode 100644 priv/static/static/js/i18n/he.Bs9RetYt.js.map delete mode 100644 priv/static/static/js/i18n/hu.CljyAvQa.js delete mode 100644 priv/static/static/js/i18n/hu.CljyAvQa.js.map create mode 100644 priv/static/static/js/i18n/hu.UfPuJQT0.js create mode 100644 priv/static/static/js/i18n/hu.UfPuJQT0.js.map create mode 100644 priv/static/static/js/i18n/id.CLCh_oXi.js create mode 100644 priv/static/static/js/i18n/id.CLCh_oXi.js.map delete mode 100644 priv/static/static/js/i18n/id.DLjgbCu5.js delete mode 100644 priv/static/static/js/i18n/id.DLjgbCu5.js.map create mode 100644 priv/static/static/js/i18n/it.DfxTv4P6.js create mode 100644 priv/static/static/js/i18n/it.DfxTv4P6.js.map delete mode 100644 priv/static/static/js/i18n/it.ERg696CD.js delete mode 100644 priv/static/static/js/i18n/it.ERg696CD.js.map delete mode 100644 priv/static/static/js/i18n/ja_easy.PsomvmuF.js.map rename priv/static/static/js/i18n/{ja_easy.PsomvmuF.js => ja_easy.r4RI35B2.js} (53%) create mode 100644 priv/static/static/js/i18n/ja_easy.r4RI35B2.js.map create mode 100644 priv/static/static/js/i18n/ja_pedantic.Cy1uUP6w.js create mode 100644 priv/static/static/js/i18n/ja_pedantic.Cy1uUP6w.js.map delete mode 100644 priv/static/static/js/i18n/ja_pedantic.VWLd9bHt.js delete mode 100644 priv/static/static/js/i18n/ja_pedantic.VWLd9bHt.js.map rename priv/static/static/js/i18n/{ko.b1sv0xRp.js => ko.BB_3jPc2.js} (52%) create mode 100644 priv/static/static/js/i18n/ko.BB_3jPc2.js.map delete mode 100644 priv/static/static/js/i18n/ko.b1sv0xRp.js.map delete mode 100644 priv/static/static/js/i18n/nan-TW.Ci98X1S7.js.map rename priv/static/static/js/i18n/{nan-TW.Ci98X1S7.js => nan-TW.DniT5teG.js} (52%) create mode 100644 priv/static/static/js/i18n/nan-TW.DniT5teG.js.map rename priv/static/static/js/i18n/{nb.DCCmz1gh.js => nb.CSJp4rKA.js} (56%) create mode 100644 priv/static/static/js/i18n/nb.CSJp4rKA.js.map delete mode 100644 priv/static/static/js/i18n/nb.DCCmz1gh.js.map delete mode 100644 priv/static/static/js/i18n/nl.D6wYIlyz.js.map rename priv/static/static/js/i18n/{nl.D6wYIlyz.js => nl.DA-1A4I-.js} (57%) create mode 100644 priv/static/static/js/i18n/nl.DA-1A4I-.js.map delete mode 100644 priv/static/static/js/i18n/oc.C4DhJxhs.js.map rename priv/static/static/js/i18n/{oc.C4DhJxhs.js => oc.CXSjczlM.js} (58%) create mode 100644 priv/static/static/js/i18n/oc.CXSjczlM.js.map create mode 100644 priv/static/static/js/i18n/pdc.DOwCrGXH.js create mode 100644 priv/static/static/js/i18n/pdc.DOwCrGXH.js.map delete mode 100644 priv/static/static/js/i18n/pdc.FmFgRqLi.js delete mode 100644 priv/static/static/js/i18n/pdc.FmFgRqLi.js.map delete mode 100644 priv/static/static/js/i18n/pl.CPLK-gPs.js.map rename priv/static/static/js/i18n/{pl.CPLK-gPs.js => pl.Cmnd-Fcg.js} (54%) create mode 100644 priv/static/static/js/i18n/pl.Cmnd-Fcg.js.map rename priv/static/static/js/i18n/{pt.BzQabObA.js => pt.B0CD21EJ.js} (53%) create mode 100644 priv/static/static/js/i18n/pt.B0CD21EJ.js.map delete mode 100644 priv/static/static/js/i18n/pt.BzQabObA.js.map delete mode 100644 priv/static/static/js/i18n/ro.BA9koejw.js delete mode 100644 priv/static/static/js/i18n/ro.BA9koejw.js.map create mode 100644 priv/static/static/js/i18n/ro.BDDUzm6K.js create mode 100644 priv/static/static/js/i18n/ro.BDDUzm6K.js.map delete mode 100644 priv/static/static/js/i18n/ru.CYi-PMDA.js.map rename priv/static/static/js/i18n/{ru.CYi-PMDA.js => ru.CZj8h1eY.js} (61%) create mode 100644 priv/static/static/js/i18n/ru.CZj8h1eY.js.map create mode 100644 priv/static/static/js/i18n/sk.BHTmHTrO.js create mode 100644 priv/static/static/js/i18n/sk.BHTmHTrO.js.map delete mode 100644 priv/static/static/js/i18n/sk.fGdLyyK_.js delete mode 100644 priv/static/static/js/i18n/sk.fGdLyyK_.js.map delete mode 100644 priv/static/static/js/i18n/te.CikZ4jPW.js delete mode 100644 priv/static/static/js/i18n/te.CikZ4jPW.js.map create mode 100644 priv/static/static/js/i18n/te.DCldbst0.js create mode 100644 priv/static/static/js/i18n/te.DCldbst0.js.map create mode 100644 priv/static/static/js/i18n/uk.A8RhHFvE.js create mode 100644 priv/static/static/js/i18n/uk.A8RhHFvE.js.map delete mode 100644 priv/static/static/js/i18n/uk.CkmtgYNq.js delete mode 100644 priv/static/static/js/i18n/uk.CkmtgYNq.js.map delete mode 100644 priv/static/static/js/i18n/vi.BsGK9mjD.js.map rename priv/static/static/js/i18n/{vi.BsGK9mjD.js => vi.QgsLBEhg.js} (54%) create mode 100644 priv/static/static/js/i18n/vi.QgsLBEhg.js.map delete mode 100644 priv/static/static/js/i18n/zh.BZkJuR5j.js.map rename priv/static/static/js/i18n/{zh.BZkJuR5j.js => zh.Dbd2AYo_.js} (51%) create mode 100644 priv/static/static/js/i18n/zh.Dbd2AYo_.js.map create mode 100644 priv/static/static/js/i18n/zh_Hant.BFQs14ie.js create mode 100644 priv/static/static/js/i18n/zh_Hant.BFQs14ie.js.map delete mode 100644 priv/static/static/js/i18n/zh_Hant.dSMADUQ9.js delete mode 100644 priv/static/static/js/i18n/zh_Hant.dSMADUQ9.js.map create mode 100644 priv/static/static/js/image_cropper.CSuez09N.js create mode 100644 priv/static/static/js/image_cropper.CSuez09N.js.map create mode 100644 priv/static/static/js/instance_capabilities.B6a7_omF.js create mode 100644 priv/static/static/js/instance_capabilities.B6a7_omF.js.map create mode 100644 priv/static/static/js/interactions.Lac6uCf8.js create mode 100644 priv/static/static/js/interactions.Lac6uCf8.js.map create mode 100644 priv/static/static/js/interface_language_switcher.cmRBgy4K.js create mode 100644 priv/static/static/js/interface_language_switcher.cmRBgy4K.js.map create mode 100644 priv/static/static/js/languages.q6q0orhR.js create mode 100644 priv/static/static/js/languages.q6q0orhR.js.map create mode 100644 priv/static/static/js/list.sBmZfIXM.js create mode 100644 priv/static/static/js/list.sBmZfIXM.js.map create mode 100644 priv/static/static/js/lists.BBz5d_Ug.js create mode 100644 priv/static/static/js/lists.BBz5d_Ug.js.map create mode 100644 priv/static/static/js/lists_edit.BFmgx9sZ.js create mode 100644 priv/static/static/js/lists_edit.BFmgx9sZ.js.map create mode 100644 priv/static/static/js/lists_timeline.D0jKFKd9.js create mode 100644 priv/static/static/js/lists_timeline.D0jKFKd9.js.map create mode 100644 priv/static/static/js/locale.service.ol8gNUGd.js create mode 100644 priv/static/static/js/locale.service.ol8gNUGd.js.map create mode 100644 priv/static/static/js/lodash.daI9UflD.js create mode 100644 priv/static/static/js/lodash.daI9UflD.js.map create mode 100644 priv/static/static/js/main.CPF6dYCY.js create mode 100644 priv/static/static/js/main.CPF6dYCY.js.map delete mode 100644 priv/static/static/js/main.qxCxIqDs.js delete mode 100644 priv/static/static/js/main.qxCxIqDs.js.map create mode 100644 priv/static/static/js/media_modal.Bn3GxKv7.js create mode 100644 priv/static/static/js/media_modal.Bn3GxKv7.js.map create mode 100644 priv/static/static/js/media_viewer.B2Dx6m_D.js create mode 100644 priv/static/static/js/media_viewer.B2Dx6m_D.js.map create mode 100644 priv/static/static/js/modal.BLgTTi-o.js create mode 100644 priv/static/static/js/modal.BLgTTi-o.js.map create mode 100644 priv/static/static/js/moderation_tools.C6XQ3bpM.js create mode 100644 priv/static/static/js/moderation_tools.C6XQ3bpM.js.map create mode 100644 priv/static/static/js/mute_confirm.DfJoacGH.js create mode 100644 priv/static/static/js/mute_confirm.DfJoacGH.js.map create mode 100644 priv/static/static/js/notifications.WzZPeGzx.js create mode 100644 priv/static/static/js/notifications.WzZPeGzx.js.map create mode 100644 priv/static/static/js/oauth.C4JLKGAu.js create mode 100644 priv/static/static/js/oauth.C4JLKGAu.js.map create mode 100644 priv/static/static/js/oauth_callback.B2cngiTu.js create mode 100644 priv/static/static/js/oauth_callback.B2cngiTu.js.map create mode 100644 priv/static/static/js/panel_loading.0JmxtEZa.js create mode 100644 priv/static/static/js/panel_loading.0JmxtEZa.js.map create mode 100644 priv/static/static/js/password_reset.HOi7zEwD.js create mode 100644 priv/static/static/js/password_reset.HOi7zEwD.js.map create mode 100644 priv/static/static/js/pinch_zoom.Djntym73.js create mode 100644 priv/static/static/js/pinch_zoom.Djntym73.js.map create mode 100644 priv/static/static/js/poll_form.D-st5E13.js create mode 100644 priv/static/static/js/poll_form.D-st5E13.js.map create mode 100644 priv/static/static/js/popover.94nM-RDc.js create mode 100644 priv/static/static/js/popover.94nM-RDc.js.map create mode 100644 priv/static/static/js/post_status.BF7jtp5r.js create mode 100644 priv/static/static/js/post_status.BF7jtp5r.js.map create mode 100644 priv/static/static/js/post_status_modal.Cjqh58_u.js create mode 100644 priv/static/static/js/post_status_modal.Cjqh58_u.js.map create mode 100644 priv/static/static/js/preload-helper.zJ_50EbN.js create mode 100644 priv/static/static/js/progress_button.B2rAp_zx.js create mode 100644 priv/static/static/js/progress_button.B2rAp_zx.js.map create mode 100644 priv/static/static/js/promise_interval.BgbE32Aa.js create mode 100644 priv/static/static/js/promise_interval.BgbE32Aa.js.map create mode 100644 priv/static/static/js/quote.D9tjkbDR.js create mode 100644 priv/static/static/js/quote.D9tjkbDR.js.map create mode 100644 priv/static/static/js/quote_form.DhluIGdz.js create mode 100644 priv/static/static/js/quote_form.DhluIGdz.js.map create mode 100644 priv/static/static/js/registration.C-t5q0zl.js create mode 100644 priv/static/static/js/registration.C-t5q0zl.js.map create mode 100644 priv/static/static/js/remote_follow.DpK1hmxn.js create mode 100644 priv/static/static/js/remote_follow.DpK1hmxn.js.map create mode 100644 priv/static/static/js/runtime-dom.esm-bundler.DzcUlaDE.js create mode 100644 priv/static/static/js/runtime-dom.esm-bundler.DzcUlaDE.js.map create mode 100644 priv/static/static/js/search.CeRVMFFx.js create mode 100644 priv/static/static/js/search.CeRVMFFx.js.map create mode 100644 priv/static/static/js/select.BU31Em0g.js create mode 100644 priv/static/static/js/select.BU31Em0g.js.map delete mode 100644 priv/static/static/js/settings_modal.B74APXc5.js delete mode 100644 priv/static/static/js/settings_modal.B74APXc5.js.map create mode 100644 priv/static/static/js/settings_modal.BDTk2X9h.js create mode 100644 priv/static/static/js/settings_modal.BDTk2X9h.js.map create mode 100644 priv/static/static/js/settings_modal_admin_content.CBR-3uLX.js create mode 100644 priv/static/static/js/settings_modal_admin_content.CBR-3uLX.js.map delete mode 100644 priv/static/static/js/settings_modal_admin_content.DWMrR-Cb.js delete mode 100644 priv/static/static/js/settings_modal_admin_content.DWMrR-Cb.js.map create mode 100644 priv/static/static/js/settings_modal_user_content.C6A-ej51.js create mode 100644 priv/static/static/js/settings_modal_user_content.C6A-ej51.js.map delete mode 100644 priv/static/static/js/settings_modal_user_content.D-dohGjw.js delete mode 100644 priv/static/static/js/settings_modal_user_content.D-dohGjw.js.map create mode 100644 priv/static/static/js/shout_panel.D2n32sK6.js create mode 100644 priv/static/static/js/shout_panel.D2n32sK6.js.map create mode 100644 priv/static/static/js/side_drawer.Cq9nZmqN.js create mode 100644 priv/static/static/js/side_drawer.Cq9nZmqN.js.map create mode 100644 priv/static/static/js/status_history_modal.LzEdYEdX.js create mode 100644 priv/static/static/js/status_history_modal.LzEdYEdX.js.map delete mode 100644 priv/static/static/js/sticker_picker.B0j2YsOB.js delete mode 100644 priv/static/static/js/sticker_picker.B0j2YsOB.js.map create mode 100644 priv/static/static/js/sticker_picker.aQ7A7kiL.js create mode 100644 priv/static/static/js/sticker_picker.aQ7A7kiL.js.map delete mode 100644 priv/static/static/js/string_setting.QmUmmIFY.js delete mode 100644 priv/static/static/js/string_setting.QmUmmIFY.js.map create mode 100644 priv/static/static/js/swipe_click.DFwOeCbK.js create mode 100644 priv/static/static/js/swipe_click.DFwOeCbK.js.map create mode 100644 priv/static/static/js/tab_switcher.BMrlDICt.js create mode 100644 priv/static/static/js/tab_switcher.BMrlDICt.js.map create mode 100644 priv/static/static/js/terms_of_service_panel.rRFEmiHL.js create mode 100644 priv/static/static/js/terms_of_service_panel.rRFEmiHL.js.map create mode 100644 priv/static/static/js/unicode_domain_indicator.BnLqD48H.js create mode 100644 priv/static/static/js/unicode_domain_indicator.BnLqD48H.js.map create mode 100644 priv/static/static/js/update_notification.D5whTpWL.js create mode 100644 priv/static/static/js/update_notification.D5whTpWL.js.map delete mode 100644 priv/static/static/js/update_notification.DdDhzBg0.js delete mode 100644 priv/static/static/js/update_notification.DdDhzBg0.js.map create mode 100644 priv/static/static/js/user_card.DVbLIASH.js create mode 100644 priv/static/static/js/user_card.DVbLIASH.js.map create mode 100644 priv/static/static/js/user_link.CzPI9p_B.js create mode 100644 priv/static/static/js/user_link.CzPI9p_B.js.map create mode 100644 priv/static/static/js/user_note.CL52aTOE.js create mode 100644 priv/static/static/js/user_note.CL52aTOE.js.map create mode 100644 priv/static/static/js/user_profile.CZtkyu5S.js create mode 100644 priv/static/static/js/user_profile.CZtkyu5S.js.map create mode 100644 priv/static/static/js/user_profile_admin_view.BVpUbjXr.js create mode 100644 priv/static/static/js/user_profile_admin_view.BVpUbjXr.js.map create mode 100644 priv/static/static/js/user_profile_link_generator.WrdnW5sg.js create mode 100644 priv/static/static/js/user_profile_link_generator.WrdnW5sg.js.map create mode 100644 priv/static/static/js/user_reporting_modal.Drcvbv7N.js create mode 100644 priv/static/static/js/user_reporting_modal.Drcvbv7N.js.map create mode 100644 priv/static/static/js/user_timed_filter_modal.Njj7Gcae.js create mode 100644 priv/static/static/js/user_timed_filter_modal.Njj7Gcae.js.map create mode 100644 priv/static/static/js/vertical_tab_switcher.CgkOofcV.js create mode 100644 priv/static/static/js/vertical_tab_switcher.CgkOofcV.js.map create mode 100644 priv/static/static/js/video_attachment.DeOyHdhJ.js create mode 100644 priv/static/static/js/video_attachment.DeOyHdhJ.js.map create mode 100644 priv/static/static/js/vuex.esm-bundler.CWMfctgB.js create mode 100644 priv/static/static/js/vuex.esm-bundler.CWMfctgB.js.map create mode 100644 priv/static/static/js/who_to_follow.I-3jjy5y.js create mode 100644 priv/static/static/js/who_to_follow.I-3jjy5y.js.map create mode 100644 priv/static/static/js/who_to_follow_panel.CeJ6wsZ3.js create mode 100644 priv/static/static/js/who_to_follow_panel.CeJ6wsZ3.js.map diff --git a/priv/static/index.html b/priv/static/index.html index 4365aa736..ef3b0afa7 100644 --- a/priv/static/index.html +++ b/priv/static/index.html @@ -11,10 +11,45 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/priv/static/static/css/about.BPcl3NxB.css b/priv/static/static/css/about.BPcl3NxB.css new file mode 100644 index 000000000..98cee111a --- /dev/null +++ b/priv/static/static/css/about.BPcl3NxB.css @@ -0,0 +1 @@ +.mrf-section{margin:1em}.mrf-section table{text-align:left;width:100%;padding-bottom:1.1em;padding-left:.5em}.mrf-section table th,.mrf-section table td{vertical-align:text-top;width:11em;max-width:25em;overflow:hidden}.mrf-section table th+th,.mrf-section table td+td{width:auto}.staff-group{padding:1em}.staff-group .basic-user-card{padding-left:0}.About dl{padding-left:1em} diff --git a/priv/static/static/css/account_actions.DjGDDSEq.css b/priv/static/static/css/account_actions.DjGDDSEq.css new file mode 100644 index 000000000..a673bc6b5 --- /dev/null +++ b/priv/static/static/css/account_actions.DjGDDSEq.css @@ -0,0 +1 @@ +.AccountActions .ellipsis-button{text-align:center;width:2.5em;margin:-.5em 0;padding:.5em 0} diff --git a/priv/static/static/css/announcements_page.DvITSMCQ.css b/priv/static/static/css/announcements_page.DvITSMCQ.css new file mode 100644 index 000000000..03b9e050a --- /dev/null +++ b/priv/static/static/css/announcements_page.DvITSMCQ.css @@ -0,0 +1 @@ +.announcement-editor{flex-direction:column;align-items:stretch;display:flex}.announcement-editor .announcement-metadata{margin-top:.5em}.announcement-editor .post-textarea{resize:vertical;box-sizing:content-box;height:10em;overflow:visible}.announcement{border-bottom:1px solid var(--border);padding:var(--status-margin);border-radius:0}.announcement .heading,.announcement .body{margin-bottom:var(--status-margin)}.announcement .footer,.announcement .footer .times{flex-direction:column;display:flex}.announcement .footer .actions{-webkit-box-pack:space-evenly;flex-direction:row;justify-content:space-evenly;display:flex}.announcement .footer .actions .btn{flex:1;max-width:10em;margin:1em}.announcements-page .post-form{padding:var(--status-margin)}.announcements-page .post-form .heading,.announcements-page .post-form .body{margin-bottom:var(--status-margin)}.announcements-page .post-form .post-button{min-width:10em} diff --git a/priv/static/static/css/attributes_helper.DsgIU8rn.css b/priv/static/static/css/attributes_helper.DsgIU8rn.css new file mode 100644 index 000000000..a9bbf0cde --- /dev/null +++ b/priv/static/static/css/attributes_helper.DsgIU8rn.css @@ -0,0 +1 @@ +.input.emoji-input{flex-direction:column;padding:0;display:flex;position:relative}.input.emoji-input .emoji-picker-icon{cursor:pointer;height:100%;padding:0 .2em;font-size:1.3em;line-height:1.2em;position:absolute;bottom:0;right:0}.input.emoji-input .emoji-picker-icon:hover i{color:var(--text)}.input.emoji-input.-textarea .emoji-picker-icon{height:auto;padding:.2em}.input.emoji-input .emoji-picker-panel{z-index:20;margin-top:.2em;position:absolute}.input.emoji-input .emoji-picker-panel.hide{display:none}.input.emoji-input input,.input.emoji-input textarea{color:inherit;box-shadow:none;border:none;outline:none;flex:1 0 auto;background:0 0!important}.input.emoji-input.-with-picker textarea,.input.emoji-input.-with-picker input{padding-right:2.4em}.input.emoji-input .hidden-overlay{opacity:0;pointer-events:none;color:red;position:absolute;inset:0;overflow:hidden}.input.emoji-input .hidden-overlay .caret{border:1px solid red;width:0;margin-right:calc(-1ch - 1px)}.autocomplete-panel{position:absolute}.autocomplete-item.menu-item{padding-top:0;padding-bottom:0;display:flex}.autocomplete-item.menu-item .image{width:calc(var(--__line-height) + var(--__vertical-gap) * 2);height:calc(var(--__line-height) + var(--__vertical-gap) * 2);line-height:var(--__line-height);text-align:center;margin-right:var(--__horizontal-gap)}.autocomplete-item.menu-item .image img{width:calc(var(--__line-height) + var(--__vertical-gap) * 2);height:calc(var(--__line-height) + var(--__vertical-gap) * 2);object-fit:contain}.autocomplete-item.menu-item .image span{font-size:var(--__line-height);line-height:var(--__line-height)}.autocomplete-item.menu-item .label{flex-direction:column;justify-content:center;margin:0 .1em 0 .2em;display:flex}.autocomplete-item.menu-item .label .displayText{line-height:1.5}.autocomplete-item.menu-item .label .detailText{font-size:.6em;line-height:.6em} diff --git a/priv/static/static/css/basic_user_card.CXE-NVII.css b/priv/static/static/css/basic_user_card.CXE-NVII.css new file mode 100644 index 000000000..4a67ae8f5 --- /dev/null +++ b/priv/static/static/css/basic_user_card.CXE-NVII.css @@ -0,0 +1 @@ +.basic-user-card{--emoji-size:1em;flex:10em;min-width:1em;margin:0;line-height:1.25;display:flex}.basic-user-card-collapsed-content{text-align:left;flex:1;min-width:0;margin-left:.7em}.basic-user-card-user-name img{object-fit:contain;vertical-align:middle;width:16px;height:16px}.basic-user-card-user-name-value,.basic-user-card-screen-name{white-space:nowrap;text-overflow:ellipsis;max-width:100%;display:inline;overflow:hidden}.basic-user-card-expanded-content{flex:1;min-width:0;margin-left:.7em} diff --git a/priv/static/static/css/bookmark_folder_edit.BYhGGKc2.css b/priv/static/static/css/bookmark_folder_edit.BYhGGKc2.css new file mode 100644 index 000000000..2848c2697 --- /dev/null +++ b/priv/static/static/css/bookmark_folder_edit.BYhGGKc2.css @@ -0,0 +1 @@ +.BookmarkFolderEdit{--panel-body-padding:.5em;flex-direction:column;display:flex;overflow:hidden}.BookmarkFolderEdit .folder-edit-heading{grid-template-columns:auto minmax(50%,1fr)}.BookmarkFolderEdit .panel-body{gap:.5em;display:flex}.BookmarkFolderEdit .emoji-picker-panel{z-index:20;margin-top:2px;position:absolute}.BookmarkFolderEdit .emoji-picker-panel.hide{display:none}.BookmarkFolderEdit .input-emoji{width:2.5em;height:2.5em;padding:0}.BookmarkFolderEdit .input-emoji .iconEmoji{text-align:center;object-fit:contain;vertical-align:middle;width:2.5em;height:2.5em;display:inline-block}.BookmarkFolderEdit .input-emoji .iconEmoji>span{font-size:1.5rem;line-height:2.5rem}.BookmarkFolderEdit .input-emoji img.iconEmoji{box-sizing:border-box;padding:.25em}.BookmarkFolderEdit .input-wrap{flex-direction:column;gap:.5em;display:flex}.BookmarkFolderEdit .go-back-button{text-align:center;height:100%;width:var(--__panel-heading-height-inner);align-self:start;line-height:1}.BookmarkFolderEdit .btn{margin:0 .5em}.BookmarkFolderEdit .panel-footer{grid-template-columns:minmax(10%,1fr)}.BookmarkFolderEdit .panel-footer .footer-button{min-width:9em} diff --git a/priv/static/static/css/bookmark_folders.CitandGk.css b/priv/static/static/css/bookmark_folders.CitandGk.css new file mode 100644 index 000000000..dfd559b1c --- /dev/null +++ b/priv/static/static/css/bookmark_folders.CitandGk.css @@ -0,0 +1 @@ +.Bookmark-folders .new-folder-button{padding:0 .5em} diff --git a/priv/static/static/css/chat.6N-tF5xA.css b/priv/static/static/css/chat.6N-tF5xA.css new file mode 100644 index 000000000..44a8e4aa2 --- /dev/null +++ b/priv/static/static/css/chat.6N-tF5xA.css @@ -0,0 +1 @@ +.chat-message-wrapper.hovered-message-chain .animated.Avatar canvas{display:none}.chat-message-wrapper.hovered-message-chain .animated.Avatar img{visibility:visible}.chat-message-wrapper .chat-message-menu{opacity:0;transition:opacity .1s;position:absolute;top:-.8em}.chat-message-wrapper .chat-message-menu button{padding-top:.2em;padding-bottom:.2em}.chat-message-wrapper .menu-icon{cursor:pointer}.chat-message-wrapper .popover{width:12em}.chat-message-wrapper .chat-message{padding-bottom:.5em;display:flex}.chat-message-wrapper .chat-message .status-body:hover{--_still-image-img-visibility:visible;--_still-image-canvas-visibility:hidden;--_still-image-label-visibility:hidden}.chat-message-wrapper .avatar-wrapper{width:32px;margin-right:.72em}.chat-message-wrapper .link-preview,.chat-message-wrapper .attachments{margin-bottom:1em}.chat-message-wrapper .status{background-color:var(--background);color:var(--text);border-radius:var(--roundness);border:1px solid var(--border);padding:.75em;display:flex}.chat-message-wrapper .created-at{float:right;opacity:.8;margin:-1em 0 -.5em;font-size:.8em;font-style:italic;position:relative}.chat-message-wrapper .without-attachment .message-content .RichContent:after{content:" ";margin-right:5.4em;display:inline-block}.chat-message-wrapper .pending .status-content.media-body,.chat-message-wrapper .pending .created-at{color:var(--faint)}.chat-message-wrapper .error .status-content.media-body,.chat-message-wrapper .error .created-at{color:var(--badgeNotification)}.chat-message-wrapper .chat-message-inner{flex-direction:column;align-items:flex-start;width:100%;min-width:10em;max-width:80%;display:flex}.chat-message-wrapper .outgoing{flex-flow:wrap;place-content:end flex-end;display:flex}.chat-message-wrapper .outgoing .chat-message-inner{align-items:flex-end}.chat-message-wrapper .outgoing .chat-message-menu{right:.4rem}.chat-message-wrapper .incoming .chat-message-menu{left:.4rem}.chat-message-wrapper .chat-message-inner.with-media,.chat-message-wrapper .chat-message-inner.with-media .status{width:100%}.chat-message-wrapper .visible{opacity:1}.chat-message-date-separator{text-align:center;-webkit-user-select:none;user-select:none;color:var(--textFaint);margin:1.4em 0;font-size:.9em}.chat-view{height:100%;display:flex}.chat-view .chat-view-inner{width:100%;height:auto;display:flex;overflow:visible}.chat-view .chat-view-body{box-sizing:border-box;width:100%;min-height:calc(100vh - var(--navbar-height));border-radius:var(--roundness);border-bottom-right-radius:0;border-bottom-left-radius:0;flex-direction:column;margin:0;display:flex;overflow:visible}.chat-view .chat-view-body:after{border-radius:0}.chat-view .message-list{flex-direction:column;justify-content:end;height:100%;padding:0 .8em;display:flex}.chat-view .footer{z-index:1;position:sticky;bottom:0}.chat-view .chat-view-heading{grid-template-columns:auto minmax(50%,1fr)}.chat-view .go-back-button{text-align:center;height:100%;width:var(--__panel-heading-height-inner);align-self:start;line-height:1}.chat-view .jump-to-bottom-button{z-index:10;transition:all .35s;opacity:0;visibility:hidden;cursor:pointer;border-radius:100%;justify-content:center;align-items:center;width:2.5em;height:2.5em;transition-timing-function:cubic-bezier(0,1,.5,1);display:flex;position:absolute;top:-3.2em;right:1.3em;box-shadow:0 1px 1px #0000004d,0 2px 4px #0000004d}.chat-view .jump-to-bottom-button.visible{opacity:1;visibility:visible}.chat-view .jump-to-bottom-button .unread-message-count{border-radius:50px;margin-top:-1rem;padding:.1em;font-size:.8em;position:absolute;left:50%}.chat-view .jump-to-bottom-button .chat-loading-error{align-items:flex-end;width:100%;height:100%;display:flex}.chat-view .jump-to-bottom-button .chat-loading-error .error{width:100%} diff --git a/priv/static/static/css/chat_list.WHtUK6wd.css b/priv/static/static/css/chat_list.WHtUK6wd.css new file mode 100644 index 000000000..f36d2bbbe --- /dev/null +++ b/priv/static/static/css/chat_list.WHtUK6wd.css @@ -0,0 +1 @@ +.chat-list-item{box-sizing:border-box;cursor:pointer;flex-direction:row;width:100%;display:flex;overflow:hidden}.chat-list-item :focus{outline:none}.chat-list-item .chat-list-item-left{margin-right:1em}.chat-list-item .chat-list-item-center{box-sizing:border-box;width:100%;overflow:hidden}.chat-list-item .heading{justify-content:space-between;width:100%;line-height:1em;display:flex}.chat-list-item .heading-right{white-space:nowrap}.chat-list-item .name-and-account-name{text-overflow:ellipsis;white-space:nowrap;line-height:var(--post-line-height);flex-shrink:1;overflow:hidden}.chat-list-item .chat-preview{white-space:nowrap;text-overflow:ellipsis;color:var(--textFaint);width:100%;margin:.35em 0;display:flex;overflow:hidden}.chat-list-item a{color:var(--linkFaint);pointer-events:none;text-decoration:none}.chat-list-item:hover .animated.avatar canvas{display:none}.chat-list-item:hover .animated.avatar img{visibility:visible}.chat-list-item .chat-preview-body{--emoji-size:1.4em;padding-right:1em}.chat-list-item .time-wrapper{line-height:var(--post-line-height)}.chat-new .input-wrap{margin:.7em .5em;display:flex}.chat-new .input-wrap input{width:100%}.chat-new .search-icon{margin-right:.3em}.chat-new .member-list{padding-bottom:.7rem}.chat-new .go-back-button{text-align:center;height:100%;width:var(--__panel-heading-height-inner);align-self:start;line-height:1}.chat-list{min-height:25em;margin-bottom:0}.empty-chat-list-alert{color:var(--textFaint);justify-content:center;padding:3em;font-size:1.2em;display:flex} diff --git a/priv/static/static/css/chat_title.B1JBlEsZ.css b/priv/static/static/css/chat_title.B1JBlEsZ.css new file mode 100644 index 000000000..2c9a8ef85 --- /dev/null +++ b/priv/static/static/css/chat_title.B1JBlEsZ.css @@ -0,0 +1 @@ +.chat-title{text-overflow:ellipsis;white-space:nowrap;--emoji-size:1em;display:flex;overflow:hidden}.chat-title .username{text-overflow:ellipsis;white-space:nowrap;max-width:100%;display:inline;overflow:hidden}.chat-title .avatar-container{align-self:center;line-height:1}.chat-title .titlebar-avatar{border-radius:var(--roundness);width:1.5em;height:1.5em;margin-right:.5em}.chat-title .titlebar-avatar.animated:before{display:none} diff --git a/priv/static/static/css/checkbox.DY0WQ--N.css b/priv/static/static/css/checkbox.DY0WQ--N.css new file mode 100644 index 000000000..4b56cf537 --- /dev/null +++ b/priv/static/static/css/checkbox.DY0WQ--N.css @@ -0,0 +1 @@ +.checkbox{align-items:baseline;gap:0 .5em;min-height:1.2em;display:inline-flex;position:relative}.checkbox-indicator,.checkbox .label{align-self:center}.checkbox>.checkbox-indicator{line-height:inherit;width:1.2em;height:1.2em;box-shadow:none;--_shadow:var(--shadow);margin:0;padding:0;display:inline-block;position:relative}.checkbox-indicator:before{content:"✓";border-radius:var(--roundness);width:1.1em;height:1.1em;box-shadow:var(--_shadow);background-color:var(--background);vertical-align:top;text-align:center;color:#0000;box-sizing:border-box;font-size:1.1em;line-height:1.1em;transition:color .2s;display:block;position:absolute;inset:0;overflow:hidden}.checkbox.-radio .checkbox-indicator,.checkbox.-radio .checkbox-indicator:before{border-radius:9999px}.checkbox.-radio .checkbox-indicator:before{content:"•"}.checkbox .disabled .checkbox-indicator:before{background-color:#0000}.checkbox input[type=checkbox]:checked+.checkbox-indicator:before{color:var(--text)}.checkbox input[type=checkbox]:indeterminate+.checkbox-indicator:before{content:"–";color:var(--text)}.checkbox.indeterminate-fix input[type=checkbox]+.checkbox-indicator:before{content:"–"} diff --git a/priv/static/static/css/confirm_modal.CxXYsWAn.css b/priv/static/static/css/confirm_modal.CxXYsWAn.css new file mode 100644 index 000000000..73c55d9e4 --- /dev/null +++ b/priv/static/static/css/confirm_modal.CxXYsWAn.css @@ -0,0 +1 @@ +.confirm-modal .confirm-icon{margin-top:.5rem;margin-bottom:.5rem;margin-left:.75rem}.confirm-modal .content{text-align:left;justify-content:center;align-items:center;line-height:1.5;display:flex}.confirm-modal .content p{margin-top:.75em;margin-bottom:.75em}.confirm-modal .content p:first-child{margin-top:0}.confirm-modal .content p:last-child{margin-bottom:0}.confirm-modal .below:not(:empty){margin-top:1em}.confirm-modal .text{max-width:50ch;margin-left:.5em;margin-right:3.5em} diff --git a/priv/static/static/css/dialog_modal.DC5hTqSH.css b/priv/static/static/css/dialog_modal.DC5hTqSH.css new file mode 100644 index 000000000..a6d901cec --- /dev/null +++ b/priv/static/static/css/dialog_modal.DC5hTqSH.css @@ -0,0 +1 @@ +.dark-overlay:before{content:" ";cursor:default;z-index:2000;background:#1b1f2380;display:block;position:fixed;inset:0}.dialog-container{justify-content:center;place-items:center;display:grid;position:fixed;inset:0;overflow:auto}.dialog-modal.panel{z-index:2001;cursor:default;max-width:90vw;display:block}.dialog-modal.panel .dialog-modal-heading .title{text-align:center}.dialog-modal.panel .dialog-modal-content{white-space:normal;text-align:center;margin:0;padding:1rem}.dialog-modal.panel .dialog-modal-footer{border-top:1px solid var(--border);grid-gap:.5em;grid-template-columns:-webkit-min-content;grid-template-columns:min-content;grid-auto-columns:-webkit-min-content;grid-auto-columns:min-content;grid-auto-flow:column dense;justify-content:end;height:auto;margin:0;padding:.5em;display:grid}.dialog-modal.panel .dialog-modal-footer button{white-space:nowrap;width:auto;padding-left:2em;padding-right:2em}#modal.-mobile .dialog-container{-webkit-box-pack:stretch;justify-content:stretch;place-items:end stretch}#modal.-mobile .dialog-container.-center-mobile{align-items:center}#modal.-mobile .dialog-modal.panel{min-width:100vw}#modal.-mobile .dialog-modal-footer{flex-direction:column;grid-template-columns:1fr;grid-auto-rows:auto;grid-auto-flow:dense;justify-content:flex-end}#modal.-mobile .dialog-modal-footer button{grid-column:1;height:3em} diff --git a/priv/static/static/css/drafts.CvPb_YQj.css b/priv/static/static/css/drafts.CvPb_YQj.css new file mode 100644 index 000000000..2cf81a7bf --- /dev/null +++ b/priv/static/static/css/drafts.CvPb_YQj.css @@ -0,0 +1 @@ +.Draft{position:relative}.Draft .status-content{margin:.5em 0;padding:.5em}.Draft .status-preview{grid-gap:.5em;grid-template-columns:1fr;grid-auto-columns:10em;grid-auto-flow:column;align-items:start;max-width:100%;display:grid}.Draft .status-preview p{white-space:normal;overflow-x:hidden}.Draft .status-preview .poll-indicator-container{border-radius:var(--roundness);align-self:start;place-items:center;height:0;padding-bottom:62.5%;display:grid;position:relative}.Draft .status-preview .poll-indicator{box-sizing:border-box;border:1px solid var(--border);place-items:center;width:100%;height:100%;display:grid;position:absolute;inset:0}.Draft .actions{-webkit-box-pack:space-evenly;flex-direction:row;justify-content:space-evenly;display:flex}.Draft .actions .btn{flex:1;margin-left:1em;margin-right:1em}.Drafts .draft{width:100%;margin:1em 0}.Drafts .remove-all{justify-content:center;margin:1em;display:flex}.Drafts .empty-drafs-list-alert{color:var(--textFaint);justify-content:center;padding:3em;font-size:1.2em;display:flex} diff --git a/priv/static/static/css/edit_status_modal.egG_jIir.css b/priv/static/static/css/edit_status_modal.egG_jIir.css new file mode 100644 index 000000000..f9c1acb35 --- /dev/null +++ b/priv/static/static/css/edit_status_modal.egG_jIir.css @@ -0,0 +1 @@ +.modal-view.edit-form-modal-view{align-items:flex-start}.edit-form-modal-panel{flex-shrink:0;width:100%;max-width:700px;margin-top:25%;margin-bottom:2em}@media (orientation:landscape){.edit-form-modal-panel{margin-top:8%}}.edit-form-modal-panel .form-bottom-left{max-width:6.5em}.edit-form-modal-panel .form-bottom-left .emoji-icon{-webkit-box-pack:right;justify-content:right} diff --git a/priv/static/static/css/emoji_picker.CJFAWWOw.css b/priv/static/static/css/emoji_picker.CJFAWWOw.css new file mode 100644 index 000000000..bed01090c --- /dev/null +++ b/priv/static/static/css/emoji_picker.CJFAWWOw.css @@ -0,0 +1 @@ +.emoji-picker{--__emoji-picker-header:2.2em;flex-direction:column;width:25em;max-width:calc(100vw - 20px);display:flex}.emoji-picker-header-image{width:var(--__emoji-picker-header);max-width:var(--__emoji-picker-header);height:var(--__emoji-picker-header);max-height:var(--__emoji-picker-header);justify-content:center;align-items:center;display:inline-flex}.emoji-picker-header-image .still-image{width:var(--__emoji-picker-header);max-width:var(--__emoji-picker-header);height:var(--__emoji-picker-header);max-height:var(--__emoji-picker-header);object-fit:contain;--_still_image-label-scale:.5}.emoji-picker .keep-open,.emoji-picker .too-many-emoji,.emoji-picker .hide-custom-emoji{padding:.5em;line-height:normal}.emoji-picker .hide-custom-emoji{padding-top:0}.emoji-picker .too-many-emoji{flex-direction:column;display:flex}.emoji-picker .keep-open-label{padding:0 .5em;display:flex}.emoji-picker .heading{flex-direction:column;padding:.7em .5em 0;display:flex}.emoji-picker .content{flex-direction:column;flex:auto;min-height:0;display:flex}.emoji-picker .emoji-tabs{flex-flow:row;flex-grow:1;display:flex;overflow:auto hidden}.emoji-picker .additional-tabs{border-left:1px solid;border-left-color:var(--border);flex:none;padding-left:.5em;display:flex}.emoji-picker .additional-tabs,.emoji-picker .emoji-tabs{scrollbar-width:thin;flex-basis:auto;align-content:center;display:flex}.emoji-picker .additional-tabs-item,.emoji-picker .emoji-tabs-item{cursor:pointer;width:var(--__emoji-picker-header);max-width:var(--__emoji-picker-header);height:var(--__emoji-picker-header);max-height:var(--__emoji-picker-header);align-items:center;padding:0 .5em;display:flex}.emoji-picker .additional-tabs-item .svg-inline--fa,.emoji-picker .emoji-tabs-item .svg-inline--fa{font-size:1.85em}.emoji-picker .additional-tabs-item.disabled,.emoji-picker .emoji-tabs-item.disabled{opacity:.5;pointer-events:none}.emoji-picker .additional-tabs-item.toggled,.emoji-picker .emoji-tabs-item.toggled{border-bottom:.2em solid}.emoji-picker .sticker-picker{flex:auto}.emoji-picker .stickers-content,.emoji-picker .emoji-content{flex-direction:column;flex:auto;min-height:0;display:flex}.emoji-picker .stickers-content.hidden,.emoji-picker .emoji-content.hidden{opacity:0;pointer-events:none;position:absolute}.emoji-picker .emoji-search{flex:none;padding-bottom:.5em}.emoji-picker .emoji-search input{width:100%}.emoji-picker .emoji-groups{scrollbar-gutter:stable both-edges;-webkit-user-select:none;user-select:none;-webkit-mask:-webkit-linear-gradient(#fff 0,#0000 100%) bottom no-repeat,-webkit-linear-gradient(0deg,#fff 0,#0000 100%) top no-repeat,-webkit-linear-gradient(#fff,#fff);-webkit-mask:linear-gradient(0deg,#fff 0,#0000 100%) bottom no-repeat,linear-gradient(#fff 0,#0000 100%) top no-repeat,linear-gradient(#fff,#fff);mask:linear-gradient(0deg,#fff 0,#0000 100%) bottom no-repeat,linear-gradient(#fff 0,#0000 100%) top no-repeat,linear-gradient(#fff,#fff);-webkit-mask-composite:xor;height:100%;min-height:200px;-webkit-mask-size:100% 20px,100% 20px,auto;mask-size:100% 20px,100% 20px,auto;mask-composite:xor;-webkit-mask-composite:xor;flex:1px;transition:-webkit-mask-size .15s,mask-size .15s;position:relative;overflow:auto;-webkit-mask-composite:xor;mask-composite:exclude}.emoji-picker .emoji-groups.scrolled-top{-webkit-mask-size:100% 20px,100% 0,auto;mask-size:100% 20px,100% 0,auto}.emoji-picker .emoji-groups.scrolled-bottom{-webkit-mask-size:100% 0,100% 20px,auto;mask-size:100% 0,100% 20px,auto}.emoji-picker .emoji-item{width:var(--emoji-size);height:var(--emoji-size);box-sizing:border-box;line-height:var(--emoji-size);cursor:pointer;justify-content:center;align-items:center;margin:.2em;display:flex}.emoji-picker .emoji-item .emoji-picker-emoji.-custom{object-fit:contain;width:var(--emoji-size);max-width:var(--emoji-size);height:var(--emoji-size);max-height:var(--emoji-size);--_still_image-label-scale:.5}.emoji-picker .emoji-item .emoji-picker-emoji.-unicode{font-size:calc(var(--emoji-size) * .8);overflow:hidden}.emoji-picker .emoji-group{grid-template-columns:repeat(var(--__amount), 1fr);grid-template-rows:repeat(1,auto);justify-content:center;place-items:center;display:grid}.emoji-picker .emoji-group.first-row{grid-template-rows:repeat(2,auto)}.emoji-picker .emoji-group.first-row .emoji-item{grid-row:2}.emoji-picker .emoji-group-title{grid-column:span var(--__amount);width:100%;margin:0;padding-left:.3em;font-size:.85em}.emoji-picker .emoji-group-title.disabled{display:none} diff --git a/priv/static/static/css/error_modal.D1oWQ5nz.css b/priv/static/static/css/error_modal.D1oWQ5nz.css new file mode 100644 index 000000000..71a8cd43d --- /dev/null +++ b/priv/static/static/css/error_modal.D1oWQ5nz.css @@ -0,0 +1 @@ +.error-modal .error-icon{margin-top:.5rem;margin-bottom:.5rem;margin-left:.75rem}.error-modal .content{text-align:left;justify-content:center;align-items:center;line-height:1.5;display:flex}.error-modal .content p{margin-top:.75em;margin-bottom:.75em}.error-modal .content p:first-child{margin-top:0}.error-modal .content p:last-child{margin-bottom:0}.error-modal .stack{margin:0;overflow-x:auto}.error-modal .below:not(:empty){margin-top:1em}.error-modal .text{max-width:50ch;margin-left:.5em;margin-right:3.5em} diff --git a/priv/static/static/css/flash.BYNCHsIH.css b/priv/static/static/css/flash.BYNCHsIH.css new file mode 100644 index 000000000..ea125f91b --- /dev/null +++ b/priv/static/static/css/flash.BYNCHsIH.css @@ -0,0 +1 @@ +.Flash{width:100%;height:100%;display:inline-block;position:relative}.Flash .player{width:100%;height:100%}.Flash .placeholder{background:var(--bg);width:100%;height:100%;color:var(--link);justify-content:center;align-items:center;display:flex}.Flash .hider{top:0}.Flash .label{text-align:center;white-space:normal;flex:1 1 0;line-height:1.2}.Flash .hidden{visibility:hidden;display:none} diff --git a/priv/static/static/css/folder_card.CTk_063i.css b/priv/static/static/css/folder_card.CTk_063i.css new file mode 100644 index 000000000..36438d475 --- /dev/null +++ b/priv/static/static/css/folder_card.CTk_063i.css @@ -0,0 +1 @@ +.FolderCard{align-items:center;display:flex}.FolderCard .folder-name{flex-grow:1;align-items:center;display:flex}.FolderCard .folder-name .icon,.FolderCard .folder-name .iconLetter,.FolderCard .folder-name .iconEmoji{width:2.5rem;height:2.5rem;margin-right:.5rem;display:inline-block}.FolderCard .folder-name .icon,.FolderCard .folder-name .iconLetter{text-align:center;font-size:1.5rem;line-height:2.5rem}.FolderCard .folder-name .iconEmoji{text-align:center;object-fit:contain;vertical-align:middle}.FolderCard .folder-name .iconEmoji>span{font-size:1.5rem;line-height:2.5rem}.FolderCard .folder-name img.iconEmoji{box-sizing:border-box;padding:.25em} diff --git a/priv/static/static/css/follow_card.jWN765qC.css b/priv/static/static/css/follow_card.jWN765qC.css new file mode 100644 index 000000000..a07f86dd2 --- /dev/null +++ b/priv/static/static/css/follow_card.jWN765qC.css @@ -0,0 +1 @@ +.follow-card-content-container{flex-flow:wrap;flex-shrink:0;justify-content:space-between;line-height:1.5em;display:flex}.follow-card-button{margin-top:.5em;margin-left:1em;padding:0 1.5em}.follow-card-follow-button{width:10em;margin-top:.5em;margin-left:auto} diff --git a/priv/static/static/css/follow_requests.5W1jgYR0.css b/priv/static/static/css/follow_requests.5W1jgYR0.css new file mode 100644 index 000000000..f6c20e035 --- /dev/null +++ b/priv/static/static/css/follow_requests.5W1jgYR0.css @@ -0,0 +1 @@ +.follow-request-card-content-container{flex-flow:wrap;display:flex}.follow-request-card-content-container button{flex:1;min-width:8em;max-width:12em;margin-top:.5em;margin-right:.5em}.follow-request-card-content-container button:last-child{margin-right:0} diff --git a/priv/static/static/css/image_cropper.iV4LurEO.css b/priv/static/static/css/image_cropper.iV4LurEO.css new file mode 100644 index 000000000..a46d7b8de --- /dev/null +++ b/priv/static/static/css/image_cropper.iV4LurEO.css @@ -0,0 +1 @@ +.image-cropper{flex-direction:column;display:flex}.image-cropper-canvas,.image-cropper .image{height:100%}.image-cropper .image-cropper-img-input{display:none}.image-cropper-buttons-wrapper{grid-gap:.5em;grid-template-columns:1fr 1fr 1fr;display:grid}.image-cropper-buttons-wrapper button{margin-top:1em} diff --git a/priv/static/static/css/interface_language_switcher.DYf3Vv_u.css b/priv/static/static/css/interface_language_switcher.DYf3Vv_u.css new file mode 100644 index 000000000..cb92cd530 --- /dev/null +++ b/priv/static/static/css/interface_language_switcher.DYf3Vv_u.css @@ -0,0 +1 @@ +.interface-language-switcher .setting-list .setting-item{grid-template-columns:subgrid;display:grid}.interface-language-switcher .add-button{text-align:center;padding-bottom:1em;display:block}.interface-language-switcher .add-button .default-button{width:auto;display:block}.-mobile .interface-language-switcher li.setting-item{border-bottom:none;flex-direction:column;align-items:stretch;gap:.5em;display:flex}.-mobile .interface-language-switcher .add-button{border-bottom:1px solid var(--border)} diff --git a/priv/static/static/css/list.Cau2FUIa.css b/priv/static/static/css/list.Cau2FUIa.css new file mode 100644 index 000000000..9ffd28aa3 --- /dev/null +++ b/priv/static/static/css/list.Cau2FUIa.css @@ -0,0 +1 @@ +.List{--__line-height:1.5em;--__horizontal-gap:.75em;--__vertical-gap:.5em;flex-direction:column;display:flex}.List .list{flex:1}.List .list-item{align-items:center;display:flex}.List .list-item[aria-expanded=true]{border-bottom-width:1px}.List .list-item:first-child{border-top-right-radius:var(--roundness);border-top-left-radius:var(--roundness);border-top-width:0}.List .list-item:last-child{border-bottom-right-radius:var(--roundness);border-bottom-left-radius:var(--roundness);border-bottom-width:0}.List .list-item:not(:last-child){border-bottom:1px dotted var(--border)}.List .list-item:empty{border:none}.List .header{padding:var(--__vertical-gap) var(--__horizontal-gap);border-bottom:1px solid;border-bottom-color:var(--border);align-items:center;display:flex}.List .header .actions{flex:1}.List .footer{text-align:center;padding:.9em}.List .footer a{cursor:pointer}.List .checkbox-wrapper{padding-right:var(--__horizontal-gap);flex:none}.List.-scrollable{flex-direction:column;display:flex;overflow-y:hidden}.List.-scrollable .list{flex:auto;overflow-y:auto} diff --git a/priv/static/static/css/lists.R2bOz6oY.css b/priv/static/static/css/lists.R2bOz6oY.css new file mode 100644 index 000000000..565382562 --- /dev/null +++ b/priv/static/static/css/lists.R2bOz6oY.css @@ -0,0 +1 @@ +.Lists .new-list-button{padding:0 .5em} diff --git a/priv/static/static/css/lists_edit.D3Eu2cDo.css b/priv/static/static/css/lists_edit.D3Eu2cDo.css new file mode 100644 index 000000000..81dfa85a7 --- /dev/null +++ b/priv/static/static/css/lists_edit.D3Eu2cDo.css @@ -0,0 +1 @@ +.ListsUserSearch .input-wrap{margin:.7em .5em;display:flex}.ListsUserSearch .input-wrap input{width:100%}.ListsUserSearch .search-icon{margin-right:.3em}.ListEdit{--panel-body-padding:.5em;height:calc(100vh - var(--navbar-height));flex-direction:column;display:flex;overflow:hidden}.ListEdit .list-edit-heading{grid-template-columns:auto minmax(50%,1fr)}.ListEdit .panel-body{flex-direction:column;flex:1;display:flex;overflow:hidden}.ListEdit .list-member-management{flex:1 0 auto}.ListEdit .search-icon{margin-right:.3em}.ListEdit .users-list{padding-bottom:.7rem;overflow-y:auto}.ListEdit .search-list,.ListEdit .members-list{flex-direction:column;min-height:0;overflow:hidden}.ListEdit .go-back-button{text-align:center;height:100%;width:var(--__panel-heading-height-inner);align-self:start;line-height:1}.ListEdit .btn{margin:0 .5em}.ListEdit .panel-footer{grid-template-columns:minmax(10%,1fr)}.ListEdit .panel-footer .footer-button{min-width:9em} diff --git a/priv/static/static/css/main.0Iw0byjp.css b/priv/static/static/css/main.0Iw0byjp.css new file mode 100644 index 000000000..7e479480f --- /dev/null +++ b/priv/static/static/css/main.0Iw0byjp.css @@ -0,0 +1 @@ +.vue-recycle-scroller{position:relative}.vue-recycle-scroller.direction-vertical:not(.page-mode){overflow-y:auto}.vue-recycle-scroller.direction-horizontal:not(.page-mode){overflow-x:auto}.vue-recycle-scroller.direction-horizontal{display:flex}.vue-recycle-scroller__slot{-webkit-box-flex:1;-ms-flex:auto 0 0px;flex:none}.vue-recycle-scroller__item-wrapper{box-sizing:border-box;flex:1;position:relative;overflow:hidden}.vue-recycle-scroller.ready .vue-recycle-scroller__item-view{will-change:transform;position:absolute;top:0;left:0}.vue-recycle-scroller.direction-vertical .vue-recycle-scroller__item-wrapper{width:100%}.vue-recycle-scroller.direction-horizontal .vue-recycle-scroller__item-wrapper{height:100%}.vue-recycle-scroller.ready.direction-vertical .vue-recycle-scroller__item-view{width:100%}.vue-recycle-scroller.ready.direction-horizontal .vue-recycle-scroller__item-view{height:100%}.resize-observer[data-v-b329ee4c]{z-index:-1;pointer-events:none;opacity:0;background-color:#0000;border:none;width:100%;height:100%;display:block;position:absolute;top:0;left:0;overflow:hidden}.resize-observer[data-v-b329ee4c] object{pointer-events:none;z-index:-1;width:100%;height:100%;display:block;position:absolute;top:0;left:0;overflow:hidden}.HashtagLink{white-space:normal;color:var(--link);display:inline-block;position:relative}.user-popover{margin:0}.user-popover .user-info{width:100%;margin:1.2em}.user-popover .user-identity{min-width:min(30em,98vw)}.user-popover .user-card-inner{display:flex}.user-popover.popover{overflow:hidden}.MentionLink{white-space:normal;word-break:normal;display:inline;position:relative}.MentionLink .new,.MentionLink .original{display:inline}.MentionLink .mention-avatar{border-radius:var(--roundness);vertical-align:middle;-webkit-user-select:none;user-select:none;width:1.5em;height:1.5em;margin-right:.2em}.MentionLink .full{pointer-events:none;opacity:0;white-space:nowrap;z-index:1;-webkit-user-select:all;user-select:all;height:100%;margin-top:.25em;padding:.5em;transition:opacity .2s;display:inline-block;position:absolute;top:100%;left:0}.MentionLink .short.-with-tooltip,.MentionLink .you{-webkit-user-select:none;user-select:none}.MentionLink .short,.MentionLink .full{white-space:nowrap}.MentionLink .shortName{white-space:normal}.MentionLink .new.-you .shortName{font-weight:600}.MentionLink .new.-has-selection{--color:var(--selectionText);--link:var(--selectionText);background-color:var(--selectionBackground)}.MentionLink .new .at{color:var(--link);opacity:.8;vertical-align:-25%;margin:0;padding:0 .1em;line-height:1;display:inline-block}.MentionLink .new.-striped .shortName{background-image:-webkit-repeating-linear-gradient(315deg, var(--____highlight-tintColor), var(--____highlight-tintColor) 5px, var(--____highlight-tintColor2) 5px, var(--____highlight-tintColor2) 10px);background-image:repeating-linear-gradient(135deg, var(--____highlight-tintColor), var(--____highlight-tintColor) 5px, var(--____highlight-tintColor2) 5px, var(--____highlight-tintColor2) 10px)}.MentionLink .new.-solid .shortName{background-image:-webkit-linear-gradient(var(--____highlight-tintColor2), var(--____highlight-tintColor2));background-image:linear-gradient(var(--____highlight-tintColor2), var(--____highlight-tintColor2))}.MentionLink .new.-side .shortName{box-shadow:0 -5px 3px -4px inset var(--____highlight-solidColor)}.MentionLink .serverName.-faded{color:var(--linkFaint)}.mention-link-popover{max-width:70ch;max-height:20rem;overflow:hidden}.MentionsLine{overflow-wrap:break-word;text-wrap:pretty}.MentionsLine .mention-link:not(:first-child):before{content:" "}.MentionsLine .showMoreLess{white-space:normal;color:var(--link);margin-left:.5em}.still-image{align-items:center;line-height:0;display:inline-flex;position:relative;overflow:hidden}.still-image canvas{object-fit:contain;width:100%;height:100%;visibility:var(--_still-image-canvas-visibility,visible);position:absolute;inset:0}.still-image img{object-fit:contain;width:100%;height:100%}.still-image.animated:before{zoom:var(--_still_image-label-scale,1);content:"gif";color:#fff;border-radius:var(--roundness);z-index:2;visibility:var(--_still-image-label-visibility,visible);background:#7f7f7f80;padding:2px 4px;font-size:.7em;line-height:1;display:block;position:absolute;top:.5em;left:.5em}.still-image.animated:hover canvas{display:none}.still-image.animated:hover:before{visibility:var(--_still-image-label-visibility,hidden)}.still-image.animated img{visibility:var(--_still-image-img-visibility,hidden)}.still-image.animated:hover img{visibility:visible}.emoji-popover{text-align:center;margin:0 .5em .5em}.emoji-popover .emoji{width:calc(var(--emoji-size) * 3);height:calc(var(--emoji-size) * 3)}.emoji-popover .emoji-popover-centered{justify-content:center;align-items:center;display:flex}.emoji-popover .emoji-popover-button{width:100%;margin-top:.5em}.emoji-popover .Select{width:100%}.RichContent{font-family:var(--font)}.RichContent.-faint{color:var(--text);--text:var(--textFaint)!important;--link:var(--linkFaint)!important;--funtextGreentext:var(--funtextGreentextFaint)!important;--funtextCyantext:var(--funtextCyantextFaint)!important}.RichContent.-faint a{color:var(--linkFaint)}.RichContent blockquote{border-left:.2em solid var(--textFaint);margin:.2em 0 .2em .2em;padding-left:1em;font-style:italic}.RichContent pre{overflow:auto}.RichContent code,.RichContent samp,.RichContent kbd,.RichContent var,.RichContent pre{font-family:var(--monoFont)}.RichContent p{margin:0 0 1em}.RichContent p:last-child{margin:0}.RichContent h1{margin:1.4em 0;font-size:1.1em;line-height:1.2em}.RichContent h2{margin:1em 0;font-size:1.1em}.RichContent h3{margin:1.2em 0;font-size:1em}.RichContent h4{margin:1.1em 0}.RichContent .img{display:inline-block}.RichContent .img button{display:inline-flex}.RichContent .emoji{width:var(--emoji-size,32px);height:var(--emoji-size,32px);display:inline-block}.RichContent.-allow-non-square-emoji .emoji{width:auto;max-width:calc(var(--emoji-size,32px) * 3);min-width:var(--emoji-size,32px)}.RichContent .img,.RichContent video{vertical-align:middle;object-fit:contain;max-width:100%;max-height:400px}.RichContent .greentext{color:var(--funtextGreentext)}.RichContent .cyantext{color:var(--funtextCyantext)}.RichContent .mfm{display:inline-block}.RichContent .mfm.-scale{font-size:calc(var(--emoji-size) / 2)}.RichContent .mfm:not(.-scale){--emoji-size:2em}.RichContent .mfm.-pause{animation-play-state:paused}.RichContent .mfm[data-mfm-operator=x2]{font-size:300%}.RichContent .mfm[data-mfm-operator=x3]{font-size:400%}.RichContent .mfm[data-mfm-operator=x4]{font-size:600%}.RichContent .mfm[data-mfm-operator=sparkle]{position:relative}.RichContent .mfm[data-mfm-operator=sparkle]:before,.RichContent .mfm[data-mfm-operator=sparkle]:after{content:"✨";opacity:.4;z-index:-1;animation-name:cheap-sparkle;animation-duration:1s;animation-timing-function:ease-in-out;animation-iteration-count:infinite;position:absolute}.RichContent .mfm[data-mfm-operator=sparkle]:before{animation-direction:alternate;left:0}.RichContent .mfm[data-mfm-operator=sparkle]:after{animation-direction:alternate-reverse;right:0}.RichContent .mfm .emoji{max-width:unset!important}.RichContent:hover .mfm{animation-play-state:running}a .RichContent{color:var(--link)!important}@keyframes mfm-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes mfm-spinX{0%{transform:perspective(8em)rotateX(0)}to{transform:perspective(8em)rotateX(360deg)}}@keyframes mfm-spinY{0%{transform:perspective(8em)rotateY(0)}to{transform:perspective(8em)rotateY(360deg)}}@keyframes mfm-jump{0%{transform:translateY(0)}25%{transform:translateY(-1em)}50%{transform:translateY(0)}75%{transform:translateY(-.5em)}to{transform:translateY(0)}}@keyframes mfm-bounce{0%{transform:translateY(0)scale(1)}25%{transform:translateY(-16px)scale(1)}50%{transform:translateY(0)scale(1)}75%{transform:translateY(0)scale(1.5,.75)}to{transform:translateY(0)scale(1)}}@keyframes mfm-twitch{0%{transform:translate(7px,-2px)}5%{transform:translate(-3px,1px)}10%{transform:translate(-7px,-1px)}15%{transform:translateY(-1px)}20%{transform:translate(-8px,6px)}25%{transform:translate(-4px,-3px)}30%{transform:translate(-4px,-6px)}35%{transform:translate(-8px,-8px)}40%{transform:translate(4px,6px)}45%{transform:translate(-3px,1px)}50%{transform:translate(2px,-10px)}55%{transform:translate(-7px)}60%{transform:translate(-2px,4px)}65%{transform:translate(3px,-8px)}70%{transform:translate(6px,7px)}75%{transform:translate(-7px,-2px)}80%{transform:translate(-7px,-8px)}85%{transform:translate(9px,3px)}90%{transform:translate(-3px,-2px)}95%{transform:translate(-10px,2px)}to{transform:translate(-2px,-6px)}}@keyframes mfm-shake{0%{transform:translate(-3px,-1px)rotate(-8deg)}5%{transform:translateY(-1px)rotate(-10deg)}10%{transform:translate(1px,-3px)rotate(0)}15%{transform:translate(1px,1px)rotate(11deg)}20%{transform:translate(-2px,1px)rotate(1deg)}25%{transform:translate(-1px,-2px)rotate(-2deg)}30%{transform:translate(-1px,2px)rotate(-3deg)}35%{transform:translate(2px,1px)rotate(6deg)}40%{transform:translate(-2px,-3px)rotate(-9deg)}45%{transform:translateY(-1px)rotate(-12deg)}50%{transform:translate(1px,2px)rotate(10deg)}55%{transform:translateY(-3px)rotate(8deg)}60%{transform:translate(1px,-1px)rotate(8deg)}65%{transform:translateY(-1px)rotate(-7deg)}70%{transform:translate(-1px,-3px)rotate(6deg)}75%{transform:translateY(-2px)rotate(4deg)}80%{transform:translate(-2px,-1px)rotate(3deg)}85%{transform:translate(1px,-3px)rotate(-10deg)}90%{transform:translate(1px)rotate(3deg)}95%{transform:translate(-2px)rotate(-3deg)}to{transform:translate(2px,1px)rotate(2deg)}}@keyframes mfm-rubberBand{0%{transform:scale(1)}30%{transform:scale(1.25,.75)}40%{transform:scale(.75,1.25)}50%{transform:scale(1.15,.85)}65%{transform:scale(.95,1.05)}75%{transform:scale(1.05,.95)}to{transform:scale(1)}}@keyframes mfm-rainbow{0%{filter:hue-rotate()contrast(150%)saturate(150%)}to{filter:hue-rotate(360deg)contrast(150%)saturate(150%)}}@keyframes cheap-sparkle{0%{filter:hue-rotate()contrast(150%)saturate(150%);transform:translateY(-.5em)}to{filter:hue-rotate(360deg)contrast(150%)saturate(150%);transform:translateY(.5em)}}.avatars{flex-wrap:wrap;height:24px;margin:0;padding:0;display:flex}.avatars .avatars-item{margin:0 0 5px 5px}.avatars .avatars-item:first-child{padding-left:5px}.avatars .avatars-item .avatar-small{border-radius:var(--roundness);width:24px;height:24px}.user-list-popover{--emoji-size:calc(var(--emojiSize,32px) / 2);padding:.5em}.user-list-popover .user-list-row{color:var(--text);flex-direction:row;padding:.25em;display:flex}.user-list-popover .user-list-row .user-list-names{flex-direction:column;min-width:5em;margin-left:.5em;display:flex}.user-list-popover .user-list-row .user-list-screen-name{font-size:.65em}.EmojiReactions{--emoji-size:calc(var(--emojiSize,1.25em) * var(--emojiReactionsScale,1));flex-wrap:wrap;margin-top:.25em;display:flex}.EmojiReactions .emoji-reaction-container{align-items:stretch;margin-top:.5em;margin-right:.5em;display:flex}.EmojiReactions .emoji-reaction-container .emoji-reaction-popover{padding:0}.EmojiReactions .emoji-reaction-container .emoji-reaction-popover .emoji-reaction-count-button{box-sizing:border-box;border-top-left-radius:0;border-bottom-left-radius:0;justify-content:center;align-items:center;min-width:2em;height:100%;margin:0;display:inline-flex}.EmojiReactions .emoji-reaction{box-sizing:border-box;border-top-right-radius:0;border-bottom-right-radius:0;justify-content:center;align-items:center;margin:0;padding-left:.5em;display:flex}.EmojiReactions .emoji-reaction .reaction-emoji{width:var(--emoji-size);height:var(--emoji-size);line-height:var(--emoji-size);justify-content:center;align-items:center;margin-right:.25em;display:flex}.EmojiReactions .emoji-reaction .reaction-emoji.-wide{width:auto;min-width:var(--emoji-size);max-width:calc(var(--emoji-size) * 3)}.EmojiReactions .emoji-reaction .reaction-emoji{--_still_image-label-scale:.3}.EmojiReactions .emoji-reaction .reaction-emoji-content{max-width:100%;max-height:100%;width:var(--emoji-size);height:var(--emoji-size);line-height:inherit;font-size:calc(var(--emoji-size) * .8);margin:0;overflow:hidden}.EmojiReactions .emoji-reaction .reaction-emoji-content.-wide{width:auto;min-width:var(--emoji-size);max-width:calc(var(--emoji-size) * 3)}.EmojiReactions .emoji-reaction .reaction-emoji-content img{object-fit:contain}.EmojiReactions .emoji-reaction:focus{outline:none}.EmojiReactions .emoji-reaction .svg-inline--fa{color:var(--text)}.EmojiReactions .emoji-reaction.-picked-reaction .svg-inline--fa{color:var(--accent)}.EmojiReactions .emoji-reaction .focus-marker{visibility:hidden}.EmojiReactions .emoji-reaction .active-marker{visibility:visible}.EmojiReactions .emoji-reaction:focus:not(:focus-visible,:hover) .focus-marker{visibility:hidden}.EmojiReactions .emoji-reaction:focus:not(:focus-visible,:hover) .active-marker{visibility:visible}.EmojiReactions .emoji-reaction:hover .svg-inline--fa,.EmojiReactions .emoji-reaction:focus .svg-inline--fa{color:var(--accent)}.EmojiReactions .emoji-reaction:hover .focus-marker,.EmojiReactions .emoji-reaction:focus .focus-marker{visibility:visible}.EmojiReactions .emoji-reaction:hover .active-marker,.EmojiReactions .emoji-reaction:focus .active-marker{visibility:hidden}.EmojiReactions .emoji-reaction:focus-visible .svg-inline--fa{color:var(--accent)}.EmojiReactions .emoji-reaction:focus-visible .focus-marker{visibility:visible}.EmojiReactions .emoji-reaction:focus-visible .active-marker{visibility:hidden}.EmojiReactions .emoji-reaction-expand{justify-content:center;align-items:center;margin-top:.5em;margin-right:.5em;padding:0 .5em;display:flex}.EmojiReactions .emoji-reaction-expand:hover{text-decoration:underline}.Attachment{border-radius:var(--roundness);border-style:solid;border-width:1px;border-color:var(--border);flex-direction:column;align-self:flex-start;height:100%;line-height:0;display:inline-flex;position:relative}.Attachment .attachment-wrapper{flex:auto;height:100%;position:relative;overflow:hidden}.Attachment .description-container{z-index:1;flex:0 1 0;padding-top:.5em;display:flex}.Attachment .description-container p{text-align:center;white-space:nowrap;text-overflow:ellipsis;flex:1;margin:0;padding:.5em;line-height:1.5;overflow:hidden}.Attachment .description-container.-static{background:var(--popover);box-shadow:var(--popupShadow);padding-top:0;position:absolute;bottom:0;left:0;right:0}.Attachment .description-field{flex:1;min-width:0}.Attachment .placeholder-container,.Attachment .image-container,.Attachment .audio-container,.Attachment .video-container,.Attachment .flash-container,.Attachment .oembed-container{justify-content:center;width:100%;height:100%;display:flex}.Attachment .image-container .image{width:100%;height:100%}.Attachment .flash-container .flash,.Attachment .flash-container video,.Attachment .video-container .flash,.Attachment .video-container video{object-fit:contain;align-self:center;width:100%;height:100%}.Attachment .video-container{color:inherit;background:0 0;border:none;outline:none}.Attachment .audio-container{align-items:flex-end;display:flex}.Attachment .audio-container audio{width:100%;height:100%}.Attachment .placeholder-container{flex-direction:column;justify-content:center;align-items:center;padding-top:.5em;display:flex}.Attachment .play-icon{color:#ffffffbf;text-shadow:0 0 2px #0006;font-size:4.5em;position:absolute;top:calc(50% - 2.25rem);left:calc(50% - 2.25rem)}.Attachment .play-icon:before{margin:0}.Attachment .attachment-buttons{z-index:1;margin-top:.5em;margin-right:.5em;display:flex;position:absolute;top:0;right:0}.Attachment .attachment-buttons .attachment-button{border-radius:var(--roundness);text-align:center;width:2em;height:2em;margin-left:.5em;padding:0;font-size:1em}.Attachment.-contain-fit img,.Attachment.-contain-fit canvas{object-fit:contain}.Attachment.-cover-fit img,.Attachment.-cover-fit canvas{object-fit:cover}.Attachment .oembed-container{flex:1 0 100%;width:100%;margin-right:15px;line-height:1.2em;display:flex}.Attachment .oembed-container img{width:100%}.Attachment .oembed-container .image{flex:1}.Attachment .oembed-container .image img{object-fit:cover;border:0;border-radius:5px;height:100%}.Attachment .oembed-container .text{overflow-wrap:break-word;text-wrap:pretty;flex:2;margin:8px}.Attachment .oembed-container .text h1{margin:0;font-size:1rem}.Attachment.-size-small .play-icon{zoom:.5;opacity:.7}.Attachment.-size-small .attachment-buttons{zoom:.7;opacity:.5}.Attachment.-editable{padding:.5em}.Attachment.-editable .description-container,.Attachment.-editable .attachment-buttons{margin:0}.Attachment.-placeholder{color:var(--link);white-space:nowrap;height:auto;line-height:1.5;display:inline-block;overflow:hidden}.Attachment.-placeholder:not(.-editable){border:none}.Attachment.-placeholder.-editable{flex-direction:row;align-items:baseline;display:flex}.Attachment.-placeholder.-editable .description-container,.Attachment.-placeholder.-editable .attachment-buttons{margin:0;padding:0;position:relative}.Attachment.-placeholder.-editable .description-container{flex:1;padding-left:.5em}.Attachment.-placeholder.-editable .attachment-buttons{order:99;align-self:center}.Attachment.-placeholder a{text-overflow:ellipsis;max-width:100%;display:inline-block;overflow:hidden}.Attachment.-placeholder svg{color:inherit}.Attachment.-loading{cursor:progress}.Attachment.-compact .placeholder-container{padding-bottom:.5em}.description-popover{box-sizing:border-box;width:50ch;max-width:90vw;padding:1em;overflow:hidden}.description-popover summary{pointer-events:none;margin-bottom:.5em;font-weight:700;display:inline-block}.description-popover span{text-wrap:pretty;white-space:pre-wrap;max-height:10.5em;line-height:1.5;display:block;overflow-y:auto}.Gallery .gallery-rows{flex-direction:column;display:flex}.Gallery .gallery-row{flex-grow:1;width:100%;height:0;position:relative}.Gallery .gallery-row .gallery-row-inner{flex-flow:wrap;align-content:stretch;display:flex;position:absolute;inset:0}.Gallery .gallery-row .gallery-row-inner .gallery-item{box-sizing:border-box;flex-grow:1;min-width:2em;height:100%;margin:0 .5em 0 0}.Gallery .gallery-row .gallery-row-inner .gallery-item:last-child{margin:0}.Gallery .gallery-row .gallery-row-inner.-grid{grid-gap:.5em;grid-template-columns:repeat(auto-fill,minmax(15em,1fr));width:100%;height:auto;display:grid;position:relative}.Gallery .gallery-row .gallery-row-inner.-grid .gallery-item{height:20em;margin:0}.Gallery .gallery-row.-grid,.Gallery .gallery-row.-minimal{height:auto}.Gallery .gallery-row.-grid .gallery-row-inner,.Gallery .gallery-row.-minimal .gallery-row-inner{position:relative}.Gallery .gallery-row:not(:first-child){margin-top:.5em}.Gallery.-long .gallery-rows{-webkit-mask:-webkit-linear-gradient(#fff,#0000) bottom/100% 70px no-repeat,-webkit-linear-gradient(#fff,#fff);-webkit-mask-composite:xor;max-height:25em;-webkit-mask:linear-gradient(#0000,#fff) bottom/100% 70px no-repeat,linear-gradient(#fff,#fff);mask:linear-gradient(#0000,#fff) bottom/100% 70px no-repeat,linear-gradient(#fff,#fff);mask-composite:xor;-webkit-mask-composite:xor;overflow:hidden;-webkit-mask-composite:xor;mask-composite:exclude}.Gallery .many-attachments-text{text-align:center;line-height:2}.Gallery .many-attachments-buttons{display:flex}.Gallery .many-attachments-button{flex:1;justify-content:center;line-height:2;display:flex}.Gallery .many-attachments-button button{padding:0 2em}.media-upload .hidden-input-file{display:none}label.media-upload{cursor:pointer}.ScopeSelector{display:inline-block}.ScopeSelector .scope{text-align:center;min-width:1.3em;min-height:1.3em;padding:.5em .25em;display:inline-block}.link-preview-card{cursor:pointer;flex-direction:row;margin-top:.5em;display:flex;overflow:hidden}.link-preview-card .card-image{flex-shrink:0;width:120px;max-width:25%}.link-preview-card .card-image img{object-fit:cover;border-radius:var(--roundness);width:100%;height:100%;max-height:10em}.link-preview-card .card-content{flex-direction:column;max-height:100%;margin:.5em;display:flex}.link-preview-card .card-host{font-size:.85em}.link-preview-card .card-description{text-overflow:ellipsis;overflow-wrap:break-word;text-wrap:pretty;max-height:calc(3.6em - 1px);margin:.5em 0 0;line-height:1.2em;overflow:hidden}.link-preview-card .nsfw-alert{margin:2em 0}.link-preview-card{color:var(--text);border-radius:var(--roundness);border-style:solid;border-width:1px;border-color:var(--border)}.poll .votes{flex-direction:column;margin:0 0 .5em;display:flex}.poll .poll-option{margin:.75em .5em}.poll .poll-option .input{line-height:inherit}.poll .option-result{height:100%;color:var(--textLight);flex-direction:row;display:flex;position:relative}.poll .option-result-label{z-index:1;overflow-wrap:break-word;text-wrap:pretty;align-items:center;padding:.1em .25em;display:flex}.poll .result-percentage{flex-shrink:0;width:3.5em}.poll .result-fill{border-radius:var(--roundness);height:100%;transition:width .5s;position:absolute;top:0;left:0}.poll input{width:3.5em}.poll.loading *{cursor:progress}.poll .poll-vote-button{margin-right:.5em;padding:0 1em}.poll .poll-checkbox{display:none}.StatusBody{flex-direction:column;display:flex}.StatusBody .emoji{--_still_image-label-scale:.5}.StatusBody .attachments{margin-top:.5em}.StatusBody .text,.StatusBody .summary{white-space:pre-wrap;overflow-wrap:break-word;text-wrap:pretty;line-height:var(--post-line-height)}.StatusBody .summary{padding-bottom:.5em;font-style:italic;display:block}.StatusBody .text.-single-line{white-space:nowrap;text-overflow:ellipsis;height:1.4em;overflow:hidden}.StatusBody .summary-wrapper{border-style:solid;border-width:0 0 1px;border-color:var(--border);flex-grow:0;margin-bottom:.5em}.StatusBody .summary-wrapper.-tall{position:relative}.StatusBody .summary-wrapper.-tall .summary{white-space:nowrap;text-overflow:ellipsis;max-height:2em;overflow:hidden}.StatusBody .text-wrapper{text-overflow:ellipsis;overflow-wrap:break-word;flex-flow:column;display:flex;position:relative;overflow:hidden}.StatusBody .text-wrapper.-tall-status{z-index:1}.StatusBody .text-wrapper.-tall-status:not(.-hidden){height:16em}.StatusBody .text-wrapper.-tall-status .media-body{-webkit-mask:-webkit-linear-gradient(#fff,#0000) bottom/100% 8em no-repeat,-webkit-linear-gradient(#fff,#fff);-webkit-mask-composite:xor;min-height:0;-webkit-mask:linear-gradient(#0000,#fff) bottom/100% 8em no-repeat,linear-gradient(#fff,#fff);mask:linear-gradient(#0000,#fff) bottom/100% 8em no-repeat,linear-gradient(#fff,#fff);mask-composite:xor;-webkit-mask-composite:xor;-webkit-mask-composite:xor;mask-composite:exclude}.StatusBody .text-wrapper.-expanded{overflow:visible}.StatusBody .tall-status-hider,.StatusBody .tall-subject-hider,.StatusBody .status-unhider,.StatusBody .cw-status-hider{overflow-wrap:break-word;text-wrap:pretty;text-align:center;width:100%;margin:.1em 0;display:inline-block}.StatusBody .status-unhider{margin-top:auto;padding-bottom:1em;position:sticky;bottom:0}.StatusBody .tall-subject-hider{padding-bottom:.5em}.StatusBody .tall-subject-hider:not(.cw-status-hider){z-index:2;height:5em;margin-top:10em;line-height:8em;position:absolute}.StatusBody .status-unhider,.StatusBody .cw-status-hider{overflow-wrap:break-word;text-wrap:pretty}.StatusBody .status-unhider svg,.StatusBody .cw-status-hider svg{color:inherit}.StatusBody .toggle-button{padding:.5em}.StatusBody.-compact{flex-direction:row;align-items:start}.StatusBody.-compact .body,.StatusBody.-compact .attachments{max-height:3.25em}.StatusBody.-compact .body{white-space:normal;-webkit-mask-image:-webkit-linear-gradient(0deg,#fff 2em,#0000 3em);-webkit-mask-position:0 0,0 0;mask-position:0 0,0 0;-webkit-mask-size:auto 3.5em,auto;mask-size:auto 3.5em,auto;-webkit-mask-repeat:repeat-x,repeat;mask-repeat:repeat-x,repeat;-webkit-mask-composite:xor;min-width:5em;mask-image:linear-gradient(#fff 2em,#0000 3em);mask-composite:xor;-webkit-mask-composite:xor;flex:5 auto;overflow:hidden;-webkit-mask-composite:xor;mask-composite:exclude}.StatusBody.-compact .attachments{flex:1 1 0;min-width:5em;height:100%;margin-top:0;margin-left:.5em}.StatusBody.-compact .summary-wrapper .summary:after{content:": "}.StatusBody.-compact .summary-wrapper{line-height:inherit;border:none;margin:0}.StatusBody.-compact .text-wrapper{width:100%;display:inline-block}.StatusContent{flex:1;min-width:0}.StatusContent .poll-icon{margin:.5em}.post-status-form{position:relative}.post-status-form .attachments{margin-bottom:.5em}.post-status-form .more-post-actions,.post-status-form .more-post-actions .btn{height:100%}.post-status-form .form-bottom{justify-content:space-between;height:2.5em;padding:.5em;display:flex}.post-status-form .form-bottom .post-button-group{width:10em;display:flex}.post-status-form .form-bottom .post-button-group .post-button{flex:1 0 auto}.post-status-form .form-bottom .post-button-group .more-post-actions{flex:none}.post-status-form .form-bottom p{margin:.35em;padding:.35em;display:flex}.post-status-form .form-bottom-left{gap:1.5em;margin-right:1em;display:flex}.post-status-form .form-bottom-left button{margin:-.5em;padding:.5em}.post-status-form .preview-heading{flex-wrap:wrap;margin:0 .5em;display:flex}.post-status-form .preview-toggle{cursor:pointer;-webkit-user-select:none;user-select:none;flex:10 0 auto}.post-status-form .preview-toggle:hover{text-decoration:underline}.post-status-form .preview-toggle svg,.post-status-form .preview-toggle i{margin-left:.2em;font-size:.8em;transform:rotate(90deg)}.post-status-form .preview-container{margin-bottom:1em}.post-status-form .preview-error{color:var(--textFaint);font-style:italic}.post-status-form .preview-status{border:1px solid var(--border);border-radius:var(--roundness);margin:0;padding:.5em}.post-status-form .reply-or-quote-selector{flex-wrap:wrap-reverse;grid-template-columns:1fr 1fr;gap:0 1em;margin-bottom:.5em;display:flex}.post-status-form .text-format .only-format{color:var(--textFaint)}.post-status-form .visibility-tray{justify-content:space-between;align-items:baseline;display:flex}.post-status-form .visibility-notice{border:1px solid var(--border);border-radius:var(--roundness)}.post-status-form .visibility-notice.edit-warning>:first-child{margin-top:0}.post-status-form .visibility-notice.edit-warning>:last-child{margin-bottom:0}.post-status-form .media-upload-icon{-webkit-box-pack:left;order:1;justify-content:left}.post-status-form .emoji-icon{order:2;justify-content:center}.post-status-form .poll-icon{order:3;justify-content:center}.post-status-form .quote-icon{-webkit-box-pack:right;order:4;justify-content:right}.post-status-form .bottom-left-button{flex:1;align-items:center;padding:0 .1em;font-size:1.85em;line-height:1.1;display:flex}.post-status-form .error{text-align:center}.post-status-form .media-upload-wrapper{width:18em;margin-bottom:.5em;margin-right:.2em}.post-status-form .media-upload-wrapper img,.post-status-form .media-upload-wrapper video{object-fit:contain;max-height:10em}.post-status-form .media-upload-wrapper .video{max-height:10em}.post-status-form .media-upload-wrapper input{flex:1;width:100%}.post-status-form .status-input-wrapper{flex-direction:column;width:100%;display:flex;position:relative}.post-status-form .btn[disabled]{cursor:not-allowed}.post-status-form form{flex-direction:column;margin:.6em;display:flex;position:relative}.post-status-form .form-group{flex-direction:column;padding:.25em .5em .5em;line-height:1.85;display:flex}.post-status-form .inputs-wrapper{padding:0}.post-status-form textarea.input.form-post-body{box-sizing:content-box;padding-bottom:calc(var(--_padding) + var(--post-line-height) * 1em);height:calc(var(--post-line-height) * 1em);min-height:calc(var(--post-line-height) * 1em);resize:none;text-wrap:stable;background:0 0;padding-right:.5em;transition:min-height .2s .1s;overflow:hidden}.post-status-form textarea.input.form-post-body.scrollable-form{overflow-y:auto}.post-status-form .main-input{position:relative}.post-status-form .subject-input{border-bottom:1px solid var(--border)}.post-status-form .character-counter{height:2.2em;margin:0;padding:0;line-height:2.2em;position:absolute;bottom:0;right:2.2em}.post-status-form .character-counter.error{color:var(--cRed)}@keyframes fade-in{0%{opacity:0}to{opacity:.6}}@keyframes fade-out{0%{opacity:.6}to{opacity:0}}.post-status-form .drop-indicator{opacity:.6;width:100%;height:100%;color:var(--text);background-color:var(--bg);border-radius:var(--roundness);border:2px dashed var(--text);justify-content:center;align-items:center;font-size:5em;display:flex;position:absolute}.post-status-form .auto-save-status{align-self:center}.bookmark-folder-menu{--__horizontal-gap:.5em;--_still-image-label-visibility:hidden}.bookmark-folder-menu .emoji{width:var(--__line-height);height:var(--__line-height)}.quick-action{border:2px solid #0000;justify-content:space-between;align-items:center;display:flex}.quick-action .chevron-popover .popover-trigger-button{display:flex}.quick-action .action-counter{text-overflow:ellipsis;white-space:nowrap;overflow-x:hidden}.quick-action .action-button-inner,.quick-action .extra-button{z-index:1;padding:.5em}.quick-action .separator{background-color:var(--icon);opacity:.75;align-self:stretch;width:1px;margin:.5em 0;display:block}.quick-action .action-button-inner{grid-gap:.125em;grid-template-columns:-webkit-max-content;grid-template-columns:max-content;grid-auto-flow:column;grid-auto-columns:-webkit-max-content;grid-auto-columns:max-content;align-items:center;display:grid}.quick-action .action-button-inner .focus-marker{visibility:hidden}.quick-action .action-button-inner .active-marker{visibility:visible}.quick-action .action-button-inner:focus:not(:focus-visible,:hover) .focus-marker{visibility:hidden}.quick-action .action-button-inner:focus:not(:focus-visible,:hover) .active-marker,.quick-action .action-button-inner:hover .focus-marker,.quick-action .action-button-inner:focus .focus-marker{visibility:visible}.quick-action .action-button-inner:hover .active-marker,.quick-action .action-button-inner:focus .active-marker{visibility:hidden}.quick-action .action-button-inner:focus-visible .focus-marker{visibility:visible}.quick-action .action-button-inner:focus-visible .active-marker{visibility:hidden}.quick-action.-pin{border:2px dashed var(--icon);border-radius:var(--roundness);grid-template-columns:minmax(-webkit-max-content, 1fr) auto;grid-template-columns:minmax(max-content,1fr) auto}.quick-action.-pin .action-button-inner{opacity:.8;pointer-events:none;padding-right:0}.quick-action.-pin .chevron-icon,.quick-action.-pin .extra-button,.quick-action.-pin .separator{display:none}.quick-action.-with-extra .action-button-inner,.quick-action.-with-extra .extra-button{padding-left:.25em;padding-right:.25em}.action-button{grid-auto-flow:column;align-items:center;padding:0;display:grid}.action-button .action-button-inner:hover.reply-button:not(.disabled) .svg-inline--fa,.action-button .action-button-inner.-active.reply-button:not(.disabled) .svg-inline--fa{color:var(--cBlue)}.action-button .action-button-inner:hover.retweet-button:not(.disabled) .svg-inline--fa,.action-button .action-button-inner.-active.retweet-button:not(.disabled) .svg-inline--fa{color:var(--cGreen)}.action-button .action-button-inner:hover.favorite-button:not(.disabled) .svg-inline--fa,.action-button .action-button-inner.-active.favorite-button:not(.disabled) .svg-inline--fa{color:var(--cOrange)}.action-button.-extra .action-counter{justify-self:end;margin-left:1em}.action-button.-extra .chevron-icon{justify-self:end}.action-button.-extra .extra-button{justify-content:end;justify-self:end}.StatusActionButtons .quick-action-buttons{grid-gap:.5em .1em;margin-left:-.5em;margin-top:var(--status-margin);grid-template-columns:repeat(auto-fill,minmax(3.75em,10%));grid-auto-rows:1fr;grid-auto-flow:dense;display:grid}.StatusActionButtons .pin-action-button{z-index:1;margin:0;padding:.5em;display:flex}.extra-action-buttons .extra-action{margin:0;padding-top:0;padding-bottom:0;padding-right:0}.status-popover.popover{border-color:var(--border);border-style:solid;border-width:1px;min-width:15em;max-width:95%;font-size:1rem}.status-popover.popover .Status.Status{border:none}.status-popover.popover .status-preview-no-content{text-align:center;padding:1em}.status-popover.popover .status-preview-no-content i{font-size:2em}.Status{white-space:normal;overflow-wrap:break-word;text-wrap:pretty;min-width:0}.Status:hover{--_still-image-img-visibility:visible;--_still-image-canvas-visibility:hidden;--_still-image-label-visibility:hidden}.Status .gravestone{padding:var(--status-margin);display:flex}.Status .gravestone .deleted-text{align-items:center;margin:.5em 0}.Status .status-container{padding:var(--status-margin);gap:var(--status-margin);display:flex}.Status .status-container>*{min-width:0}.Status .pin{justify-content:flex-end;align-items:center;margin-right:.5em;display:flex}._misclick-prevention .Status{pointer-events:none}._misclick-prevention .Status .attachments{pointer-events:initial;cursor:initial}.Status .left-side{flex:none}.Status .right-side{flex:auto}.Status .usercard{margin-bottom:var(--status-margin)}.Status .status-username{white-space:nowrap;text-overflow:ellipsis;--_still_image-label-scale:.25;--emoji-size:1em;flex-shrink:1;max-width:85%;margin-right:.4em;font-weight:700;overflow:hidden}.Status .status-favicon{object-fit:contain;width:1.2em;height:1.2em;margin-right:.4em}.Status .status-heading{margin-bottom:.5em}.Status .heading-name-row{justify-content:space-between;line-height:1.3;display:flex}.Status .heading-name-row a{white-space:nowrap;text-overflow:ellipsis;width:100%;display:inline-block;overflow-x:hidden}.Status .account-name{white-space:nowrap;text-overflow:ellipsis;flex:1 1 0;min-width:1em;margin-right:.4em;display:inline-block;overflow:hidden}.Status .account-name.unknown{min-width:8em}.Status .heading-left{min-width:0;display:flex}.Status .heading-right{word-break:keep-all;flex-shrink:0;align-self:baseline;display:flex}.Status .heading-right .button-unstyled{margin:-.2em;padding:.2em}.Status .heading-right .svg-inline--fa{margin-left:.25em}.Status .glued-label{white-space:nowrap;display:inline-flex}.Status .timeago{margin-right:.2em}.Status .heading-reply-row,.Status .heading-edited-row{align-content:baseline;align-items:stretch;max-width:100%;margin-top:.2em;font-size:.85em;line-height:130%;position:relative}.Status .reply-to-popover,.Status .reply-to-no-popover,.Status .mentions{flex-shrink:0;min-width:0;margin-right:.4em}.Status .reply-glued-label{margin-right:.5em}.Status .reply-to-popover .reply-to:hover:before{content:"";border-bottom:1px solid var(--faint);pointer-events:none;width:100%;display:block;position:absolute;bottom:0}.Status .reply-to-popover .faint-link:hover{text-decoration:none}.Status .reply-to-popover.-strikethrough .reply-to:after{content:"";border-bottom:1px solid var(--faint);pointer-events:none;width:100%;display:block;position:absolute;top:50%}.Status .mentions,.Status .reply-to{white-space:nowrap;position:relative}.Status .mentions-text,.Status .reply-to-text{color:var(--faint);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.Status .mentions-line{display:inline}.Status .replies{flex-wrap:wrap;margin-top:.25em;font-size:.85em;line-height:1.3;display:flex}.Status .replies>*{margin-right:.4em}.Status .reply-link{height:17px}.Status .repeat-info{padding:.4em var(--status-margin);align-items:center;display:flex}.Status .repeat-info .repeater-avatar{border-radius:var(--roundness);flex:0 0 1.5em;width:1.5em;height:1.5em;margin-left:2em}.Status .repeat-info .right-side{text-overflow:ellipsis;flex:auto;gap:.5em;margin-right:0;display:flex;overflow-x:hidden}.Status .repeat-info .right-side .repeater-name{flex:0 auto;margin:0}.Status .repeat-info .right-side .repeat-label{white-space:nowrap;flex:none}.Status .repeat-info .right-side .repeat-label .repeat-icon{vertical-align:middle;color:var(--cGreen)}.Status .repeat-info .right-side .emoji{vertical-align:middle;object-fit:contain;width:1em;height:1em}.Status .status-fadein{animation-name:fadein;animation-duration:.4s}@keyframes fadein{0%{opacity:0}to{opacity:1}}.Status .status-actions{width:100%;margin-top:var(--status-margin);grid-template-columns:1fr;grid-auto-columns:1fr;grid-auto-flow:column;display:grid;position:relative}.Status .muted{text-overflow:ellipsis;flex-wrap:nowrap;gap:1ex;height:1.2em;padding:.25em .6em;line-height:1.2em;display:flex;overflow:hidden}.Status .muted .status-username,.Status .muted .mute-reason{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.Status .muted .status-username{flex:0 auto;margin-right:.2em;font-size:smaller;font-weight:400;display:flex}.Status .muted .unmute{flex:none;margin-left:auto;display:block}.Status .reply-form{padding-top:0;padding-bottom:0}.Status .reply-body{flex:1}.Status .favs-repeated-users{margin-top:var(--status-margin)}.Status .stats{width:100%;line-height:1em;display:flex}.Status .avatar-row{flex:1;align-items:center;display:flex;position:relative;overflow:hidden}.Status .avatar-row:before{content:"";background-color:var(--textFaint);width:1px;height:100%;position:absolute;left:0}.Status .stat-count{margin-right:var(--status-margin);-webkit-user-select:none;user-select:none}.Status .stat-count .stat-title{color:var(--textFaint);text-transform:uppercase;font-size:.85em;position:relative}.Status .stat-count .stat-number{color:var(--text);font-size:1.1em;font-weight:bolder;line-height:1em}.Status .stat-count:hover .stat-title{text-decoration:underline}.SearchBar{vertical-align:baseline;justify-content:flex-end;align-items:baseline;display:flex}.SearchBar .search-bar-input,.SearchBar .search-button{height:2em}.SearchBar .search-bar-input{flex:1 0 auto}.SearchBar .cancel-search{height:50px}.SearchBar .cancel-icon{color:var(--text)}.DesktopNav{width:100%;z-index:var(--ZI_navbar)}.DesktopNav input{color:var(--inputTopbarText,var(--inputText))}.DesktopNav .inner-nav{grid-template-rows:var(--navbar-height);box-sizing:border-box;grid-template-columns:minmax(5em,1fr) auto minmax(5em,1fr);grid-template-areas:"sitename logo actions";max-width:980px;margin:auto;padding:0 1.2em;display:grid}.DesktopNav.-column-stretch .inner-nav{--miniColumn:25rem;--maxiColumn:45rem;--columnGap:1em;max-width:calc(var(--sidebarColumnWidth,var(--miniColumn)) + var(--contentColumnWidth,var(--maxiColumn)) + var(--columnGap))}.DesktopNav.-logoLeft .inner-nav{grid-template-columns:auto minmax(5em,1fr) minmax(5em,1fr);grid-template-areas:"logo sitename actions"}.DesktopNav.-column-stretch.-wide .inner-nav{max-width:calc(var(--sidebarColumnWidth,var(--miniColumn)) + var(--contentColumnWidth,var(--maxiColumn)) + var(--notifsColumnWidth,var(--miniColumn)) + var(--columnGap))}.DesktopNav .button-default,.DesktopNav .button-default svg{color:var(--text)}.DesktopNav .logo{transition:opacity;grid-area:logo;transition-duration:.1s;transition-timing-function:ease-out;position:relative}@media (width>=800px){.DesktopNav .logo{opacity:1!important}}.DesktopNav .logo .mask{background-color:var(--text);position:absolute;inset:0;-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:contain;mask-size:contain;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.DesktopNav .logo img{height:var(--navbar-height);display:inline-block}.DesktopNav .nav-icon{text-align:center;width:2em;height:100%;margin-left:.2em}.DesktopNav .sitename{grid-area:sitename}.DesktopNav .actions{text-align:right;z-index:1;grid-area:actions;justify-content:flex-end}.DesktopNav .item{line-height:var(--navbar-height);height:var(--navbar-height);display:flex}.DesktopNav .spacer{min-width:1em}.features-panel li{line-height:24px}.global-notice-list{top:calc(var(--navbar-height) + .5em);pointer-events:none;width:100%;z-index:var(--ZI_modals_popovers);flex-direction:column;align-items:center;display:flex;position:fixed}.global-notice-list .global-notice{pointer-events:auto;text-align:center;width:40em;max-width:calc(100% - 3em);margin-bottom:.5em;padding-left:1.5em;line-height:2;display:flex}.global-notice-list .global-notice .notice-message{flex:100%}.global-notice-list .close-notice{padding-right:.2em}.instance-specific-panel .panel-body{border-radius:var(--roundness)}.NavigationPins{flex-wrap:wrap;height:100%;display:flex;overflow:hidden}.NavigationPins.alert-dot-notification{background-color:var(--badgeNotification)}.NavigationPins.alert-dot-neutral{background-color:var(--badgeNeutral)}.NavigationPins .pinned-item{text-align:center;box-sizing:border-box;flex:1 0 3em;min-width:2em;height:100%;position:relative;overflow:visible}.NavigationPins .pinned-item .bookmark-emoji{box-sizing:border-box;height:100%;padding:.5em}.NavigationPins .pinned-item .bookmark-emoji,.NavigationPins .pinned-item .svg-inline--fa,.NavigationPins .pinned-item .iconLetter{margin:0}.NavigationPins .pinned-item.toggled{border-bottom:4px solid;margin-bottom:-4px}.MobileNav{z-index:var(--ZI_navbar)}.MobileNav .mobile-nav{line-height:var(--navbar-height);grid-template-rows:var(--navbar-height);box-sizing:border-box;grid-template-columns:2fr auto;width:100%;display:grid}.MobileNav .mobile-nav a{color:var(--link)}.MobileNav .mobile-inner-nav{align-items:center;width:100%;display:flex}.MobileNav .mobile-nav-button{text-align:center;cursor:pointer;padding:0 1em;display:inline-block;position:relative}.MobileNav .site-name{padding:0 .3em;display:inline-block}.MobileNav .item{display:flex}.MobileNav .mobile-notifications-drawer{width:100%;height:100vh;box-shadow:var(--shadow);z-index:var(--ZI_navbar);-webkit-overflow-scrolling:touch;background:var(--background);transition-property:transform;transition-duration:.25s;position:fixed;top:0;left:0;overflow-x:hidden;transform:translate(0)}.MobileNav .mobile-notifications-drawer.-closed{box-shadow:none;transform:translate(100%)}.MobileNav .mobile-notifications-header{z-index:calc(var(--ZI_navbar) + 100);width:100%;height:3.5em;box-shadow:var(--shadow);justify-content:space-between;align-items:center;line-height:3.5em;display:flex;position:absolute}.MobileNav .mobile-notifications-header .spacer{flex:1}.MobileNav .mobile-notifications-header .title{white-space:nowrap;text-overflow:ellipsis;margin-left:.6em;font-size:1.3em;overflow-x:hidden}.MobileNav .pins{flex:1}.MobileNav .pins .pinned-item{flex-grow:1}.MobileNav .mobile-notifications{width:100vw;height:calc(100vh - var(--navbar-height));margin-top:3.5em;overflow:hidden scroll}.MobileNav .mobile-notifications .notifications{box-shadow:none;border-radius:0;padding:0}.MobileNav .mobile-notifications .notifications .panel{box-shadow:none;border-radius:0;margin:0}.MobileNav .mobile-notifications .notifications .panel:after{border-radius:0}.MobileNav .mobile-notifications .notifications .panel .panel-heading{box-shadow:none;border-radius:0}.MobileNav .confirm-modal.dark-overlay:before{z-index:3000}.MobileNav .confirm-modal.dark-overlay .dialog-modal.panel{z-index:3001}.MobilePostButton.button-default{z-index:10;transition:transform .35s;border-radius:100%;justify-content:center;align-items:center;width:5em;height:5em;transition-timing-function:cubic-bezier(0,1,.5,1);display:flex;position:fixed;bottom:1.5em;right:1.5em;box-shadow:0 2px 2px #0000004d,0 4px 6px #0000004d}.MobilePostButton.hidden{transform:translateY(150%)}.MobilePostButton svg{color:var(--text);font-size:1.5em}@media (width>=801px){.new-status-button:not(.always-show){display:none}}.NavigationEntry.menu-item{--__line-height:2.5em;--__horizontal-gap:.5em;--__vertical-gap:.4em;padding:var(--__vertical-gap) var(--__horizontal-gap);grid-template-columns:1fr;grid-auto-columns:var(--__line-height);grid-gap:var(--__horizontal-gap);grid-auto-flow:column;align-items:baseline;display:grid}.NavigationEntry.menu-item[aria-expanded]{padding-right:var(--__horizontal-gap)}.NavigationEntry.menu-item .main-link{line-height:var(--__line-height);box-sizing:border-box}.NavigationEntry.menu-item .menu-icon{line-height:var(--__line-height);width:var(--__line-height);margin-right:var(--__horizontal-gap);padding:0}.NavigationEntry.menu-item .timelines-chevron,.NavigationEntry.menu-item .extra-button{line-height:var(--__line-height);text-align:center;width:100%;padding:0}.NavigationEntry.menu-item .badge{justify-self:center}.NavigationEntry.menu-item .iconEmoji{text-align:center;object-fit:contain;vertical-align:middle;height:var(--__line-height);width:var(--__line-height);display:inline-block}.NavigationEntry.menu-item .iconEmoji>span{font-size:1.5rem}.NavigationEntry.menu-item img.iconEmoji{box-sizing:border-box;padding:.25rem}.NavPanel .panel{box-shadow:var(--shadow);overflow:hidden}.NavPanel ul{margin:0;padding:0;list-style:none}.NavPanel .navigation-chevron{margin-left:.8em;margin-right:.8em;font-size:1.1em}.NavPanel .timelines-background{padding:0 0 0 .6em}.NavPanel .nav-panel-heading{--panel-heading-height-padding:0px}.login-panel .login-form{flex-direction:column;padding:.6em;display:flex}.login-panel .btn{width:10em;min-height:2em}.login-panel .register{flex:1}.login-panel .login-bottom{flex-direction:row;justify-content:space-between;align-items:center;margin-top:1em;display:flex}.login-panel .form-group{flex-direction:column;padding:.3em .5em .6em;line-height:24px;display:flex}.login-panel .login-error{margin:.5em;line-height:2;animation-name:shakeError;animation-duration:.4s;animation-timing-function:ease-in-out;display:flex}.login-panel .error-message{flex:1}.user-panel .panel{background:var(--background);-webkit-backdrop-filter:var(--backdrop-filter);backdrop-filter:var(--backdrop-filter)}.user-panel .user-info{margin:.6em .6em 0}.user-panel .user-info .Avatar{width:min(5em,20cqw);height:min(5em,20cqw)}.user-panel .post-status-form form{margin-top:0}.user-panel .signed-in{z-index:10}:root,:host{--fa-font-solid:normal 900 1em/1 "Font Awesome 7 Free";--fa-font-regular:normal 400 1em/1 "Font Awesome 7 Free";--fa-font-light:normal 300 1em/1 "Font Awesome 7 Pro";--fa-font-thin:normal 100 1em/1 "Font Awesome 7 Pro";--fa-font-duotone:normal 900 1em/1 "Font Awesome 7 Duotone";--fa-font-duotone-regular:normal 400 1em/1 "Font Awesome 7 Duotone";--fa-font-duotone-light:normal 300 1em/1 "Font Awesome 7 Duotone";--fa-font-duotone-thin:normal 100 1em/1 "Font Awesome 7 Duotone";--fa-font-brands:normal 400 1em/1 "Font Awesome 7 Brands";--fa-font-sharp-solid:normal 900 1em/1 "Font Awesome 7 Sharp";--fa-font-sharp-regular:normal 400 1em/1 "Font Awesome 7 Sharp";--fa-font-sharp-light:normal 300 1em/1 "Font Awesome 7 Sharp";--fa-font-sharp-thin:normal 100 1em/1 "Font Awesome 7 Sharp";--fa-font-sharp-duotone-solid:normal 900 1em/1 "Font Awesome 7 Sharp Duotone";--fa-font-sharp-duotone-regular:normal 400 1em/1 "Font Awesome 7 Sharp Duotone";--fa-font-sharp-duotone-light:normal 300 1em/1 "Font Awesome 7 Sharp Duotone";--fa-font-sharp-duotone-thin:normal 100 1em/1 "Font Awesome 7 Sharp Duotone";--fa-font-slab-regular:normal 400 1em/1 "Font Awesome 7 Slab";--fa-font-slab-press-regular:normal 400 1em/1 "Font Awesome 7 Slab Press";--fa-font-whiteboard-semibold:normal 600 1em/1 "Font Awesome 7 Whiteboard";--fa-font-thumbprint-light:normal 300 1em/1 "Font Awesome 7 Thumbprint";--fa-font-notdog-solid:normal 900 1em/1 "Font Awesome 7 Notdog";--fa-font-notdog-duo-solid:normal 900 1em/1 "Font Awesome 7 Notdog Duo";--fa-font-etch-solid:normal 900 1em/1 "Font Awesome 7 Etch";--fa-font-jelly-regular:normal 400 1em/1 "Font Awesome 7 Jelly";--fa-font-jelly-fill-regular:normal 400 1em/1 "Font Awesome 7 Jelly Fill";--fa-font-jelly-duo-regular:normal 400 1em/1 "Font Awesome 7 Jelly Duo";--fa-font-chisel-regular:normal 400 1em/1 "Font Awesome 7 Chisel";--fa-font-utility-semibold:normal 600 1em/1 "Font Awesome 7 Utility";--fa-font-utility-duo-semibold:normal 600 1em/1 "Font Awesome 7 Utility Duo";--fa-font-utility-fill-semibold:normal 600 1em/1 "Font Awesome 7 Utility Fill"}.svg-inline--fa{box-sizing:content-box;display:var(--fa-display,inline-block);vertical-align:-.125em;height:1em;width:var(--fa-width,1.25em);overflow:visible}.svg-inline--fa.fa-2xs{vertical-align:.1em}.svg-inline--fa.fa-xs{vertical-align:0}.svg-inline--fa.fa-sm{vertical-align:-.0714286em}.svg-inline--fa.fa-lg{vertical-align:-.2em}.svg-inline--fa.fa-xl{vertical-align:-.25em}.svg-inline--fa.fa-2xl{vertical-align:-.3125em}.svg-inline--fa.fa-pull-left,.svg-inline--fa .fa-pull-start{float:inline-start;-webkit-margin-end:var(--fa-pull-margin,.3em);margin-inline-end:var(--fa-pull-margin,.3em)}.svg-inline--fa.fa-pull-right,.svg-inline--fa .fa-pull-end{float:inline-end;-webkit-margin-start:var(--fa-pull-margin,.3em);margin-inline-start:var(--fa-pull-margin,.3em)}.svg-inline--fa.fa-li{width:var(--fa-li-width,2em);inset-block-start:.25em;inset-inline-start:calc(-1 * var(--fa-li-width,2em))}.fa-layers-counter,.fa-layers-text{text-align:center;display:inline-block;position:absolute}.fa-layers{text-align:center;vertical-align:-.125em;height:1em;width:var(--fa-width,1.25em);display:inline-block;position:relative}.fa-layers .svg-inline--fa{transform-origin:50%;margin:auto;position:absolute;inset:0}.fa-layers-text{transform-origin:50%;top:50%;left:50%;transform:translate(-50%,-50%)}.fa-layers-counter{background-color:var(--fa-counter-background-color,#ff253a);border-radius:var(--fa-counter-border-radius,1em);box-sizing:border-box;color:var(--fa-inverse,#fff);line-height:var(--fa-counter-line-height,1);max-width:var(--fa-counter-max-width,5em);min-width:var(--fa-counter-min-width,1.5em);padding:var(--fa-counter-padding,.25em .5em);right:var(--fa-right,0);text-overflow:ellipsis;top:var(--fa-top,0);-webkit-transform:scale(var(--fa-counter-scale,.25));transform:scale(var(--fa-counter-scale,.25));transform-origin:100% 0;overflow:hidden}.fa-layers-bottom-right{bottom:var(--fa-bottom,0);right:var(--fa-right,0);-webkit-transform:scale(var(--fa-layers-scale,.25));transform:scale(var(--fa-layers-scale,.25));transform-origin:100% 100%;top:auto}.fa-layers-bottom-left{bottom:var(--fa-bottom,0);left:var(--fa-left,0);-webkit-transform:scale(var(--fa-layers-scale,.25));transform:scale(var(--fa-layers-scale,.25));transform-origin:0 100%;top:auto;right:auto}.fa-layers-top-right{top:var(--fa-top,0);right:var(--fa-right,0);-webkit-transform:scale(var(--fa-layers-scale,.25));transform:scale(var(--fa-layers-scale,.25));transform-origin:100% 0}.fa-layers-top-left{left:var(--fa-left,0);right:auto;top:var(--fa-top,0);-webkit-transform:scale(var(--fa-layers-scale,.25));transform:scale(var(--fa-layers-scale,.25));transform-origin:0 0}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-2xs{vertical-align:.225em;font-size:.625em;line-height:.1em}.fa-xs{vertical-align:.125em;font-size:.75em;line-height:.0833333em}.fa-sm{vertical-align:.0535715em;font-size:.875em;line-height:.0714286em}.fa-lg{vertical-align:-.075em;font-size:1.25em;line-height:.05em}.fa-xl{vertical-align:-.125em;font-size:1.5em;line-height:.0416667em}.fa-2xl{vertical-align:-.1875em;font-size:2em;line-height:.03125em}.fa-width-auto{--fa-width:auto}.fa-fw,.fa-width-fixed{--fa-width:1.25em}.fa-ul{-webkit-margin-start:var(--fa-li-margin,2.5em);-webkit-padding-start:0;margin-inline-start:var(--fa-li-margin,2.5em);padding-inline-start:0;list-style-type:none}.fa-ul>li{position:relative}.fa-li{text-align:center;width:var(--fa-li-width,2em);line-height:inherit;position:absolute;inset-inline-start:calc(-1 * var(--fa-li-width,2em))}.fa-border{border-color:var(--fa-border-color,#eee);border-radius:var(--fa-border-radius,.1em);border-style:var(--fa-border-style,solid);border-width:var(--fa-border-width,.0625em);box-sizing:var(--fa-border-box-sizing,content-box);padding:var(--fa-border-padding,.1875em .25em)}.fa-pull-left,.fa-pull-start{float:inline-start;-webkit-margin-end:var(--fa-pull-margin,.3em);margin-inline-end:var(--fa-pull-margin,.3em)}.fa-pull-right,.fa-pull-end{float:inline-end;-webkit-margin-start:var(--fa-pull-margin,.3em);margin-inline-start:var(--fa-pull-margin,.3em)}.fa-beat{animation-name:fa-beat;-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,ease-in-out);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-bounce{animation-name:fa-bounce;-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,cubic-bezier(.28, .84, .42, 1));animation-timing-function:var(--fa-animation-timing,cubic-bezier(.28, .84, .42, 1))}.fa-fade{animation-name:fa-fade;-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4, 0, .6, 1));animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4, 0, .6, 1))}.fa-beat-fade{animation-name:fa-beat-fade;-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4, 0, .6, 1));animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4, 0, .6, 1))}.fa-flip{animation-name:fa-flip;-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,ease-in-out);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-shake{animation-name:fa-shake;-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,linear);animation-timing-function:var(--fa-animation-timing,linear)}.fa-spin{animation-name:fa-spin;-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,2s);animation-duration:var(--fa-animation-duration,2s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,linear);animation-timing-function:var(--fa-animation-timing,linear)}.fa-spin-reverse{--fa-animation-direction:reverse}.fa-pulse,.fa-spin-pulse{animation-name:fa-spin;-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,steps(8));animation-timing-function:var(--fa-animation-timing,steps(8))}@media (prefers-reduced-motion:reduce){.fa-beat,.fa-bounce,.fa-fade,.fa-beat-fade,.fa-flip,.fa-pulse,.fa-shake,.fa-spin,.fa-spin-pulse{transition:none!important;animation:none!important}}@keyframes fa-beat{0%,90%{transform:scale(1)}45%{-webkit-transform:scale(var(--fa-beat-scale,1.25));transform:scale(var(--fa-beat-scale,1.25))}}@keyframes fa-bounce{0%{transform:scale(1)translateY(0)}10%{-webkit-transform:scale(var(--fa-bounce-start-scale-x,1.1), var(--fa-bounce-start-scale-y,.9)) translateY(0);transform:scale(var(--fa-bounce-start-scale-x,1.1), var(--fa-bounce-start-scale-y,.9)) translateY(0)}30%{-webkit-transform:scale(var(--fa-bounce-jump-scale-x,.9), var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em));transform:scale(var(--fa-bounce-jump-scale-x,.9), var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em))}50%{-webkit-transform:scale(var(--fa-bounce-land-scale-x,1.05), var(--fa-bounce-land-scale-y,.95)) translateY(0);transform:scale(var(--fa-bounce-land-scale-x,1.05), var(--fa-bounce-land-scale-y,.95)) translateY(0)}57%{-webkit-transform:scale(1, 1) translateY(var(--fa-bounce-rebound,-.125em));transform:scale(1, 1) translateY(var(--fa-bounce-rebound,-.125em))}64%{transform:scale(1)translateY(0)}to{transform:scale(1)translateY(0)}}@keyframes fa-fade{50%{opacity:var(--fa-fade-opacity,.4)}}@keyframes fa-beat-fade{0%,to{opacity:var(--fa-beat-fade-opacity,.4);transform:scale(1)}50%{opacity:1;-webkit-transform:scale(var(--fa-beat-fade-scale,1.125));transform:scale(var(--fa-beat-fade-scale,1.125))}}@keyframes fa-flip{50%{-webkit-transform:rotate3d(var(--fa-flip-x,0), var(--fa-flip-y,1), var(--fa-flip-z,0), var(--fa-flip-angle,-180deg));transform:rotate3d(var(--fa-flip-x,0), var(--fa-flip-y,1), var(--fa-flip-z,0), var(--fa-flip-angle,-180deg))}}@keyframes fa-shake{0%{transform:rotate(-15deg)}4%{transform:rotate(15deg)}8%,24%{transform:rotate(-18deg)}12%,28%{transform:rotate(18deg)}16%{transform:rotate(-22deg)}20%{transform:rotate(22deg)}32%{transform:rotate(-12deg)}36%{transform:rotate(12deg)}40%,to{transform:rotate(0)}}@keyframes fa-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.fa-rotate-90{transform:rotate(90deg)}.fa-rotate-180{transform:rotate(180deg)}.fa-rotate-270{transform:rotate(270deg)}.fa-flip-horizontal{transform:scaleX(-1)}.fa-flip-vertical{transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{transform:scale(-1)}.fa-rotate-by{-webkit-transform:rotate(var(--fa-rotate-angle,0));transform:rotate(var(--fa-rotate-angle,0))}.svg-inline--fa .fa-primary{fill:var(--fa-primary-color,currentColor);opacity:var(--fa-primary-opacity,1)}.svg-inline--fa .fa-secondary{fill:var(--fa-secondary-color,currentColor);opacity:var(--fa-secondary-opacity,.4)}.svg-inline--fa.fa-swap-opacity .fa-primary{opacity:var(--fa-secondary-opacity,.4)}.svg-inline--fa.fa-swap-opacity .fa-secondary{opacity:var(--fa-primary-opacity,1)}.svg-inline--fa mask .fa-primary,.svg-inline--fa mask .fa-secondary{fill:#000}.svg-inline--fa.fa-inverse{fill:var(--fa-inverse,#fff)}.fa-stack{vertical-align:middle;width:2.5em;height:2em;line-height:2em;display:inline-block;position:relative}.fa-inverse{color:var(--fa-inverse,#fff)}.svg-inline--fa.fa-stack-1x{--fa-width:1.25em;height:1em;width:var(--fa-width)}.svg-inline--fa.fa-stack-2x{--fa-width:2.5em;height:2em;width:var(--fa-width)}.fa-stack-1x,.fa-stack-2x{z-index:var(--fa-stack-z-index,auto);margin:auto;position:absolute;inset:0}pinch-zoom{touch-action:none;--scale:1;--x:0;--y:0;display:block;overflow:hidden}pinch-zoom>*{-webkit-transform:translate(var(--x), var(--y)) scale(var(--scale));transform:translate(var(--x), var(--y)) scale(var(--scale));transform-origin:0 0;will-change:transform}.panel{--__panel-background:var(--background);--__panel-backdrop-filter:var(--backdrop-filter);--_shadow:var(--shadow)}.panel .tab-switcher .tabs{background:var(--__panel-background);-webkit-backdrop-filter:var(--__panel-backdrop-filter);backdrop-filter:var(--__panel-backdrop-filter)}.panel{flex-direction:column;display:flex;position:relative}.panel .panel-heading{background-color:inherit}.panel:after,.panel{border-radius:var(--roundness)}.panel:after{content:"";z-index:5;box-shadow:var(--_shadow);pointer-events:none;position:absolute;inset:0}.panel-body{padding:var(--panel-body-padding,0);background:var(--background);-webkit-backdrop-filter:var(--__panel-backdrop-filter);backdrop-filter:var(--__panel-backdrop-filter)}.panel-body .tab-switcher .tabs{-webkit-backdrop-filter:none;backdrop-filter:none;background:0 0}.panel-body:empty:before{content:"¯\\_(ツ)_/¯";text-align:center;padding:1em;display:block}.panel-body>p{margin:0;padding:1em;line-height:1.3}.panel-heading,.panel-footer{--panel-heading-height-padding:calc(var(--panel-header-height) * .2);--__panel-heading-gap:calc(var(--panel-header-height) * .1565);--__panel-heading-height:var(--panel-header-height);--__panel-heading-height-inner:calc(var(--__panel-heading-height) - 2 * var(--panel-heading-height-padding,0));font-size:calc(var(--panelHeaderSize) / 3.2);-webkit-backdrop-filter:var(--__panel-backdrop-filter);backdrop-filter:var(--__panel-backdrop-filter);box-sizing:border-box;grid-column-gap:var(--__panel-heading-gap);padding:var(--panel-heading-height-padding);height:var(--__panel-heading-height);line-height:var(--__panel-heading-height-inner);z-index:4;background-size:cover;flex:none;grid-template-columns:minmax(50%,1fr);grid-auto-columns:auto;grid-auto-flow:column;display:grid;position:relative}.panel-heading.-flexible-height,.panel-footer.-flexible-height{--__panel-heading-height:auto}.panel-heading.-flexible-height:after,.panel-heading.-flexible-height:before,.panel-footer.-flexible-height:after,.panel-footer.-flexible-height:before{display:none}.panel-heading.-stub,.panel-heading.-stub:after,.panel-footer.-stub,.panel-footer.-stub:after{border-radius:var(--roundness)}.panel-heading.-sticky,.panel-footer.-sticky{top:var(--navbar-height);position:sticky}.panel-heading:after,.panel-heading:before,.panel-footer:after,.panel-footer:before{content:"";pointer-events:none;position:absolute;inset:0}.panel-heading .title,.panel-footer .title{margin:0;font-size:1.3em;font-weight:400}.panel-heading .alert,.panel-footer .alert{white-space:nowrap;text-overflow:ellipsis;overflow-x:hidden}.panel-heading:not(.-flexible-height)>.button-default,.panel-heading:not(.-flexible-height)>.alert,.panel-footer:not(.-flexible-height)>.button-default,.panel-footer:not(.-flexible-height)>.alert{height:var(--__panel-heading-height-inner);box-sizing:border-box;align-self:stretch;min-width:1px;min-height:0;margin:0;padding-top:0;padding-bottom:0}.panel-heading:not(.-flexible-height)>.alert,.panel-footer:not(.-flexible-height)>.alert{line-height:calc(var(--__panel-heading-height-inner) - 2px)}.panel-heading{border-radius:var(--roundness) var(--roundness) 0 0;background-image:-webkit-linear-gradient(top, var(--background), var(--background)), -webkit-linear-gradient(top, var(--__panel-background), var(--__panel-background));background-image:linear-gradient(to bottom, var(--background), var(--background)), linear-gradient(to bottom, var(--__panel-background), var(--__panel-background));text-overflow:ellipsis;border-width:0 0 1px;align-items:start;overflow:hidden}.panel-heading .title{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.panel-heading{--_shadow:var(--shadow)}.panel-heading:after{background-color:var(--background);z-index:-2;border-radius:var(--roundness) var(--roundness) 0 0;box-shadow:var(--_shadow)}.panel-heading:not(.-flexible-height)>.button-default{flex-shrink:0}.panel-heading .rightside-button{text-align:center;width:var(--__panel-heading-height);height:var(--__panel-heading-height);margin:calc(-1 * var(--panel-heading-height-padding)) 0;margin-right:calc(-1 * var(--__panel-heading-gap));align-self:stretch}.panel-heading .rightside-button>button{box-sizing:border-box;padding:calc(1 * var(--panel-heading-height-padding)) 0;text-align:center;width:100%;height:100%}.panel-heading .rightside-button>button svg{font-size:1.2em}.panel-heading .rightside-icon{text-align:center;width:var(--__panel-heading-height);margin-right:calc(-1 * var(--__panel-heading-gap));align-self:stretch}.panel-heading .rightside-icon svg{font-size:1.2em}.panel-footer{border-style:solid;border-width:1px 0 0;border-color:var(--border);background-color:var(--__panel-background);border-top-left-radius:0;border-top-right-radius:0;align-items:center}:root{--status-margin:.75em;--post-line-height:1.4;--ZI_media_modal:9000;--ZI_modals_popovers:8500;--ZI_modals:8000;--ZI_navbar_popovers:7500;--ZI_navbar:7000;--ZI_popovers:6000;--background:var(--bg)}html{font-size:var(--textSize,1rem);--navbar-height:var(--navbarSize,3.5rem);--emoji-size:var(--emojiSize,32px);--panel-header-height:var(--panelHeaderSize,3.2rem)}body{font-family:sans-serif;font-family:var(--font);color:var(--text);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;overscroll-behavior-y:none;margin:0;padding:0;overflow:clip scroll}body.hidden{display:none}@media (any-pointer:fine){*{scrollbar-color:var(--icon) transparent}::-webkit-scrollbar{background:0 0}::-webkit-scrollbar-corner{background:0 0}::-webkit-resizer{background-image:-webkit-linear-gradient(315deg, transparent calc(50% - 1px), var(--textFaint) 50%, transparent calc(50% + 1px), transparent calc(75% - 1px), var(--textFaint) 75%, transparent calc(75% + 1px));background-image:linear-gradient(135deg, transparent calc(50% - 1px), var(--textFaint) 50%, transparent calc(50% + 1px), transparent calc(75% - 1px), var(--textFaint) 75%, transparent calc(75% + 1px));background-color:#0000!important}::-webkit-scrollbar-button{box-shadow:var(--shadow);border-radius:var(--roundness)}::-webkit-scrollbar-thumb{box-shadow:var(--shadow);border-radius:var(--roundness)}::-webkit-scrollbar-button{--___bgPadding:2px;color:var(--text);background-repeat:no-repeat,no-repeat}::-webkit-scrollbar-button:horizontal{background-size:50% calc(50% - var(--___bgPadding)), 50% calc(50% - var(--___bgPadding))}::-webkit-scrollbar-button:horizontal:increment{background-image:-webkit-linear-gradient(45deg, var(--text) 50%, transparent 51%), -webkit-linear-gradient(135deg, transparent 50%, var(--text) 51%);background-image:linear-gradient(45deg, var(--text) 50%, transparent 51%), linear-gradient(-45deg, transparent 50%, var(--text) 51%);background-position:top var(--___bgPadding) left 50%, right 50% bottom var(--___bgPadding)}::-webkit-scrollbar-button:horizontal:decrement{background-image:-webkit-linear-gradient(45deg, transparent 50%, var(--text) calc(50% + 1px)), -webkit-linear-gradient(135deg, var(--text) 50%, transparent 51%);background-image:linear-gradient(45deg, transparent 50%, var(--text) calc(50% + 1px)), linear-gradient(-45deg, var(--text) 50%, transparent 51%);background-position:bottom var(--___bgPadding) right 50%, left 50% top var(--___bgPadding)}::-webkit-scrollbar-button:vertical{background-size:calc(50% - var(--___bgPadding)) 50%, calc(50% - var(--___bgPadding)) 50%}::-webkit-scrollbar-button:vertical:increment{background-image:-webkit-linear-gradient(135deg, transparent 50%, var(--text) 51%), -webkit-linear-gradient(45deg, transparent 50%, var(--text) 51%);background-image:linear-gradient(-45deg, transparent 50%, var(--text) 51%), linear-gradient(45deg, transparent 50%, var(--text) 51%);background-position:right var(--___bgPadding) top 50%, left var(--___bgPadding) top 50%}::-webkit-scrollbar-button:vertical:decrement{background-image:-webkit-linear-gradient(135deg, var(--text) 50%, transparent 51%), -webkit-linear-gradient(45deg, var(--text) 50%, transparent 51%);background-image:linear-gradient(-45deg, var(--text) 50%, transparent 51%), linear-gradient(45deg, var(--text) 50%, transparent 51%);background-position:left var(--___bgPadding) top 50%, right var(--___bgPadding) top 50%}html{scrollbar-color:var(--icon) var(--wallpaper);background:var(--wallpaper)}}a{color:var(--link);text-decoration:none}h4{margin:0}code{background:var(--background);border:1px solid var(--border);border-radius:var(--roundness);padding:0 .2em}code pre,pre code{padding:.2em;display:block;overflow-x:auto}code.pre{white-space:pre;display:block}.iconLetter{text-align:center;font-weight:1000;display:inline-block}i[class*=icon-],.svg-inline--fa,.iconLetter{color:var(--icon)}nav{z-index:var(--ZI_navbar);box-shadow:var(--shadow);box-sizing:border-box;height:var(--navbar-height);font-size:calc(var(--navbar-height) / 3.5);position:fixed}#sidebar{grid-area:sidebar}#modal{z-index:var(--ZI_modals);position:absolute}.column.-scrollable{top:var(--navbar-height);position:sticky}#main-scroller{grid-area:content;position:relative}#notifs-column{grid-area:notifs}.app-bg-wrapper{height:100%;top:var(--navbar-height);z-index:-1000;background-repeat:no-repeat;background-size:cover;background-color:var(--wallpaper);background-image:var(--body-background-image);background-position:50%;transition:background-image 1s;position:fixed;left:0;right:-20px}.underlay{pointer-events:none;background-color:var(--underlay);z-index:-1000;grid-area:1/1/1/span 3}.app-layout{--miniColumn:25rem;--maxiColumn:45rem;--columnGap:1rem;--effectiveSidebarColumnWidth:minmax(var(--miniColumn), var(--sidebarColumnWidth,var(--miniColumn)));--effectiveNotifsColumnWidth:minmax(var(--miniColumn), var(--notifsColumnWidth,var(--miniColumn)));--effectiveContentColumnWidth:minmax(var(--miniColumn), var(--contentColumnWidth,var(--maxiColumn)));grid-template-columns:var(--effectiveSidebarColumnWidth) var(--effectiveContentColumnWidth);box-sizing:border-box;flex-wrap:wrap;grid-template-rows:1fr;grid-template-areas:"sidebar content";place-content:flex-start center;min-height:100vh;margin:0 auto;display:grid;position:relative;overflow-x:clip}.app-layout .column{--___columnMargin:var(--columnGap);box-sizing:border-box;margin:0 calc(var(--___columnMargin) / 2);padding:calc(var(--___columnMargin)) 0;row-gap:var(--___columnMargin);grid-row:1/1;grid-template-columns:100%;align-content:start;display:grid}.app-layout .column:not(.-scrollable){margin-top:var(--navbar-height)}.app-layout .column:hover{z-index:2}.app-layout .column.-full-height{margin-bottom:0;padding-top:0;padding-bottom:0}.app-layout .column.-scrollable{--___paddingIncrease:calc(var(--columnGap) / 2);top:var(--navbar-height);max-height:calc(100vh - var(--navbar-height));margin-left:calc(var(--___paddingIncrease) * -1);padding-left:calc(var(--___paddingIncrease) + var(--___columnMargin) / 2);position:sticky;overflow:hidden auto}@supports (scrollbar-width:none) or (-webkit-text-fill-color:initial){.app-layout .column.-scrollable:not(.-show-scrollbar){scrollbar-width:none;margin-right:calc(var(--___paddingIncrease) * -1);padding-right:calc(var(--___paddingIncrease) + var(--___columnMargin) / 2)}.app-layout .column.-scrollable:not(.-show-scrollbar)::-webkit-scrollbar{width:0;display:block}}.app-layout .column.-scrollable .panel-heading.-sticky{top:calc(var(--columnGap) / -1)}.app-layout.-has-new-post-button .column{padding-bottom:10rem}.app-layout.-no-sticky-headers .column .panel-heading.-sticky{position:relative;top:0}.app-layout .column-inner{box-sizing:border-box;grid-template-columns:100%;align-content:start;row-gap:1em;display:grid}.app-layout.-reverse:not(.-wide,.-mobile){grid-template-columns:var(--effectiveContentColumnWidth) var(--effectiveSidebarColumnWidth);grid-template-areas:"content sidebar"}.app-layout.-wide{grid-template-columns:var(--effectiveSidebarColumnWidth) var(--effectiveContentColumnWidth) var(--effectiveNotifsColumnWidth);grid-template-areas:"sidebar content notifs"}.app-layout.-wide.-reverse{grid-template-columns:var(--effectiveNotifsColumnWidth) var(--effectiveContentColumnWidth) var(--effectiveSidebarColumnWidth);grid-template-areas:"notifs content sidebar"}.app-layout.-mobile{grid-template-columns:100vw;grid-template-areas:"content";padding:0}.app-layout.-mobile .column{margin:var(--navbar-height) 0 0 0;padding-top:0}.app-layout.-mobile .panel-heading,.app-layout.-mobile .panel-heading:after,.app-layout.-mobile .panel-heading:before,.app-layout.-mobile .panel,.app-layout.-mobile .panel:after{border-top-left-radius:0;border-top-right-radius:0}.app-layout.-mobile #sidebar,.app-layout.-mobile #notifs-column,.app-layout.-normal #notifs-column{display:none}.text-center{text-align:center}.button-default{-webkit-user-select:none;user-select:none;color:var(--text);cursor:pointer;background-color:var(--background);box-shadow:var(--shadow);font-family:sans-serif;font-size:1em;font-family:var(--font);border:none}.button-default.-transparent{-webkit-backdrop-filter:blur(.125em)contrast(60%);backdrop-filter:blur(.125em)contrast(60%)}.button-default::-moz-focus-inner{border:none}.button-default:disabled{cursor:not-allowed}.button-default:active{transform:translate(1px,1px)}.menu-item{line-height:var(--__line-height);cursor:pointer;font-family:inherit;font-size:100%;font-weight:400}.menu-item a,.menu-item button:not(.button-default){color:var(--text);text-align:initial;font-family:inherit;font-size:100%;line-height:unset;background:0 0;border:none;outline:none;padding:0;display:inline}.menu-item.disabled{cursor:not-allowed}.menu-item,.list-item{box-sizing:border-box;text-align:initial;color:inherit;clear:both;white-space:nowrap;width:100%;padding:var(--__vertical-gap) var(--__horizontal-gap);--__line-height:1.5em;--__horizontal-gap:.75em;--__vertical-gap:.5em;background:0 0;border:none;outline:none;display:block;position:relative}.button-unstyled{text-align:initial;box-shadow:var(--shadow);font-family:inherit;font-size:100%;line-height:unset;cursor:pointer;box-sizing:content-box;color:inherit;background-color:#0000;border:none;outline:none;padding:0;display:inline}.button-unstyled.-link{color:var(--link)!important}label.-disabled{color:var(--textFaint)}input,textarea{border:none;outline:none;display:inline-block}.input.unstyled{box-shadow:none;height:unset;border-radius:0;background:0 0!important}.input::placeholder{color:var(--textFaint)}.input{--_padding:.5em;background-color:var(--background);color:var(--text);box-shadow:var(--shadow);font-family:var(--font);box-sizing:border-box;-webkit-hyphens:none;hyphens:none;padding:0 var(--_padding);border:none;margin:0;font-size:1em;line-height:2;display:inline-block;position:relative}.input:disabled,.input[disabled=disabled],.input.disabled{cursor:not-allowed;color:var(--textFaint);background-color:#0000!important}.input[type=range]{box-shadow:none;background:0 0;border:none;flex:1;margin:0}.input[type=radio]{display:none}.input[type=radio]:checked+label:before{box-shadow:var(--shadow);background-color:var(--background);color:var(--text)}.input[type=radio]:disabled,.input[type=radio]:disabled+label,.input[type=radio]:disabled+label:before{opacity:.5}.input[type=radio]:disabled{background-color:var(--background)}.input[type=radio]+label:before{content:"•";background-color:var(--background);width:1.1em;height:1.1em;box-shadow:var(--shadow);vertical-align:top;text-align:center;box-sizing:border-box;color:#0000;border-radius:100%;flex-shrink:0;margin-right:.5em;font-size:1.1em;line-height:1.1;transition:box-shadow .2s;display:inline-block;overflow:hidden}.input[type=checkbox]:checked+label:before{color:var(--text);background-color:var(--background);box-shadow:var(--shadow)}.input[type=checkbox]:disabled,.input[type=checkbox]:disabled+label,.input[type=checkbox]:disabled+label:before{opacity:.5}.input[type=checkbox]+label:before{content:"✓";border-radius:var(--roundness);width:1.1em;height:1.1em;box-shadow:var(--shadow);vertical-align:top;text-align:center;box-sizing:border-box;color:#0000;flex-shrink:0;margin-right:.5em;font-size:1.1em;line-height:1.1;transition:color .2s;display:inline-block;overflow:hidden}.input.resize-height{resize:vertical}.input,.button-default{--_roundness-left:var(--roundness);--_roundness-right:var(--roundness);border-top-left-radius:var(--_roundness-left);border-bottom-left-radius:var(--_roundness-left);border-top-right-radius:var(--_roundness-right);border-bottom-right-radius:var(--_roundness-right)}textarea.input{padding:var(--_padding);line-height:var(--post-line-height)}option{color:var(--text);background-color:var(--background)}.hide-number-spinner{appearance:textfield}.hide-number-spinner[type=number]::-webkit-inner-spin-button{opacity:0;display:none}.hide-number-spinner[type=number]::-webkit-outer-spin-button{opacity:0;display:none}.cards-list{grid-gap:.5em;grid-template-columns:repeat(auto-fit,minmax(20em,1fr));grid-auto-flow:dense;list-style:none;display:grid}.cards-list li{border:1px solid var(--border);border-radius:var(--roundness);margin:.25em;padding:.5em}.btn-group{vertical-align:middle;display:inline-flex;position:relative}.btn-group>*,.btn-group>* .button-default{--_roundness-left:0;--_roundness-right:0;position:relative}.btn-group>:first-child,.btn-group>:first-child .button-default{--_roundness-left:var(--roundness)}.btn-group>:last-child,.btn-group>:last-child .button-default{--_roundness-right:var(--roundness)}.fa{color:gray}.mobile-shown{display:none}.badge{box-sizing:border-box;vertical-align:middle;text-align:center;white-space:nowrap;text-overflow:ellipsis;border-radius:99px;min-width:1.7em;max-width:10em;height:1.3em;padding:.15em;font-size:.9em;font-style:normal;font-weight:400;line-height:1;display:inline-block;overflow:hidden}.badge.-dot,.badge.-counter{margin:0;position:absolute}.badge.-dot{min-width:.6em;max-width:.6em;min-height:.6em;max-height:.6em;margin:0;padding:0;line-height:0;top:calc(50% - 1em);left:calc(50% + .5em)}.badge.-counter{border-radius:var(--roundness);text-align:right;min-width:0;margin-top:-1em;margin-left:.7em;padding:.2em;font-size:.75em;line-height:1;top:calc(50% - .4em);left:calc(50% - .5em)}.alert{border-radius:var(--roundness);border:1px solid var(--border);margin:0 .35em;padding:0 .25em}.alert.-dismissible{align-items:baseline;margin:0;padding-left:.5em;line-height:2;display:flex}.alert.-dismissible span{flex:1 0 auto;display:block}.faint{--text:var(--textFaint);--link:var(--linkFaint);color:var(--text)}.notice-dismissible{align-items:baseline;padding:.75em 1em;line-height:1.5;display:flex}.notice-dismissible p,.notice-dismissible span{flex:auto;margin:0;display:block}.notice-dismissible .dismiss{color:inherit}.fa-scale-110.svg-inline--fa,.fa-scale-110.iconLetter{font-size:1.1em}.fa-scale-110.svg-inline--fa{vertical-align:-.15em}.fa-old-padding.iconLetter,.fa-old-padding.svg-inline--fa,.fa-old-padding-layer{padding:0 .3em}.veryfaint{opacity:.25}.timeago{--link:var(--text);--linkFaint:var(--textFaint)}.login-hint{text-align:center}@media (width>=801px){.login-hint{display:none}}.login-hint a{width:100%;padding:1em 0;display:inline-block}.btn.button-default{min-height:2em}.new-status-notification{z-index:1;flex:1;font-size:1.1em;position:relative}@media (width<=800px){.mobile-hidden{display:none}}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(359deg)}}@keyframes shakeError{0%{transform:translate(0)}15%{transform:translate(.375rem)}30%{transform:translate(-.375rem)}45%{transform:translate(.375rem)}60%{transform:translate(-.375rem)}75%{transform:translate(.375rem)}90%{transform:translate(-.375rem)}to{transform:translate(0)}}.fade-enter-active,.fade-leave-active{transition:opacity .3s}.fade-enter-from,.fade-leave-active{opacity:0}.visible-for-screenreader-only{visibility:visible;clip:rect(0 0 0 0);width:1px;height:1px;margin:-1px;padding:0;display:block;position:absolute;overflow:hidden}::selection{color:var(--selectionText);background-color:var(--selectionBackground)}#splash{pointer-events:none}#splash #status.css-ok:before{content:"CSS OK";display:inline-block}#splash #status .initial-text{display:none}#splash #throbber{transform-origin:bottom;animation-name:bounce;animation-duration:3s;animation-iteration-count:infinite;animation-direction:normal}#splash #throbber.dead{animation-name:dead;animation-duration:.5s;animation-iteration-count:1;transform:rotateX(90deg)rotateY(0)rotate(-45deg)}@keyframes dead{0%{transform:rotateX(0)rotateY(0)rotate(0)}5%{transform:rotateX(0)rotateY(0)rotate(1deg)}10%{transform:rotateX(0)rotateY(0)rotate(-2deg)}15%{transform:rotateX(0)rotateY(0)rotate(3deg)}20%{transform:rotateX(0)rotateY(0)rotate(0)}25%{transform:rotateX(0)rotateY(0)rotate(0)}30%{transform:rotateX(10deg)rotateY(0)rotate(0)}35%{transform:rotateX(-10deg)rotateY(0)rotate(0)}40%{transform:rotateX(10deg)rotateY(0)rotate(0)}45%{transform:rotateX(-10deg)rotateY(0)rotate(0)}50%{transform:rotateX(10deg)rotateY(0)rotate(0)}to{transition-timing-function:cubic-bezier(.755,.05,.855,.06);transform:rotateX(90deg)rotateY(0)rotate(-45deg)}}@keyframes bounce{0%{animation-timing-function:ease-out;translate:0;scale:1}10%{translate:0;scale:1.2 .8;-webkit-transform:rotateZ(var(--defaultZ));transform:rotateZ(var(--defaultZ));animation-timing-function:ease-out}30%{translate:0 -40%;scale:.9 1.1;-webkit-transform:rotateZ(var(--defaultZ));transform:rotateZ(var(--defaultZ));animation-timing-function:ease-in}40%{translate:0 -50%;scale:1.1 .9;-webkit-transform:rotateZ(var(--defaultZ));transform:rotateZ(var(--defaultZ));animation-timing-function:ease-in}45%{translate:0 -45%;scale:.9 1.1;-webkit-transform:rotateZ(var(--defaultZ));transform:rotateZ(var(--defaultZ));animation-timing-function:ease-in}50%{animation-timing-function:ease-in;translate:0 -40%;scale:1.05 .95}55%{translate:0 -35%;scale:.985 1.025;-webkit-transform:rotateZ(var(--defaultZ));transform:rotateZ(var(--defaultZ));animation-timing-function:ease-in}60%{translate:0 -30%;scale:1.0125 .9985;-webkit-transform:rotateZ(var(--defaultZ));transform:rotateZ(var(--defaultZ));animation-timing-function:ease-in}80%{translate:0 -10%;scale:1.0063 .9938;-webkit-transform:rotateZ(var(--defaultZ));transform:rotateZ(var(--defaultZ));animation-timing-function:ease-in-out}90%{translate:0;scale:1.2 .8;-webkit-transform:rotateZ(var(--defaultZ));transform:rotateZ(var(--defaultZ));animation-timing-function:ease-out}to{translate:0;scale:1;-webkit-transform:rotateZ(var(--defaultZ));transform:rotateZ(var(--defaultZ));animation-timing-function:ease-out}}@property --shadow{syntax:"*";inherits:false}.thread-tree-replies{margin-left:var(--status-margin);border-left:2px solid var(--border)}.thread-tree-replies-hidden{padding:var(--status-margin);flex-direction:column;align-items:stretch;display:flex}.Conversation{z-index:1}.Conversation.-hidden{background:var(--__panel-background);-webkit-backdrop-filter:var(--__panel-backdrop-filter);backdrop-filter:var(--__panel-backdrop-filter)}.Conversation .conversation-dive-to-top-level-box{padding:var(--status-margin);border-bottom:1px solid var(--border);border-radius:0;flex-direction:column;align-items:stretch;display:flex}.Conversation .thread-ancestors{margin-left:var(--status-margin);border-left:2px solid var(--border)}.Conversation .thread-ancestor.-faded .RichContent{--text:var(--textFaint)!important;--link:var(--linkFaint)!important;--funtextGreentext:var(--funtextGreentextFaint)!important;--funtextCyantext:var(--funtextCyantextFaint)!important}.Conversation .thread-ancestor-dive-box{padding-left:var(--status-margin);border-bottom:1px solid var(--border);border-radius:0}.Conversation .thread-ancestor-dive-box,.Conversation .thread-ancestor-dive-box-inner{flex-direction:column;align-items:stretch;display:flex}.Conversation .thread-ancestor-dive-box-inner{padding:var(--status-margin)}.Conversation .conversation-status{border-bottom:1px solid var(--border);border-radius:0}.Conversation .thread-ancestor-has-other-replies .conversation-status,.Conversation:last-child:not(.-expanded) .conversation-status,.Conversation.-expanded .conversation-status:last-child,.Conversation .thread-ancestor:last-child .conversation-status,.Conversation .thread-ancestor:last-child .thread-ancestor-dive-box,.Conversation.-expanded .thread-tree .conversation-status{border-bottom:none}.Conversation .thread-ancestors+.thread-tree>.conversation-status{border-top:1px solid var(--border)}.Conversation.status-fadein.-expanded .thread-body{border-left:4px solid var(--cRed);border-radius:var(--roundness);border-bottom:1px solid var(--border);border-top-left-radius:0;border-top-right-radius:0}.Conversation.-expanded.status-fadein{--___margin:calc(var(--status-margin) / 2);background:var(--background);margin:var(--___margin)}.Conversation.-expanded.status-fadein:before{z-index:-1;content:"";inset:calc(var(--___margin) * -1);background:var(--background);-webkit-backdrop-filter:var(--__panel-backdrop-filter);backdrop-filter:var(--__panel-backdrop-filter);display:block;position:absolute}.scroll-to-top{display:none}.-scrolled .scroll-to-top{display:inline-block}.timeline-menu-popover{border-top-left-radius:0;border-top-right-radius:0;min-width:24rem;max-width:100vw;margin-top:.6rem;font-size:1rem}.timeline-menu-popover ul{margin:0;padding:0;list-style:none}.TimelineMenu{min-width:0;margin-right:auto}.TimelineMenu .popover-trigger-button{vertical-align:bottom}.TimelineMenu .panel:after{border-top-left-radius:0;border-top-right-radius:0}.TimelineMenu .timeline-menu-title{cursor:pointer;-webkit-user-select:none;user-select:none;width:100%;margin:0;display:flex}.TimelineMenu .timeline-menu-title .timeline-menu-name{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.TimelineMenu .timeline-menu-title svg{margin-left:.6em;transition:transform .1s}.TimelineMenu .timeline-menu-title .click-blocker{cursor:default;flex-grow:1}.TimelineMenu.open .timeline-menu-title svg{transform:rotate(180deg)}.TimelineMenu .panel{box-shadow:var(--popoverShadow)}.Timeline .timeline-body{-webkit-backdrop-filter:none;backdrop-filter:none;background:0 0}.Timeline .alert-badge{text-align:right;border-radius:var(--roundness);margin-top:-1em;margin-left:.7em;padding:.2em;font-size:.75em;line-height:1;position:absolute;top:calc(50% - .4em);left:calc(50% - .5em)}.Timeline .loadmore-button{position:relative}.Timeline.-blocked{cursor:progress}.Timeline .conversation-heading{top:calc(var(--__panel-heading-height) * var(--currentPanelStack,1) + var(--navbar-height));z-index:2}.Timeline.-embedded .timeline-heading{text-align:center;padding:0 .5em;line-height:2.75em}.Timeline.-embedded .timeline-heading:empty:before{content:initial}.Timeline.-embedded .timeline-heading .button-default,.Timeline.-embedded .timeline-heading .alert{width:100%;line-height:2em} diff --git a/priv/static/static/css/main.CF7uYwoC.css b/priv/static/static/css/main.CF7uYwoC.css deleted file mode 100644 index 092237dd7..000000000 --- a/priv/static/static/css/main.CF7uYwoC.css +++ /dev/null @@ -1 +0,0 @@ -@charset "UTF-8";.modal-view{z-index:var(--ZI_modals);position:fixed;top:0;right:0;bottom:0;left:0;display:-webkit-box;display:flex;-webkit-box-pack:center;justify-content:center;-webkit-box-align:center;align-items:center;overflow:auto;pointer-events:none;-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-name:modal-background-fadein;animation-name:modal-background-fadein;opacity:0}.modal-view>*{pointer-events:initial}.modal-view.modal-background{pointer-events:initial;background-color:#00000080}.modal-view.open{opacity:1}@-webkit-keyframes modal-background-fadein{0%{background-color:#0000}to{background-color:#00000080}}@keyframes modal-background-fadein{0%{background-color:#0000}to{background-color:#00000080}}.vue-recycle-scroller{position:relative}.vue-recycle-scroller.direction-vertical:not(.page-mode){overflow-y:auto}.vue-recycle-scroller.direction-horizontal:not(.page-mode){overflow-x:auto}.vue-recycle-scroller.direction-horizontal{display:-webkit-box;display:-ms-flexbox;display:flex}.vue-recycle-scroller__slot{-webkit-box-flex:1;-ms-flex:auto 0 0px;flex:auto 0 0}.vue-recycle-scroller__item-wrapper{-webkit-box-flex:1;-ms-flex:1;flex:1;-webkit-box-sizing:border-box;box-sizing:border-box;overflow:hidden;position:relative}.vue-recycle-scroller.ready .vue-recycle-scroller__item-view{position:absolute;top:0;left:0;will-change:transform}.vue-recycle-scroller.direction-vertical .vue-recycle-scroller__item-wrapper{width:100%}.vue-recycle-scroller.direction-horizontal .vue-recycle-scroller__item-wrapper{height:100%}.vue-recycle-scroller.ready.direction-vertical .vue-recycle-scroller__item-view{width:100%}.vue-recycle-scroller.ready.direction-horizontal .vue-recycle-scroller__item-view{height:100%}.resize-observer[data-v-b329ee4c]{position:absolute;top:0;left:0;z-index:-1;width:100%;height:100%;border:none;background-color:transparent;pointer-events:none;display:block;overflow:hidden;opacity:0}.resize-observer[data-v-b329ee4c] object{display:block;position:absolute;top:0;left:0;height:100%;width:100%;overflow:hidden;pointer-events:none;z-index:-1}.login-panel .login-form{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;padding:.6em}.login-panel .btn{min-height:2em;width:10em}.login-panel .register{-webkit-box-flex:1;flex:1 1}.login-panel .login-bottom{margin-top:1em;display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-direction:row;-webkit-box-align:center;align-items:center;-webkit-box-pack:justify;justify-content:space-between}.login-panel .form-group{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;padding:.3em .5em .6em;line-height:24px}.login-panel .login-error{display:-webkit-box;display:flex;line-height:2;margin:.5em;-webkit-animation-name:shakeError;animation-name:shakeError;-webkit-animation-duration:.4s;animation-duration:.4s;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}.login-panel .error-message{-webkit-box-flex:1;flex:1}.media-upload .hidden-input-file{display:none}label.media-upload{cursor:pointer}.ScopeSelector{display:inline-block}.ScopeSelector .scope{display:inline-block;min-width:1.3em;min-height:1.3em;text-align:center;padding:.5em .25em}.checkbox{position:relative;display:inline-block;min-height:1.2em}.checkbox-indicator,.checkbox .label{vertical-align:middle}.checkbox>.checkbox-indicator{padding:0;margin:0;position:relative;line-height:inherit;display:inline-block;width:1.2em;height:1.2em;box-shadow:none;--_shadow: var(--shadow)}.checkbox-indicator:before{position:absolute;top:0;right:0;bottom:0;left:0;display:block;content:"✓";-webkit-transition:color .2s;transition:color .2s;width:1.1em;height:1.1em;border-radius:var(--roundness);box-shadow:var(--_shadow);background-color:var(--background);vertical-align:top;text-align:center;line-height:1.1em;font-size:1.1em;color:transparent;overflow:hidden;box-sizing:border-box}.checkbox.-radio .checkbox-indicator,.checkbox.-radio .checkbox-indicator:before{border-radius:9999px}.checkbox.-radio .checkbox-indicator:before{content:"•"}.checkbox .disabled .checkbox-indicator:before{background-color:transparent}.checkbox input[type=checkbox]:checked+.checkbox-indicator:before{color:var(--text)}.checkbox input[type=checkbox]:indeterminate+.checkbox-indicator:before{content:"–";color:var(--text)}.checkbox.indeterminate-fix input[type=checkbox]+.checkbox-indicator:before{content:"–"}.checkbox>.label.-after{margin-left:.5em}.checkbox>.label.-before{margin-right:.5em}.popover-trigger-button{display:inline-block}.popover{z-index:var(--ZI_popover_override, var(--ZI_popovers));position:fixed;min-width:0;max-width:calc(100vw - 20px);--_shadow: var(--shadow)}.popover-default{border-radius:var(--roundness);border-color:var(--border);border-style:solid;border-width:1px;background-color:var(--background)}.popover-default:after{content:"";position:absolute;top:-1px;right:-1px;bottom:-1px;left:-1px;box-shadow:var(--_shadow);pointer-events:none}.dropdown-menu{display:block;padding:0;font-size:1em;text-align:left;list-style:none;max-width:100vw;z-index:var(--ZI_popover_override, var(--ZI_popovers));white-space:nowrap;background-color:var(--background)}.dropdown-menu .dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid var(--border)}.dropdown-menu .dropdown-item{padding:0;display:grid;grid-template-columns:1fr;grid-auto-flow:column;grid-auto-columns:auto;grid-gap:.5em}.dropdown-menu .dropdown-item .popover-wrapper{box-sizing:border-box;display:grid;grid-template-columns:1fr}.dropdown-menu .dropdown-item .extra-button{border-left:1px solid;border-image-source:linear-gradient(to bottom,transparent 0%,var(--icon) var(--__horizontal-gap) calc(100% - var(--__horizontal-gap)),transparent 100%);border-image-slice:1;padding-left:calc(var(--__horizontal-gap) - 1px);padding-right:var(--__horizontal-gap);padding-top:var(--__horizontal-gap);padding-bottom:var(--__horizontal-gap);max-width:-webkit-fit-content;max-width:fit-content}.dropdown-menu .dropdown-item .main-button{width:100%;padding:var(--__horizontal-gap) var(--__horizontal-gap);grid-gap:var(--__horizontal-gap);grid-template-columns:1fr var(--__line-height);grid-auto-flow:column;grid-auto-columns:auto}.dropdown-menu .dropdown-item .main-button .menu-checkbox{display:inline-block;vertical-align:middle;min-width:calc(var(--__line-height) + 1px);max-width:calc(var(--__line-height) + 1px);min-height:calc(var(--__line-height) + 1px);max-height:calc(var(--__line-height) + 1px);line-height:var(--__line-height);text-align:center;border-radius:0;box-shadow:var(--shadow);margin-right:var(--__horizontal-gap)}.dropdown-menu .dropdown-item .main-button .menu-checkbox.menu-checkbox-checked:after{font-size:1.25em;content:"✓"}.dropdown-menu .dropdown-item .main-button .menu-checkbox.-radio{border-radius:9999px}.dropdown-menu .dropdown-item .main-button .menu-checkbox.-radio.menu-checkbox-checked:after{font-size:2em;content:"•"}.dropdown-menu .dropdown-item .main-button,.dropdown-menu .dropdown-item .extra-button{display:grid;box-sizing:border-box;-webkit-box-align:center;align-items:center}.dropdown-menu .dropdown-item .main-button.disabled,.dropdown-menu .dropdown-item .extra-button.disabled{cursor:not-allowed}.dropdown-menu .dropdown-item .main-button:not(.disabled),.dropdown-menu .dropdown-item .extra-button:not(.disabled){cursor:pointer}.dropdown-menu .dropdown-item.-icon .main-button{grid-template-columns:var(--__line-height) 1fr}.dropdown-menu .dropdown-item.-icon-space .main-button{padding-left:calc(var(--__line-height) + var(--__horizontal-gap) * 2)}.dropdown-menu .dropdown-item.-icon-double .main-button{grid-template-columns:var(--__line-height) var(--__line-height) 1fr}.still-image{position:relative;line-height:0;overflow:hidden;display:-webkit-inline-box;display:inline-flex;-webkit-box-align:center;align-items:center}.still-image canvas{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;object-fit:contain;visibility:var(--_still-image-canvas-visibility, visible)}.still-image img{width:100%;height:100%;object-fit:contain}.still-image.animated:before{zoom:var(--_still_image-label-scale, 1);content:"gif";position:absolute;line-height:1;font-size:.7em;top:.5em;left:.5em;background:#7f7f7f80;color:#fff;display:block;padding:2px 4px;border-radius:var(--roundness);z-index:2;visibility:var(--_still-image-label-visibility, visible)}.still-image.animated:hover canvas{display:none}.still-image.animated:hover:before{visibility:var(--_still-image-label-visibility, hidden)}.still-image.animated img{visibility:var(--_still-image-img-visibility, hidden)}.still-image.animated:hover img{visibility:visible}.emoji-picker{--__emoji-picker-header: 2.2em;width:25em;max-width:calc(100vw - 20px);display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column}.emoji-picker-header-image{display:-webkit-inline-box;display:inline-flex;-webkit-box-pack:center;justify-content:center;-webkit-box-align:center;align-items:center;width:var(--__emoji-picker-header);max-width:var(--__emoji-picker-header);height:var(--__emoji-picker-header);max-height:var(--__emoji-picker-header)}.emoji-picker-header-image .still-image{width:var(--__emoji-picker-header);max-width:var(--__emoji-picker-header);height:var(--__emoji-picker-header);max-height:var(--__emoji-picker-header);object-fit:contain;--_still_image-label-scale: .5}.emoji-picker .keep-open,.emoji-picker .too-many-emoji,.emoji-picker .hide-custom-emoji{padding:.5em;line-height:normal}.emoji-picker .hide-custom-emoji{padding-top:0}.emoji-picker .too-many-emoji{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column}.emoji-picker .keep-open-label{padding:0 .5em;display:-webkit-box;display:flex}.emoji-picker .heading{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;padding:.7em .5em 0}.emoji-picker .content{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;-webkit-box-flex:1;flex:1 1 auto;min-height:0}.emoji-picker .emoji-tabs{-webkit-box-flex:1;flex-grow:1;display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row nowrap;overflow:auto hidden}.emoji-picker .additional-tabs{display:-webkit-box;display:flex;border-left:1px solid;border-left-color:var(--border);padding-left:.5em;-webkit-box-flex:0;flex:0 0 auto}.emoji-picker .additional-tabs,.emoji-picker .emoji-tabs{flex-basis:auto;display:-webkit-box;display:flex;align-content:center;scrollbar-width:thin}.emoji-picker .additional-tabs-item,.emoji-picker .emoji-tabs-item{padding:0 .5em;cursor:pointer;width:var(--__emoji-picker-header);max-width:var(--__emoji-picker-header);height:var(--__emoji-picker-header);max-height:var(--__emoji-picker-header);display:-webkit-box;display:flex;-webkit-box-align:center;align-items:center}.emoji-picker .additional-tabs-item .svg-inline--fa,.emoji-picker .emoji-tabs-item .svg-inline--fa{font-size:1.85em}.emoji-picker .additional-tabs-item.disabled,.emoji-picker .emoji-tabs-item.disabled{opacity:.5;pointer-events:none}.emoji-picker .additional-tabs-item.toggled,.emoji-picker .emoji-tabs-item.toggled{border-bottom:.2em solid}.emoji-picker .sticker-picker{-webkit-box-flex:1;flex:1 1 auto}.emoji-picker .stickers-content,.emoji-picker .emoji-content{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;-webkit-box-flex:1;flex:1 1 auto;min-height:0}.emoji-picker .stickers-content.hidden,.emoji-picker .emoji-content.hidden{opacity:0;pointer-events:none;position:absolute}.emoji-picker .emoji-search{padding-bottom:.5em;-webkit-box-flex:0;flex:0 0 auto}.emoji-picker .emoji-search input{width:100%}.emoji-picker .emoji-groups{height:100%;min-height:200px;-webkit-box-flex:1;flex:1 1 1px;position:relative;overflow:auto;scrollbar-gutter:stable both-edges;-webkit-user-select:none;user-select:none;-webkit-mask:-webkit-linear-gradient(bottom,white 0,transparent 100%) bottom no-repeat,-webkit-linear-gradient(top,white 0,transparent 100%) top no-repeat,-webkit-linear-gradient(bottom,white,white);mask:linear-gradient(to top,white 0,transparent 100%) bottom no-repeat,linear-gradient(to bottom,white 0,transparent 100%) top no-repeat,linear-gradient(to top,white,white);-webkit-transition:-webkit-mask-size .15s;transition:-webkit-mask-size .15s;transition:mask-size .15s;transition:mask-size .15s,-webkit-mask-size .15s;-webkit-mask-size:100% 20px,100% 20px,auto;mask-size:100% 20px,100% 20px,auto;mask-composite:xor;-webkit-mask-composite:xor;mask-composite:exclude}.emoji-picker .emoji-groups.scrolled-top{-webkit-mask-size:100% 20px,100% 0,auto;mask-size:100% 20px,100% 0,auto}.emoji-picker .emoji-groups.scrolled-bottom{-webkit-mask-size:100% 0,100% 20px,auto;mask-size:100% 0,100% 20px,auto}.emoji-picker .emoji-item{width:var(--emoji-size);height:var(--emoji-size);box-sizing:border-box;display:-webkit-box;display:flex;line-height:var(--emoji-size);-webkit-box-align:center;align-items:center;-webkit-box-pack:center;justify-content:center;margin:.2em;cursor:pointer}.emoji-picker .emoji-item .emoji-picker-emoji.-custom{object-fit:contain;width:var(--emoji-size);max-width:var(--emoji-size);height:var(--emoji-size);max-height:var(--emoji-size);--_still_image-label-scale: .5}.emoji-picker .emoji-item .emoji-picker-emoji.-unicode{font-size:calc(var(--emoji-size) * .8);overflow:hidden}.emoji-picker .emoji-group{display:grid;grid-template-columns:repeat(var(--__amount),1fr);place-items:center center;-webkit-box-pack:center;justify-content:center;grid-template-rows:repeat(1,auto)}.emoji-picker .emoji-group.first-row{grid-template-rows:repeat(2,auto)}.emoji-picker .emoji-group.first-row .emoji-item{grid-row:2}.emoji-picker .emoji-group-title{font-size:.85em;grid-column:span var(--__amount);width:100%;margin:0;padding-left:.3em}.emoji-picker .emoji-group-title.disabled{display:none}.input.emoji-input{padding:0;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;position:relative}.input.emoji-input .emoji-picker-icon{position:absolute;top:0;right:0;margin:.2em .25em;font-size:1.3em;cursor:pointer;line-height:1.2em}.input.emoji-input .emoji-picker-icon:hover i{color:var(--text)}.input.emoji-input .emoji-picker-panel{position:absolute;z-index:20;margin-top:.2em}.input.emoji-input .emoji-picker-panel.hide{display:none}.input.emoji-input input,.input.emoji-input textarea{-webkit-box-flex:1;flex:1 0 auto;color:inherit;background:none!important;box-shadow:none;border:none;outline:none}.input.emoji-input.with-picker input{padding-right:2em}.input.emoji-input .hidden-overlay{opacity:0;pointer-events:none;position:absolute;top:0;right:0;bottom:0;left:0;overflow:hidden;color:red}.input.emoji-input .hidden-overlay .caret{width:0;margin-right:calc(-1ch - 1px);border:1px solid red}.autocomplete-panel{position:absolute}.autocomplete-item.menu-item{display:-webkit-box;display:flex;padding-top:0;padding-bottom:0}.autocomplete-item.menu-item .image{width:calc(var(--__line-height) + var(--__vertical-gap) * 2);height:calc(var(--__line-height) + var(--__vertical-gap) * 2);line-height:var(--__line-height);text-align:center;margin-right:var(--__horizontal-gap)}.autocomplete-item.menu-item .image img{width:calc(var(--__line-height) + var(--__vertical-gap) * 2);height:calc(var(--__line-height) + var(--__vertical-gap) * 2);object-fit:contain}.autocomplete-item.menu-item .image span{font-size:var(--__line-height);line-height:var(--__line-height)}.autocomplete-item.menu-item .label{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;-webkit-box-pack:center;justify-content:center;margin:0 .1em 0 .2em}.autocomplete-item.menu-item .label .displayText{line-height:1.5}.autocomplete-item.menu-item .label .detailText{font-size:.6em;line-height:.6em}label.Select{padding:0;display:-webkit-box;display:flex}label.Select select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;border:none;color:var(--text);margin:0;padding:0 2em 0 .5em;font-family:var(--font);font-size:1em;width:100%;z-index:1;line-height:2}label.Select select[multiple],label.Select select[size]{height:100%;padding:.2em}label.Select select[multiple] option,label.Select select[size] option{background-color:transparent}label.Select select[multiple] option:checked,label.Select select[multiple] option.-active,label.Select select[size] option:checked,label.Select select[size] option.-active{color:var(--selectionText);background-color:var(--selectionBackground)}label.Select.disabled,label.Select:disabled{background-color:var(--background);opacity:1;color:var(--faint)}label.Select.disabled select[multiple] option.-active,label.Select.disabled select[size] option.-active,label.Select:disabled select[multiple] option.-active,label.Select:disabled select[size] option.-active{color:var(--faint);background:transparent}label.Select .select-down-icon{position:absolute;top:0;bottom:0;right:.5em;height:100%;width:.875em;font-family:var(--font);line-height:2;z-index:1;pointer-events:none}.poll-form{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;padding:0 .5em .5em}.poll-form .add-option{align-self:flex-start;padding-top:.25em;padding-left:.1em}.poll-form .poll-option{display:-webkit-box;display:flex;-webkit-box-align:baseline;align-items:baseline;-webkit-box-pack:justify;justify-content:space-between;margin-bottom:.25em}.poll-form .input-container{width:100%}.poll-form .input-container input{padding-right:2.5em;width:100%}.poll-form .delete-option{width:1.5em;margin-left:-1.5em;z-index:1}.poll-form .poll-type-expiry{margin-top:.5em;display:-webkit-box;display:flex;width:100%}.poll-form .poll-type{margin-right:.75em;-webkit-box-flex:1;flex:1 1 60%}.poll-form .poll-type .poll-type-select{padding-right:.75em}.poll-form .poll-expiry{display:-webkit-box;display:flex}.poll-form .poll-expiry .expiry-amount{width:3em;text-align:right}.Flash{display:inline-block;width:100%;height:100%;position:relative}.Flash .player{height:100%;width:100%}.Flash .placeholder{height:100%;width:100%;display:-webkit-box;display:flex;-webkit-box-align:center;align-items:center;-webkit-box-pack:center;justify-content:center;background:var(--bg);color:var(--link)}.Flash .hider{top:0}.Flash .label{text-align:center;-webkit-box-flex:1;flex:1 1 0;line-height:1.2;white-space:normal}.Flash .hidden{display:none;visibility:hidden}.Attachment{display:-webkit-inline-box;display:inline-flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;position:relative;align-self:flex-start;line-height:0;height:100%;border-style:solid;border-width:1px;border-radius:var(--roundness);border-color:var(--border)}.Attachment .attachment-wrapper{-webkit-box-flex:1;flex:1 1 auto;height:100%;position:relative;overflow:hidden}.Attachment .description-container{-webkit-box-flex:0;flex:0 1 0;display:-webkit-box;display:flex;padding-top:.5em;z-index:1}.Attachment .description-container p{-webkit-box-flex:1;flex:1;text-align:center;line-height:1.5;padding:.5em;margin:0;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.Attachment .description-container.-static{position:absolute;left:0;right:0;bottom:0;padding-top:0;background:var(--popover);box-shadow:var(--popupShadow)}.Attachment .description-field{-webkit-box-flex:1;flex:1;min-width:0}.Attachment .placeholder-container,.Attachment .image-container,.Attachment .audio-container,.Attachment .video-container,.Attachment .flash-container,.Attachment .oembed-container{display:-webkit-box;display:flex;-webkit-box-pack:center;justify-content:center;width:100%;height:100%}.Attachment .image-container .image{width:100%;height:100%}.Attachment .flash-container .flash,.Attachment .flash-container video,.Attachment .video-container .flash,.Attachment .video-container video{width:100%;height:100%;object-fit:contain;align-self:center}.Attachment .video-container{border:none;outline:none;color:inherit;background:transparent}.Attachment .audio-container{display:-webkit-box;display:flex;-webkit-box-align:end;align-items:flex-end}.Attachment .audio-container audio{width:100%;height:100%}.Attachment .placeholder-container{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;-webkit-box-align:center;align-items:center;-webkit-box-pack:center;justify-content:center;padding-top:.5em}.Attachment .play-icon{position:absolute;font-size:4.5em;top:calc(50% - 2.25rem);left:calc(50% - 2.25rem);color:#ffffffbf;text-shadow:0 0 2px rgba(0,0,0,.4)}.Attachment .play-icon:before{margin:0}.Attachment .attachment-buttons{display:-webkit-box;display:flex;position:absolute;right:0;top:0;margin-top:.5em;margin-right:.5em;z-index:1}.Attachment .attachment-buttons .attachment-button{padding:0;border-radius:var(--roundness);text-align:center;width:2em;height:2em;margin-left:.5em;font-size:1.25em}.Attachment.-contain-fit img,.Attachment.-contain-fit canvas{object-fit:contain}.Attachment.-cover-fit img,.Attachment.-cover-fit canvas{object-fit:cover}.Attachment .oembed-container{line-height:1.2em;-webkit-box-flex:1;flex:1 0 100%;width:100%;margin-right:15px;display:-webkit-box;display:flex}.Attachment .oembed-container img{width:100%}.Attachment .oembed-container .image{-webkit-box-flex:1;flex:1}.Attachment .oembed-container .image img{border:0;border-radius:5px;height:100%;object-fit:cover}.Attachment .oembed-container .text{-webkit-box-flex:2;flex:2;margin:8px;overflow-wrap:break-word;text-wrap:pretty}.Attachment .oembed-container .text h1{font-size:1rem;margin:0}.Attachment.-size-small .play-icon{zoom:.5;opacity:.7}.Attachment.-size-small .attachment-buttons{zoom:.7;opacity:.5}.Attachment.-editable{padding:.5em}.Attachment.-editable .description-container,.Attachment.-editable .attachment-buttons{margin:0}.Attachment.-placeholder{display:inline-block;color:var(--link);overflow:hidden;white-space:nowrap;height:auto;line-height:1.5}.Attachment.-placeholder:not(.-editable){border:none}.Attachment.-placeholder.-editable{display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-direction:row;-webkit-box-align:baseline;align-items:baseline}.Attachment.-placeholder.-editable .description-container,.Attachment.-placeholder.-editable .attachment-buttons{margin:0;padding:0;position:relative}.Attachment.-placeholder.-editable .description-container{-webkit-box-flex:1;flex:1;padding-left:.5em}.Attachment.-placeholder.-editable .attachment-buttons{-webkit-box-ordinal-group:100;order:99;align-self:center}.Attachment.-placeholder a{display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis}.Attachment.-placeholder svg{color:inherit}.Attachment.-loading{cursor:progress}.Attachment.-compact .placeholder-container{padding-bottom:.5em}.Gallery .gallery-rows{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column}.Gallery .gallery-row{position:relative;height:0;width:100%;-webkit-box-flex:1;flex-grow:1}.Gallery .gallery-row .gallery-row-inner{position:absolute;top:0;right:0;bottom:0;left:0;display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row wrap;align-content:stretch}.Gallery .gallery-row .gallery-row-inner .gallery-item{margin:0 .5em 0 0;-webkit-box-flex:1;flex-grow:1;height:100%;box-sizing:border-box;min-width:2em}.Gallery .gallery-row .gallery-row-inner .gallery-item:last-child{margin:0}.Gallery .gallery-row .gallery-row-inner.-grid{width:100%;height:auto;position:relative;display:grid;grid-gap:.5em;grid-template-columns:repeat(auto-fill,minmax(15em,1fr))}.Gallery .gallery-row .gallery-row-inner.-grid .gallery-item{margin:0;height:15em}.Gallery .gallery-row.-grid,.Gallery .gallery-row.-minimal{height:auto}.Gallery .gallery-row.-grid .gallery-row-inner,.Gallery .gallery-row.-minimal .gallery-row-inner{position:relative}.Gallery .gallery-row:not(:first-child){margin-top:.5em}.Gallery.-long .gallery-rows{max-height:25em;overflow:hidden;-webkit-mask:-webkit-linear-gradient(bottom,white,transparent) bottom/100% 70px no-repeat,-webkit-linear-gradient(bottom,white,white);mask:linear-gradient(to top,white,transparent) bottom/100% 70px no-repeat,linear-gradient(to top,white,white);mask-composite:xor;-webkit-mask-composite:xor;mask-composite:exclude}.Gallery .many-attachments-text{text-align:center;line-height:2}.Gallery .many-attachments-buttons{display:-webkit-box;display:flex}.Gallery .many-attachments-button{display:-webkit-box;display:flex;-webkit-box-flex:1;flex:1;-webkit-box-pack:center;justify-content:center;line-height:2}.Gallery .many-attachments-button button{padding:0 2em}.emoji-popover{margin:0 .5em .5em;text-align:center}.emoji-popover .emoji{width:4.6em;height:4.6em}.emoji-popover .emoji-popover-centered{display:-webkit-box;display:flex;-webkit-box-align:center;align-items:center;-webkit-box-pack:center;justify-content:center}.emoji-popover .emoji-popover-button{width:100%;margin-top:.5em}.emoji-popover .Select{width:100%}.Avatar{--_avatarShadowBox: var(--shadow);--_avatarShadowFilter: var(--shadowFilter);--_avatarShadowInset: var(--shadowInset);--_still-image-label-visibility: hidden;display:inline-block;position:relative;width:3.5em;height:3.5em}.Avatar.-compact{width:2.2em;height:2.2em;border-radius:var(--roundness)}.Avatar .avatar{width:100%;height:100%;box-shadow:var(--_avatarShadowBox);border-radius:var(--roundness)}.Avatar .avatar.-better-shadow{box-shadow:var(--_avatarShadowInset);-webkit-filter:var(--_avatarShadowFilter);filter:var(--_avatarShadowFilter)}.Avatar .avatar.-animated:before{display:none}.Avatar .avatar.-compact{border-radius:var(--roundness)}.Avatar .avatar.-placeholder{background-color:var(--background)}.Avatar img{width:100%;height:100%}.Avatar .actor-type-indicator{position:absolute;bottom:0;right:0;margin:-.2em;padding:.2em;background:#7f7f7f80;color:#fff;border-radius:var(--roundness)}.MentionLink{position:relative;white-space:normal;display:inline;word-break:normal}.MentionLink .new,.MentionLink .original{display:inline}.MentionLink .mention-avatar{border-radius:var(--roundness);width:1.5em;height:1.5em;vertical-align:middle;-webkit-user-select:none;user-select:none;margin-right:.2em}.MentionLink .full{position:absolute;display:inline-block;pointer-events:none;opacity:0;top:100%;left:0;height:100%;white-space:nowrap;-webkit-transition:opacity .2s ease;transition:opacity .2s ease;z-index:1;margin-top:.25em;padding:.5em;-webkit-user-select:all;user-select:all}.MentionLink .short.-with-tooltip,.MentionLink .you{-webkit-user-select:none;user-select:none}.MentionLink .short,.MentionLink .full{white-space:nowrap}.MentionLink .shortName{white-space:normal}.MentionLink .new.-you .shortName{font-weight:600}.MentionLink .new.-has-selection{--color: var(--selectionText);--link: var(--selectionText);background-color:var(--selectionBackground)}.MentionLink .new .at{color:var(--link);opacity:.8;display:inline-block;line-height:1;padding:0 .1em;vertical-align:-25%;margin:0}.MentionLink .new.-striped .shortName{background-image:-webkit-repeating-linear-gradient(315deg,var(--____highlight-tintColor),var(--____highlight-tintColor) 5px,var(--____highlight-tintColor2) 5px,var(--____highlight-tintColor2) 10px);background-image:repeating-linear-gradient(135deg,var(--____highlight-tintColor),var(--____highlight-tintColor) 5px,var(--____highlight-tintColor2) 5px,var(--____highlight-tintColor2) 10px)}.MentionLink .new.-solid .shortName{background-image:-webkit-linear-gradient(var(--____highlight-tintColor2),var(--____highlight-tintColor2));background-image:linear-gradient(var(--____highlight-tintColor2),var(--____highlight-tintColor2))}.MentionLink .new.-side .shortName{box-shadow:0 -5px 3px -4px inset var(--____highlight-solidColor)}.MentionLink .serverName.-faded{color:var(--linkFaint)}.mention-link-popover{max-width:70ch;max-height:20rem;overflow:hidden}.MentionsLine{overflow-wrap:break-word;text-wrap:pretty}.MentionsLine .mention-link:not(:first-child):before{content:" "}.MentionsLine .showMoreLess{margin-left:.5em;white-space:normal;color:var(--link)}.HashtagLink{position:relative;white-space:normal;display:inline-block;color:var(--link)}.RichContent{font-family:var(--font)}.RichContent.-faint{color:var(--text);--text: var(--textFaint) !important;--link: var(--linkFaint) !important;--funtextGreentext: var(--funtextGreentextFaint) !important;--funtextCyantext: var(--funtextCyantextFaint) !important}.RichContent.-faint a{color:var(--linkFaint)}.RichContent blockquote{margin:.2em 0 .2em .2em;font-style:italic;border-left:.2em solid var(--textFaint);padding-left:1em}.RichContent pre{overflow:auto}.RichContent code,.RichContent samp,.RichContent kbd,.RichContent var,.RichContent pre{font-family:var(--monoFont)}.RichContent p{margin:0 0 1em}.RichContent p:last-child{margin:0}.RichContent h1{font-size:1.1em;line-height:1.2em;margin:1.4em 0}.RichContent h2{font-size:1.1em;margin:1em 0}.RichContent h3{font-size:1em;margin:1.2em 0}.RichContent h4{margin:1.1em 0}.RichContent .img{display:inline-block}.RichContent .img button{display:-webkit-inline-box;display:inline-flex}.RichContent .emoji{display:inline-block;width:var(--emoji-size, 32px);height:var(--emoji-size, 32px)}.RichContent .img,.RichContent video{max-width:100%;max-height:400px;vertical-align:middle;object-fit:contain}.RichContent .greentext{color:var(--funtextGreentext)}.RichContent .cyantext{color:var(--funtextCyantext)}a .RichContent{color:var(--link)!important}.poll .votes{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;margin:0 0 .5em}.poll .poll-option{margin:.75em .5em}.poll .poll-option .input{line-height:inherit}.poll .option-result{height:100%;display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-direction:row;position:relative;color:var(--textLight)}.poll .option-result-label{display:-webkit-box;display:flex;-webkit-box-align:center;align-items:center;padding:.1em .25em;z-index:1;overflow-wrap:break-word;text-wrap:pretty}.poll .result-percentage{width:3.5em;flex-shrink:0}.poll .result-fill{height:100%;position:absolute;border-radius:var(--roundness);top:0;left:0;-webkit-transition:width .5s;transition:width .5s}.poll input{width:3.5em}.poll.loading *{cursor:progress}.poll .poll-vote-button{padding:0 1em;margin-right:.5em}.poll .poll-checkbox{display:none}.StatusBody{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column}.StatusBody .emoji{--_still_image-label-scale: .5}.StatusBody .attachments{margin-top:.5em}.StatusBody .text,.StatusBody .summary{white-space:pre-wrap;overflow-wrap:break-word;text-wrap:pretty;line-height:var(--post-line-height)}.StatusBody .summary{display:block;font-style:italic;padding-bottom:.5em}.StatusBody .text.-single-line{white-space:nowrap;text-overflow:ellipsis;overflow:hidden;height:1.4em}.StatusBody .summary-wrapper{margin-bottom:.5em;border-style:solid;border-width:0 0 1px;border-color:var(--border);-webkit-box-flex:0;flex-grow:0}.StatusBody .summary-wrapper.-tall{position:relative}.StatusBody .summary-wrapper.-tall .summary{max-height:2em;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.StatusBody .text-wrapper{text-overflow:ellipsis;overflow-wrap:break-word;overflow:hidden;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-flow:column nowrap}.StatusBody .text-wrapper.-tall-status{position:relative;height:16em;z-index:1}.StatusBody .text-wrapper.-tall-status .media-body{min-height:0;-webkit-mask:-webkit-linear-gradient(bottom,white,transparent) bottom/100% 8em no-repeat,-webkit-linear-gradient(bottom,white,white);mask:linear-gradient(to top,white,transparent) bottom/100% 8em no-repeat,linear-gradient(to top,white,white);mask-composite:xor;-webkit-mask-composite:xor;mask-composite:exclude}.StatusBody .text-wrapper.-expanded{overflow:visible}.StatusBody .tall-status-hider,.StatusBody .tall-subject-hider,.StatusBody .status-unhider,.StatusBody .cw-status-hider{display:inline-block;overflow-wrap:break-word;text-wrap:pretty;width:100%;text-align:center}.StatusBody .status-unhider{margin-top:auto;position:-webkit-sticky;position:sticky;bottom:0;padding-bottom:1em}.StatusBody .tall-status-hider{position:absolute;height:5em;margin-top:10em;line-height:8em;z-index:2}.StatusBody .tall-subject-hider{padding-bottom:.5em}.StatusBody .status-unhider,.StatusBody .cw-status-hider{overflow-wrap:break-word;text-wrap:pretty}.StatusBody .status-unhider svg,.StatusBody .cw-status-hider svg{color:inherit}.StatusBody .toggle-button{padding:.5em}.StatusBody.-compact{-webkit-box-align:start;align-items:start;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-direction:row;--emoji-size: calc(var(--emojiSize, 32px) / 2)}.StatusBody.-compact .body,.StatusBody.-compact .attachments{max-height:3.25em}.StatusBody.-compact .body{overflow:hidden;white-space:normal;min-width:5em;-webkit-box-flex:5;flex:5 1 auto;-webkit-mask-size:auto 3.5em,auto auto;mask-size:auto 3.5em,auto auto;-webkit-mask-position:0 0,0 0;mask-position:0 0,0 0;-webkit-mask-repeat:repeat-x,repeat;mask-repeat:repeat-x,repeat;-webkit-mask-image:-webkit-linear-gradient(top,white 2em,transparent 3em);mask-image:linear-gradient(to bottom,white 2em,transparent 3em);mask-composite:xor;-webkit-mask-composite:xor;mask-composite:exclude}.StatusBody.-compact .attachments{margin-top:0;-webkit-box-flex:1;flex:1 1 0;min-width:5em;height:100%;margin-left:.5em}.StatusBody.-compact .summary-wrapper{line-height:inherit;margin:0;border:none}.StatusBody.-compact .summary-wrapper .summary:after{content:": "}.StatusBody.-compact .text-wrapper{display:inline-block;width:100%}.link-preview-card{display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-direction:row;cursor:pointer;overflow:hidden;margin-top:.5em;color:var(--text);border-style:solid;border-width:1px;border-radius:var(--roundness);border-color:var(--border)}.link-preview-card .card-image{flex-shrink:0;width:120px;max-width:25%}.link-preview-card .card-image img{width:100%;height:100%;object-fit:cover;border-radius:var(--roundness)}.link-preview-card .card-content{max-height:100%;margin:.5em;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column}.link-preview-card .card-host{font-size:.85em}.link-preview-card .card-description{margin:.5em 0 0;overflow:hidden;text-overflow:ellipsis;overflow-wrap:break-word;text-wrap:pretty;line-height:1.2em;max-height:calc(3.6em - 1px)}.link-preview-card .nsfw-alert{margin:2em 0}.StatusContent{-webkit-box-flex:1;flex:1;min-width:0}.StatusContent .poll-icon{margin:.5em}.dark-overlay:before{top:0;right:0;bottom:0;left:0;content:" ";display:block;cursor:default;position:fixed;background:#1b1f2380;z-index:2000}.dialog-container{display:grid;position:fixed;top:0;right:0;bottom:0;left:0;-webkit-box-pack:center;justify-content:center;place-items:center center;overflow:auto}.dialog-modal.panel{max-width:90vw;z-index:2001;cursor:default;display:block}.dialog-modal.panel .dialog-modal-heading .title{text-align:center}.dialog-modal.panel .dialog-modal-content{margin:0;padding:1rem;white-space:normal;text-align:center}.dialog-modal.panel .dialog-modal-footer{margin:0;padding:.5em;border-top:1px solid var(--border);display:grid;-webkit-box-pack:end;justify-content:end;grid-gap:.5em;grid-template-columns:-webkit-min-content;grid-template-columns:min-content;grid-auto-columns:-webkit-min-content;grid-auto-columns:min-content;grid-auto-flow:column dense;height:auto}.dialog-modal.panel .dialog-modal-footer button{width:auto;white-space:nowrap;padding-left:2em;padding-right:2em}#modal.-mobile .dialog-container{-webkit-box-pack:stretch;justify-content:stretch;place-items:end stretch}#modal.-mobile .dialog-container.-center-mobile{-webkit-box-align:center;align-items:center}#modal.-mobile .dialog-modal.panel{min-width:100vw}#modal.-mobile .dialog-modal-footer{-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;-webkit-box-pack:end;justify-content:flex-end;grid-template-columns:1fr;grid-auto-rows:auto;grid-auto-flow:row dense}#modal.-mobile .dialog-modal-footer button{grid-column:1;height:3em}.post-status-form{position:relative}.post-status-form .attachments{margin-bottom:.5em}.post-status-form .more-post-actions,.post-status-form .more-post-actions .btn{height:100%}.post-status-form .form-bottom{display:-webkit-box;display:flex;-webkit-box-pack:justify;justify-content:space-between;padding:.5em;height:2.5em}.post-status-form .form-bottom .post-button-group{width:10em;display:-webkit-box;display:flex}.post-status-form .form-bottom .post-button-group .post-button{-webkit-box-flex:1;flex:1 0 auto}.post-status-form .form-bottom .post-button-group .more-post-actions{-webkit-box-flex:0;flex:0 0 auto}.post-status-form .form-bottom p{margin:.35em;padding:.35em;display:-webkit-box;display:flex}.post-status-form .form-bottom-left{display:-webkit-box;display:flex;gap:1.5em}.post-status-form .form-bottom-left button{padding:.5em;margin:-.5em}.post-status-form .preview-heading{display:-webkit-box;display:flex;flex-wrap:wrap}.post-status-form .preview-toggle{-webkit-box-flex:10;flex:10 0 auto;cursor:pointer;-webkit-user-select:none;user-select:none;padding-left:.5em}.post-status-form .preview-toggle:hover{text-decoration:underline}.post-status-form .preview-toggle svg,.post-status-form .preview-toggle i{margin-left:.2em;font-size:.8em;-webkit-transform:rotate(90deg);transform:rotate(90deg)}.post-status-form .preview-container{margin-bottom:1em}.post-status-form .preview-error{font-style:italic;color:var(--textFaint)}.post-status-form .preview-status{border:1px solid var(--border);border-radius:var(--roundness);padding:.5em;margin:0}.post-status-form .reply-or-quote-selector{-webkit-box-flex:1;flex:1 0 auto;margin-bottom:.5em;display:grid;grid-template-columns:1fr 1fr}.post-status-form .text-format .only-format{color:var(--textFaint)}.post-status-form .visibility-tray{display:-webkit-box;display:flex;-webkit-box-pack:justify;justify-content:space-between;-webkit-box-align:baseline;align-items:baseline;margin-left:-.5em}.post-status-form .visibility-notice{border:1px solid var(--border);border-radius:var(--roundness);padding:.5em 1em}.post-status-form .visibility-notice.edit-warning>:first-child{margin-top:0}.post-status-form .visibility-notice.edit-warning>:last-child{margin-bottom:0}.post-status-form .media-upload-icon{-webkit-box-ordinal-group:2;order:1;-webkit-box-pack:left;justify-content:left}.post-status-form .emoji-icon{-webkit-box-ordinal-group:3;order:2;-webkit-box-pack:center;justify-content:center}.post-status-form .poll-icon{-webkit-box-ordinal-group:4;order:3;-webkit-box-pack:right;justify-content:right}.post-status-form .media-upload-icon,.post-status-form .poll-icon,.post-status-form .emoji-icon{font-size:1.85em;line-height:1.1;-webkit-box-flex:1;flex:1;padding:0 .1em;display:-webkit-box;display:flex;-webkit-box-align:center;align-items:center}.post-status-form .error{text-align:center}.post-status-form .media-upload-wrapper{margin-right:.2em;margin-bottom:.5em;width:18em}.post-status-form .media-upload-wrapper img,.post-status-form .media-upload-wrapper video{object-fit:contain;max-height:10em}.post-status-form .media-upload-wrapper .video{max-height:10em}.post-status-form .media-upload-wrapper input{-webkit-box-flex:1;flex:1;width:100%}.post-status-form .status-input-wrapper{display:-webkit-box;display:flex;position:relative;width:100%;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column}.post-status-form .btn[disabled]{cursor:not-allowed}.post-status-form form{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;margin:.6em;position:relative}.post-status-form .form-group{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;padding:.25em .5em .5em;line-height:1.85}.post-status-form .inputs-wrapper{padding:0}.post-status-form .input.form-post-body{box-sizing:content-box;overflow:hidden;-webkit-transition:min-height .2s .1s;transition:min-height .2s .1s;padding-bottom:calc(var(--_padding) + var(--post-line-height) * 1em);padding-right:1.5em;height:calc(var(--post-line-height) * 1em);min-height:calc(var(--post-line-height) * 1em);resize:none;background:transparent;text-wrap:stable}.post-status-form .input.form-post-body.scrollable-form{overflow-y:auto}.post-status-form .main-input{position:relative}.post-status-form .subject-input{border-bottom:1px solid var(--border)}.post-status-form .character-counter{position:absolute;bottom:0;right:0;padding:0;margin:0 .5em}.post-status-form .character-counter.error{color:var(--cRed)}@-webkit-keyframes fade-in{0%{opacity:0}to{opacity:.6}}@keyframes fade-in{0%{opacity:0}to{opacity:.6}}@-webkit-keyframes fade-out{0%{opacity:.6}to{opacity:0}}@keyframes fade-out{0%{opacity:.6}to{opacity:0}}.post-status-form .drop-indicator{position:absolute;width:100%;height:100%;font-size:5em;display:-webkit-box;display:flex;-webkit-box-align:center;align-items:center;-webkit-box-pack:center;justify-content:center;opacity:.6;color:var(--text);background-color:var(--bg);border-radius:var(--roundness);border:2px dashed var(--text)}.post-status-form .auto-save-status{align-self:center}.color-input{display:-webkit-inline-box;display:inline-flex;flex-wrap:wrap;max-width:10em}.color-input.-compact{max-width:none}.color-input .label{-webkit-box-flex:1;flex:1 1 auto;grid-area:label}.color-input .opt{grid-area:checkbox;margin-right:.5em}.color-input-field.input{-webkit-box-flex:1;flex:1 1 10em;max-width:10em;grid-area:input;display:-webkit-box;display:flex;-webkit-box-align:stretch;align-items:stretch}.color-input-field.input input{color:var(--text);background:none;border:none;padding:0;margin:0}.color-input-field.input input.textColor{-webkit-box-flex:1;flex:1 0 3em;min-width:3em;padding:0}.color-input-field.input .nativeColor{cursor:pointer;-webkit-box-flex:0;flex:0 0 auto;padding:0}.color-input-field.input .nativeColor input{-webkit-appearance:none;-moz-appearance:none;appearance:none;max-width:0;min-width:0;max-height:0;opacity:0!important}.color-input-field.input .computedIndicator,.color-input-field.input .validIndicator,.color-input-field.input .invalidIndicator,.color-input-field.input .transparentIndicator{-webkit-box-flex:0;flex:0 0 2em;margin:.2em .5em;min-width:2em;align-self:stretch;min-height:1.1em;border-radius:var(--roundness)}.color-input-field.input .invalidIndicator{background:transparent;box-sizing:border-box;border:2px solid var(--cRed)}.color-input-field.input .transparentIndicator{background-color:#f0f;position:relative}.color-input-field.input .transparentIndicator:before,.color-input-field.input .transparentIndicator:after{display:block;content:"";background-color:#000;position:absolute;height:50%;width:50%}.color-input-field.input .transparentIndicator:after{top:0;left:0;border-top-left-radius:var(--roundness)}.color-input-field.input .transparentIndicator:before{bottom:0;right:0;border-bottom-right-radius:var(--roundness)}.color-input-field.input.disabled .nativeColor input,.color-input-field.input.disabled .computedIndicator,.color-input-field.input.disabled .validIndicator,.color-input-field.input.disabled .invalidIndicator,.color-input-field.input.disabled .transparentIndicator,.color-input-field.input:disabled .nativeColor input,.color-input-field.input:disabled .computedIndicator,.color-input-field.input:disabled .validIndicator,.color-input-field.input:disabled .invalidIndicator,.color-input-field.input:disabled .transparentIndicator{opacity:.25!important}.remote-follow{max-width:220px}.remote-follow .remote-button{width:100%;min-height:2em}.moderation-tools-popover{height:100%}.moderation-tools-popover .trigger{display:-webkit-box!important;display:flex!important;height:100%}.moderation-tools-button{white-space:nowrap}.moderation-tools-button svg,.moderation-tools-button i{font-size:.8em}.UserTimedFilterModal .input-dont-ask-again{margin-left:1em}.UserTimedFilterModal .input-expire-at{margin-left:.25em}.UserTimedFilterModal .footer-left-checkbox{width:-webkit-max-content;width:max-content}.AccountActions .ellipsis-button{width:2.5em;margin:-.5em 0;padding:.5em 0;text-align:center}.panel-loading{display:-webkit-box;display:flex;height:100%;-webkit-box-align:center;align-items:center;-webkit-box-pack:center;justify-content:center;font-size:2em;color:var(--text)}.panel-loading .loading-text svg{line-height:0;vertical-align:middle;color:var(--text)}.user-note{position:relative}.user-note h4{-webkit-box-pack:justify;justify-content:space-between;margin-bottom:.5em}.user-note .note-text{align-self:stretch;min-width:100%;margin:-.6em}.user-note .overlay{position:absolute;top:0;right:0;bottom:0;left:0;background-color:#0000004d}.user-note .overlay .panel-loading{font-size:1em}.image-cropper{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column}.image-cropper-canvas,.image-cropper .image{height:100%}.image-cropper .image-cropper-img-input{display:none}.image-cropper-buttons-wrapper{display:grid;grid-gap:.5em;grid-template-columns:1fr 1fr 1fr}.image-cropper-buttons-wrapper button{margin-top:1em}.user-card{position:relative;z-index:1;overflow:hidden;border-top-left-radius:calc(var(--roundness) - 1px);border-top-right-radius:calc(var(--roundness) - 1px)}.user-card h4{line-height:2;display:-webkit-box;display:flex}.user-card h3{padding-left:.6em;margin-bottom:0}.user-card h3 .button-default{font-size:1rem;line-height:2;padding:0 .6em}.user-card .input.bio{height:auto}.user-card .follow-button{-webkit-box-flex:1;flex:1 0 auto}.user-card .user-card-inner{padding-bottom:0}.user-card-setting,.user-card-bio{color:var(--lightText);display:block;line-height:1.3;padding:0 .6em}.user-card-setting img,.user-card-bio img{object-fit:contain;vertical-align:middle;max-width:100%;max-height:400px}.user-card .user-card-setting{margin-left:.6em;margin-right:.6em}.user-card .user-card-bio{text-align:center;margin:0 .6em}.user-card .user-card-bio.input{margin:0 1em}.user-card .user-card-bio.input textarea{text-align:inherit}.user-card .user-card-bio,.user-card .user-card-bio *{line-height:1.5}.user-card .user-card-bio.-justify-left{text-align:start}.user-card:hover{--_still-image-img-visibility: visible;--_still-image-canvas-visibility: hidden;--_still-image-label-visibility: hidden}.user-card .personal-marks{margin:.6em;padding:.6em}.user-card .personal-marks:not(:last-child){border-bottom:1px solid var(--border)}.user-card .personal-marks .highlighter h4{margin-top:.6em}.user-card .personal-marks .highlighter .userHighlightSel{vertical-align:bottom;margin-left:-.1em}.user-card .personal-marks .highlighter .userHighlightSel.-none select{color:var(--textFaint)}.user-card .personal-marks .highlighter .userHighlightSel.-none select option{color:var(--text);background:var(--background)}.user-card .personal-marks .highlighter .highlighter-color{vertical-align:bottom;margin-left:.6em}.user-card .header-overlay{position:absolute;bottom:0;right:-1.2em;left:-1.2em;top:-1.4em;-webkit-mask:-webkit-linear-gradient(bottom,transparent 0,white 5em) bottom no-repeat;mask:linear-gradient(to top,transparent 0,white 5em) bottom no-repeat}.user-card .banner-overlay,.user-card .banner-image{position:absolute;top:0;right:0;bottom:0;left:0;padding:0;border-top-left-radius:calc(var(--roundness) - 1px);border-top-right-radius:calc(var(--roundness) - 1px)}.user-card .banner-image{z-index:-2}.user-card .banner-image img{object-fit:cover;height:100%;width:100%}.user-card .banner-overlay{background-color:var(--profileTint);opacity:.5;pointer-events:none;z-index:-1}.user-card .bottom-buttons{display:-webkit-box;display:flex;gap:.5em}.user-info{position:relative;text-align:left;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;gap:1em}.user-info .user-identity{position:relative;aspect-ratio:3;min-height:6em;display:-webkit-box;display:flex;-webkit-box-align:end;align-items:flex-end;container:user-card/inline-size}.user-info .user-identity>*{min-width:0}.user-info .user-identity>a{vertical-align:middle;display:-webkit-box;display:flex}.user-info .user-identity .Avatar{--_avatarShadowBox: var(--avatarShadow);--_avatarShadowFilter: var(--avatarShadowFilter);--_avatarShadowInset: var(--avatarShadowInset);width:7em;width:min(7em,20cqw);height:7em;height:min(7em,20cqw);object-fit:cover}.user-info .other-actions{position:relative;display:inline-grid;grid-auto-flow:column;grid-gap:.6em;font-size:1.25rem;z-index:2;align-self:end}.user-info .other-actions a,.user-info .other-actions button,.user-info .other-actions div{text-align:center;width:2em;height:2em;line-height:2em;padding:.6em;margin:-.6em}.user-info .other-actions a.edit-banner-button,.user-info .other-actions button.edit-banner-button,.user-info .other-actions div.edit-banner-button{width:auto}.user-info .other-actions a:hover .icon,.user-info .other-actions button:hover .icon,.user-info .other-actions div:hover .icon{color:var(--textFaint)}.user-info .other-actions a:not(:hover) .icon,.user-info .other-actions button:not(:hover) .icon,.user-info .other-actions div:not(:hover) .icon{color:var(--lightText)}.user-info-avatar{position:relative;cursor:pointer;margin-right:.6em}.user-info-avatar.-overlay{position:absolute;top:-.6em;bottom:-.6em;left:-.6em;right:-1.2em;display:-webkit-box;display:flex;-webkit-box-pack:center;justify-content:center;-webkit-box-align:center;align-items:center;border-radius:var(--roundness);opacity:0;-webkit-transition:opacity .2s ease;transition:opacity .2s ease}.user-info-avatar.-overlay svg{color:#fff;margin:.5em}.user-info-avatar:hover .user-info-avatar.-overlay{opacity:1;background-color:#0000004d}.user-info .user-info-avatar.-editable .-overlay{opacity:1;place-items:start end;-webkit-box-pack:end;justify-content:end}.user-info .user-summary{align-self:stretch;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;margin-left:.6em;text-align:left;text-overflow:ellipsis;white-space:nowrap;-webkit-box-flex:1;flex:1 1 0;z-index:1;line-height:2em;-webkit-filter:drop-shadow(0 0 .5em var(--profileTint)) drop-shadow(0 0 .2em var(--profileTint));filter:drop-shadow(0 0 .5em var(--profileTint)) drop-shadow(0 0 .2em var(--profileTint));--emoji-size: 1.7em}.user-info .user-summary .alert{text-shadow:none}.user-info .user-summary .RichContent{--link: var(--text) !important}.user-info .user-summary .name-wrapper{display:-webkit-box;display:flex;-webkit-box-align:baseline;align-items:baseline}.user-info .user-summary .name-wrapper .edit-button{width:3em;text-align:center}.user-info .user-summary .name-wrapper .edit-button:hover .icon{color:var(--textFaint)}.user-info .user-summary .name-wrapper .edit-button:not(:hover) .icon{color:var(--lightText)}.user-info .user-summary .name-wrapper .input,.user-info .user-summary .name-wrapper .user-name{-webkit-box-flex:1;flex:1;font-weight:600;line-height:2;margin-right:1em;text-overflow:ellipsis;overflow:hidden}.user-info .user-summary .name-wrapper .input{margin:0 -.5em;padding:0 .5em}.user-info .user-summary .top-line{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;-webkit-box-pack:justify;justify-content:space-between;-webkit-box-flex:1;flex:1;line-height:2;margin-bottom:-.2em;font-size:max(110%,4cqw)}.user-info .user-summary .bottom-line{display:-webkit-box;display:flex;-webkit-box-align:baseline;align-items:baseline;flex-wrap:wrap;white-space:normal;font-weight:500;font-size:.9em;font-size:max(90%,2.5cqw);line-height:1.5;margin-top:.5em}.user-info .user-summary .lock-icon{display:inline-block;vertical-align:baseline;overflow-x:hidden;margin:0 .35em 0 .5em}.user-info .user-summary .user-screen-name{color:var(--text);text-overflow:ellipsis;overflow:hidden;display:inline-block}.user-info .user-summary .user-role{display:inline-block;vertical-align:baseline}.user-info .user-interactions{position:relative;display:-webkit-box;display:flex;flex-wrap:wrap;gap:.6em}.user-info .user-interactions>*{-webkit-box-flex:0;flex:0 0 8em}.user-info .user-interactions .popover-trigger-button,.user-info .user-interactions .moderation-tools-button{width:100%}.user-info .user-interactions>.btn-group,.user-info .user-interactions>button{white-space:nowrap}.sidebar .edit-profile-button{display:none}.user-extras{padding:1.2em .6em;line-height:1.5}.user-extras .user-stats{display:block;text-align:center;word-wrap:break-word;width:100%}.user-extras .user-stats dl{display:inline;margin-right:1em;white-space:nowrap}.user-extras .user-stats dd{font-weight:bolder;text-overflow:ellipsis;overflow:hidden;margin:0}.user-extras .user-stats dd,.user-extras .user-stats dt{display:inline}.user-extras .user-stats a{text-decoration:none}.user-extras .birthday{width:100%;text-align:center;white-space:nowrap;--icon: var(--text)}.user-profile-fields{margin:0 .5em;padding:0 .5em;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;--emoji-size: 1.8em}.user-profile-fields .user-profile-field-add,.user-profile-fields .user-profile-field{display:-webkit-box;display:flex;-webkit-box-align:center;align-items:center;margin:.25em;border:1px solid var(--border);border-radius:var(--roundness);line-height:2em}.user-profile-fields .user-profile-field-add .label,.user-profile-fields .user-profile-field .label{margin-left:.5em}.user-profile-fields .user-profile-field-add{-webkit-box-pack:center;justify-content:center;margin:.25em}.user-profile-fields .user-profile-field .input{text-align:inherit;-webkit-box-flex:1;flex:1}.user-profile-fields .user-profile-field .delete-field{display:inline-block;text-align:center;width:2em}.user-profile-fields .user-profile-field .user-profile-field-name,.user-profile-fields .user-profile-field .user-profile-field-value,.user-profile-fields .user-profile-field .user-profile-field-add{text-overflow:ellipsis;white-space:nowrap;overflow:hidden;box-sizing:border-box;display:-webkit-inline-box;display:inline-flex}.user-profile-fields .user-profile-field .user-profile-field-name.-edit,.user-profile-fields .user-profile-field .user-profile-field-value.-edit,.user-profile-fields .user-profile-field .user-profile-field-add.-edit{padding:0}.user-profile-fields .user-profile-field .user-profile-field-name.-edit input,.user-profile-fields .user-profile-field .user-profile-field-value.-edit input,.user-profile-fields .user-profile-field .user-profile-field-add.-edit input{font-weight:inherit}.user-profile-fields .user-profile-field .user-profile-field-name{-webkit-box-flex:0;flex:0 1 50%;font-weight:600;text-align:right;-webkit-box-pack:end;justify-content:end;color:var(--lightText);min-width:9em;border-right:1px solid var(--border);padding-left:1.2em;padding-right:.6em}.user-profile-fields .user-profile-field .user-profile-field-value{-webkit-box-flex:1;flex:1 1 55%;color:var(--text);padding-left:.6em;margin:0}.edit-image .panel-body{text-align:center;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column}.edit-image .current-avatar{object-fit:contain}.edit-image .image-container{display:-webkit-box;display:flex;align-self:center;margin:0 0 1em;max-height:30em;max-width:100%;-webkit-box-flex:1;flex:1 0 20em;aspect-ratio:1;gap:.5em}.edit-image .image-container.-banner{aspect-ratio:3;max-width:100%}.edit-image .image-container .new-image{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column}.edit-image .image-container .cropper{-webkit-box-flex:1;flex:1;overflow-x:auto}.edit-image .image-container>*{-webkit-box-flex:1;flex:1 0 10em;max-height:100%;aspect-ratio:1}.edit-image .image-container .separator{min-width:1.1em;font-size:500%;align-self:center;-webkit-box-flex:0;flex:0 1 5em;aspect-ratio:unset}.edit-image.-banner .images-container{grid-template-rows:20em 5em 20em;grid-template-columns:1fr}.edit-image.-banner .images-container>*{-webkit-box-flex:1;flex:1 0 10em;width:100%;aspect-ratio:3}.edit-image.-banner .separator{min-height:1.1em;font-size:500%;justify-self:center;-webkit-box-flex:0;flex:0 1 5em;aspect-ratio:unset}#modal.-mobile .edit-image #pick-image{height:3em}#modal.-mobile .edit-image .image-container.-banner{max-height:10em}.user-panel .panel{background:var(--background);-webkit-backdrop-filter:var(--backdrop-filter);backdrop-filter:var(--backdrop-filter)}.user-panel .user-info{margin:.6em .6em 0}.user-panel .user-info .Avatar{width:5em;width:min(5em,20cqw);height:5em;height:min(5em,20cqw)}.user-panel .post-status-form form{margin-top:0}.user-panel .signed-in{z-index:10}.NavigationEntry.menu-item{--__line-height: 2.5em;--__horizontal-gap: .5em;--__vertical-gap: .4em;padding:var(--__vertical-gap) var(--__horizontal-gap);display:grid;grid-template-columns:1fr;grid-auto-columns:var(--__line-height);grid-auto-flow:column;grid-gap:var(--__horizontal-gap);-webkit-box-align:baseline;align-items:baseline}.NavigationEntry.menu-item[aria-expanded]{padding-right:var(--__horizontal-gap)}.NavigationEntry.menu-item .main-link{line-height:var(--__line-height);box-sizing:border-box}.NavigationEntry.menu-item .menu-icon{line-height:var(--__line-height);padding:0;width:var(--__line-height);margin-right:var(--__horizontal-gap)}.NavigationEntry.menu-item .timelines-chevron,.NavigationEntry.menu-item .extra-button{line-height:var(--__line-height);width:100%;padding:0;text-align:center}.NavigationEntry.menu-item .badge{justify-self:center}.NavigationEntry.menu-item .iconEmoji{display:inline-block;text-align:center;object-fit:contain;vertical-align:middle;height:var(--__line-height);width:var(--__line-height)}.NavigationEntry.menu-item .iconEmoji>span{font-size:1.5rem}.NavigationEntry.menu-item img.iconEmoji{padding:.25rem;box-sizing:border-box}.NavigationPins{display:-webkit-box;display:flex;flex-wrap:wrap;overflow:hidden;height:100%}.NavigationPins.alert-dot-notification{background-color:var(--badgeNotification)}.NavigationPins.alert-dot-neutral{background-color:var(--badgeNeutral)}.NavigationPins .pinned-item{position:relative;-webkit-box-flex:1;flex:1 0 3em;min-width:2em;text-align:center;overflow:visible;box-sizing:border-box;height:100%}.NavigationPins .pinned-item .bookmark-emoji{height:100%;box-sizing:border-box;padding:.5em}.NavigationPins .pinned-item .bookmark-emoji,.NavigationPins .pinned-item .svg-inline--fa,.NavigationPins .pinned-item .iconLetter{margin:0}.NavigationPins .pinned-item.toggled{margin-bottom:-4px;border-bottom:4px solid}.NavPanel .panel{overflow:hidden;box-shadow:var(--shadow)}.NavPanel ul{list-style:none;margin:0;padding:0}.NavPanel .navigation-chevron{margin-left:.8em;margin-right:.8em;font-size:1.1em}.NavPanel .timelines-background{padding:0 0 0 .6em}.NavPanel .nav-panel-heading{--panel-heading-height-padding: 0px}.features-panel li{line-height:24px}.who-to-follow *{vertical-align:middle}.who-to-follow img{width:32px;height:32px}.who-to-follow{padding:0 1em;margin:0}.who-to-follow-items{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:0;margin:1em 0}.who-to-follow-more{padding:0;margin:1em 0;text-align:center}.floating-shout{position:fixed;bottom:.5em;z-index:var(--ZI_popovers);max-width:25em}.floating-shout.-left{left:.5em}.floating-shout:not(.-left){right:.5em}.shout-panel .shout-heading{cursor:pointer}.shout-panel .shout-heading .icon{color:var(--text);margin-right:.5em}.shout-panel .shout-heading .title{display:-webkit-box;display:flex;-webkit-box-pack:justify;justify-content:space-between;-webkit-box-align:center;align-items:center}.shout-panel .shout-window{overflow:hidden auto;max-height:20em}.shout-panel .shout-window-container{height:100%}.shout-panel .shout-message{display:-webkit-box;display:flex;padding:.2em .5em}.shout-panel .shout-avatar img{height:.6em;width:.6em;border-radius:var(--roundness);margin-right:.5em;margin-top:.25em}.shout-panel .shout-input{display:-webkit-box;display:flex}.shout-panel .shout-input textarea{-webkit-box-flex:1;flex:1;margin:.6em;min-height:3.5em;resize:none}.shout-panel .shout-panel .title{display:-webkit-box;display:flex;-webkit-box-pack:justify;justify-content:space-between}@-webkit-keyframes media-fadein{0%{opacity:0}to{opacity:1}}@keyframes media-fadein{0%{opacity:0}to{opacity:1}}.media-modal-view .modal-image-container{display:-webkit-box;display:flex;overflow:hidden;-webkit-box-align:center;align-items:center;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;max-width:100%;max-height:100%;width:100%;height:100%;-webkit-box-flex:1;flex-grow:1;-webkit-box-pack:center;justify-content:center}.media-modal-view .modal-image-container-inner{width:100%;height:100%;-webkit-box-flex:1;flex-grow:1;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;-webkit-box-align:center;align-items:center;-webkit-box-pack:center;justify-content:center}.media-modal-view .description,.media-modal-view .counter{color:#fff;margin-top:1em;text-shadow:0 0 10px black,0 0 10px black;padding:.2em 2em}.media-modal-view .description{-webkit-box-flex:0;flex:0 0 auto;overflow-y:auto;min-height:1em;max-width:35.8em;max-height:9.5em;overflow-wrap:break-word;text-wrap:pretty}.media-modal-view .modal-image{max-width:100%;max-height:100%;image-orientation:from-image;-webkit-animation:.1s cubic-bezier(.7,0,1,.6) media-fadein;animation:.1s cubic-bezier(.7,0,1,.6) media-fadein}.media-modal-view .modal-image.loading{opacity:.5}.media-modal-view .loading-spinner{width:100%;height:100%;position:absolute;pointer-events:none;display:-webkit-box;display:flex;-webkit-box-pack:center;justify-content:center;-webkit-box-align:center;align-items:center}.media-modal-view .loading-spinner svg{color:#fff}.media-modal-view .modal-view-button{border:0;padding:0;opacity:0;box-shadow:none;background:none;-webkit-appearance:none;-moz-appearance:none;appearance:none;overflow:visible;cursor:pointer;-webkit-transition:opacity 333ms cubic-bezier(.4,0,.22,1);transition:opacity 333ms cubic-bezier(.4,0,.22,1);height:3em;width:3em}.media-modal-view .modal-view-button .button-icon{position:absolute;height:3em;width:3em;font-size:1rem;line-height:3em;color:#fff;text-align:center;background-color:#0000004d}.media-modal-view .modal-view-button-arrow{position:absolute;display:block;top:50%;margin-top:1.5em;width:3em;height:3em}.media-modal-view .modal-view-button-arrow .arrow-icon{position:absolute;top:0;line-height:3em;color:#fff;text-align:center;background-color:#0000004d}.media-modal-view .modal-view-button-arrow--prev{left:0}.media-modal-view .modal-view-button-arrow--prev .arrow-icon{left:.5em}.media-modal-view .modal-view-button-arrow--next{right:0}.media-modal-view .modal-view-button-arrow--next .arrow-icon{right:.5em}.media-modal-view .modal-view-button-hide{position:absolute;top:0;right:0}.media-modal-view .modal-view-button-hide .button-icon{top:.5em;right:.5em}.modal-view.media-modal-view{z-index:var(--ZI_media_modal);-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;overflow:hidden}.modal-view.media-modal-view .modal-view-button-arrow,.modal-view.media-modal-view .modal-view-button-hide{opacity:.75}.modal-view.media-modal-view .modal-view-button-arrow:focus,.modal-view.media-modal-view .modal-view-button-arrow:hover,.modal-view.media-modal-view .modal-view-button-hide:focus,.modal-view.media-modal-view .modal-view-button-hide:hover{outline:none;box-shadow:none}.modal-view.media-modal-view .modal-view-button-arrow:hover,.modal-view.media-modal-view .modal-view-button-hide:hover{opacity:1}.side-drawer-container{position:fixed;z-index:var(--ZI_navbar);top:0;left:0;width:100%;height:100%;display:-webkit-box;display:flex;-webkit-box-align:stretch;align-items:stretch;-webkit-transition-duration:0s;transition-duration:0s;-webkit-transition-property:-webkit-transform;transition-property:-webkit-transform;transition-property:transform;transition-property:transform,-webkit-transform}.side-drawer-container-open{-webkit-transform:translate(0%);transform:translate(0)}.side-drawer-container-closed{-webkit-transition-delay:.35s;transition-delay:.35s;-webkit-transform:translate(-100%);transform:translate(-100%)}.side-drawer-darken{top:0;left:0;width:100vw;height:100vh;position:fixed;z-index:-1;-webkit-transition:.35s;transition:.35s;-webkit-transition-property:background-color;transition-property:background-color;background-color:#00000080}.side-drawer-darken-closed{background-color:#0000}.side-drawer-click-outside{-webkit-box-flex:1;flex:1 1 100%}.side-drawer{overflow-x:hidden;-webkit-transition:.35s;transition:.35s;-webkit-transition-timing-function:cubic-bezier(0,1,.5,1);transition-timing-function:cubic-bezier(0,1,.5,1);-webkit-transition-property:-webkit-transform;transition-property:-webkit-transform;transition-property:transform;transition-property:transform,-webkit-transform;margin:0 0 0 -100px;padding:0 0 1em 100px;width:80%;max-width:20em;-webkit-box-flex:0;flex:0 0 80%;box-shadow:var(--shadow);background-color:var(--background)}.side-drawer .badge{margin-left:10px}.side-drawer-logo-wrapper{display:-webkit-box;display:flex;-webkit-box-align:center;align-items:center;padding:.85em}.side-drawer-logo-wrapper img{-webkit-box-flex:0;flex:none;height:50px;margin-right:.85em}.side-drawer-logo-wrapper span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.side-drawer-click-outside-closed{-webkit-box-flex:0;flex:0 0 0}.side-drawer-closed{-webkit-transform:translate(-100%);transform:translate(-100%)}.side-drawer-heading{background:transparent;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;-webkit-box-align:stretch;align-items:stretch;display:-webkit-box;display:flex;padding:0;margin:0}.side-drawer-heading .user-info{margin:1em}.side-drawer ul{list-style:none;margin:0;padding:0;border-bottom:1px solid;border-color:var(--border)}.side-drawer ul:last-child{border:0}.side-drawer li{padding:0}.side-drawer li a,.side-drawer li button{box-sizing:border-box;display:block;height:3em;line-height:3em;padding:0 .7em}.MobilePostButton.button-default{width:5em;height:5em;border-radius:100%;position:fixed;bottom:1.5em;right:1.5em;display:-webkit-box;display:flex;-webkit-box-pack:center;justify-content:center;-webkit-box-align:center;align-items:center;box-shadow:0 2px 2px #0000004d,0 4px 6px #0000004d;z-index:10;-webkit-transition:.35s -webkit-transform;transition:.35s -webkit-transform;transition:.35s transform;transition:.35s transform,.35s -webkit-transform;-webkit-transition-timing-function:cubic-bezier(0,1,.5,1);transition-timing-function:cubic-bezier(0,1,.5,1)}.MobilePostButton.hidden{-webkit-transform:translateY(150%);transform:translateY(150%)}.MobilePostButton svg{font-size:1.5em;color:var(--text)}@media all and (width >= 801px){.new-status-button:not(.always-show){display:none}}.avatars{display:-webkit-box;display:flex;margin:0;padding:0;flex-wrap:wrap;height:24px}.avatars .avatars-item{margin:0 0 5px 5px}.avatars .avatars-item:first-child{padding-left:5px}.avatars .avatars-item .avatar-small{border-radius:var(--roundness);height:24px;width:24px}.status-popover.popover{font-size:1rem;min-width:15em;max-width:95%;border-color:var(--border);border-style:solid;border-width:1px}.status-popover.popover .Status.Status{border:none}.status-popover.popover .status-preview-no-content{padding:1em;text-align:center}.status-popover.popover .status-preview-no-content i{font-size:2em}.user-popover{margin:0}.user-popover .user-info{width:100%;margin:1.2em}.user-popover .user-identity{aspect-ratio:unset;min-width:min(30em,98vw)}.user-popover .user-card-inner{display:-webkit-box;display:flex}.user-popover.popover{overflow:hidden}.user-list-popover{padding:.5em;--emoji-size: calc(var(--emojiSize, 32px) / 2)}.user-list-popover .user-list-row{padding:.25em;display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-direction:row}.user-list-popover .user-list-row .user-list-names{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;margin-left:.5em;min-width:5em}.user-list-popover .user-list-row .user-list-screen-name{font-size:.65em}.EmojiReactions{display:-webkit-box;display:flex;margin-top:.25em;flex-wrap:wrap;--emoji-size: calc(var(--emojiSize, 1.25em) * var(--emojiReactionsScale, 1))}.EmojiReactions .emoji-reaction-container{display:-webkit-box;display:flex;-webkit-box-align:stretch;align-items:stretch;margin-top:.5em;margin-right:.5em}.EmojiReactions .emoji-reaction-container .emoji-reaction-popover{padding:0}.EmojiReactions .emoji-reaction-container .emoji-reaction-popover .emoji-reaction-count-button{margin:0;height:100%;border-top-left-radius:0;border-bottom-left-radius:0;box-sizing:border-box;min-width:2em;display:-webkit-inline-box;display:inline-flex;-webkit-box-pack:center;justify-content:center;-webkit-box-align:center;align-items:center}.EmojiReactions .emoji-reaction{padding-left:.5em;display:-webkit-box;display:flex;-webkit-box-align:center;align-items:center;-webkit-box-pack:center;justify-content:center;box-sizing:border-box;border-top-right-radius:0;border-bottom-right-radius:0;margin:0}.EmojiReactions .emoji-reaction .reaction-emoji{width:var(--emoji-size);height:var(--emoji-size);margin-right:.25em;line-height:var(--emoji-size);display:-webkit-box;display:flex;-webkit-box-pack:center;justify-content:center;-webkit-box-align:center;align-items:center;--_still_image-label-scale: .3}.EmojiReactions .emoji-reaction .reaction-emoji-content{max-width:100%;max-height:100%;width:var(--emoji-size);height:var(--emoji-size);line-height:inherit;overflow:hidden;font-size:calc(var(--emoji-size) * .8);margin:0}.EmojiReactions .emoji-reaction .reaction-emoji-content img{object-fit:contain}.EmojiReactions .emoji-reaction:focus{outline:none}.EmojiReactions .emoji-reaction .svg-inline--fa{color:var(--text)}.EmojiReactions .emoji-reaction.-picked-reaction .svg-inline--fa{color:var(--accent)}.EmojiReactions .emoji-reaction .focus-marker{visibility:hidden}.EmojiReactions .emoji-reaction .active-marker{visibility:visible}.EmojiReactions .emoji-reaction:focus:not(:focus-visible,:hover) .focus-marker{visibility:hidden}.EmojiReactions .emoji-reaction:focus:not(:focus-visible,:hover) .active-marker{visibility:visible}.EmojiReactions .emoji-reaction:hover .svg-inline--fa,.EmojiReactions .emoji-reaction:focus .svg-inline--fa{color:var(--accent)}.EmojiReactions .emoji-reaction:hover .focus-marker,.EmojiReactions .emoji-reaction:focus .focus-marker{visibility:visible}.EmojiReactions .emoji-reaction:hover .active-marker,.EmojiReactions .emoji-reaction:focus .active-marker{visibility:hidden}.EmojiReactions .emoji-reaction:focus-visible .svg-inline--fa{color:var(--accent)}.EmojiReactions .emoji-reaction:focus-visible .focus-marker{visibility:visible}.EmojiReactions .emoji-reaction:focus-visible .active-marker{visibility:hidden}.EmojiReactions .emoji-reaction-expand{padding:0 .5em;margin-right:.5em;margin-top:.5em;display:-webkit-box;display:flex;-webkit-box-align:center;align-items:center;-webkit-box-pack:center;justify-content:center}.EmojiReactions .emoji-reaction-expand:hover{text-decoration:underline}.bookmark-folder-menu{--__horizontal-gap: .5em;--_still-image-label-visibility: hidden}.bookmark-folder-menu .emoji{width:var(--__line-height);height:var(--__line-height)}.quick-action{-webkit-box-pack:justify;justify-content:space-between;display:-webkit-box;display:flex;-webkit-box-align:baseline;align-items:baseline;-webkit-box-align:center;align-items:center;height:1.5em}.quick-action .action-counter{overflow-x:hidden;text-overflow:ellipsis;white-space:nowrap;margin-left:1em}.quick-action .action-button-inner,.quick-action .extra-button{margin:-.5em;padding:.5em}.quick-action .separator{display:block;align-self:stretch;width:1px;background-color:var(--icon);margin-left:.75em;margin-right:.125em}.quick-action.-pin{margin:calc(-2px - .25em);padding:.25em;border:2px dashed var(--icon);border-radius:var(--roundness);grid-template-columns:minmax(-webkit-max-content,1fr) auto;grid-template-columns:minmax(max-content,1fr) auto}.quick-action.-pin .chevron-icon,.quick-action.-pin .extra-button,.quick-action.-pin .separator{display:none}.quick-action .action-button-inner{display:grid;grid-gap:1em;grid-template-columns:-webkit-max-content;grid-template-columns:max-content;grid-auto-flow:column;grid-auto-columns:-webkit-max-content;grid-auto-columns:max-content;-webkit-box-align:center;align-items:center}.quick-action .action-button-inner .focus-marker{visibility:hidden}.quick-action .action-button-inner .active-marker{visibility:visible}.quick-action .action-button-inner:focus:not(:focus-visible,:hover) .focus-marker{visibility:hidden}.quick-action .action-button-inner:focus:not(:focus-visible,:hover) .active-marker{visibility:visible}.quick-action .action-button-inner:hover .focus-marker,.quick-action .action-button-inner:focus .focus-marker{visibility:visible}.quick-action .action-button-inner:hover .active-marker,.quick-action .action-button-inner:focus .active-marker{visibility:hidden}.quick-action .action-button-inner:focus-visible .focus-marker{visibility:visible}.quick-action .action-button-inner:focus-visible .active-marker{visibility:hidden}.action-button{display:grid;grid-auto-flow:column;-webkit-box-align:center;align-items:center;padding:0}.action-button .action-button-inner:hover.reply-button:not(.disabled) .svg-inline--fa,.action-button .action-button-inner.-active.reply-button:not(.disabled) .svg-inline--fa{color:var(--cBlue)}.action-button .action-button-inner:hover.retweet-button:not(.disabled) .svg-inline--fa,.action-button .action-button-inner.-active.retweet-button:not(.disabled) .svg-inline--fa{color:var(--cGreen)}.action-button .action-button-inner:hover.favorite-button:not(.disabled) .svg-inline--fa,.action-button .action-button-inner.-active.favorite-button:not(.disabled) .svg-inline--fa{color:var(--cOrange)}.action-button.-extra .action-counter{justify-self:end;margin-right:1em}.action-button.-extra .chevron-icon{justify-self:end}.action-button.-extra .extra-button{justify-self:end;-webkit-box-pack:end;justify-content:end}.expiry-amount{width:4em;text-align:right}.StatusActionButtons .quick-action-buttons{display:grid;grid-template-columns:repeat(auto-fill,minmax(10%,3em));grid-auto-flow:row dense;grid-auto-rows:1fr;grid-gap:1.25em 1em;margin-top:var(--status-margin)}.StatusActionButtons .pin-action-button{margin:-.5em;padding:.5em}.extra-action-buttons .extra-action{margin:0;padding-top:0;padding-bottom:0;padding-right:0}.Status{min-width:0;white-space:normal;overflow-wrap:break-word;text-wrap:pretty}.Status:hover{--_still-image-img-visibility: visible;--_still-image-canvas-visibility: hidden;--_still-image-label-visibility: hidden}.Status .gravestone{padding:var(--status-margin);display:-webkit-box;display:flex}.Status .gravestone .deleted-text{margin:.5em 0;-webkit-box-align:center;align-items:center}.Status .status-container{display:-webkit-box;display:flex;padding:var(--status-margin)}.Status .status-container>*{min-width:0}.Status .status-container.-repeat{padding-top:0}.Status .pin{padding:var(--status-margin) var(--status-margin) 0;display:-webkit-box;display:flex;-webkit-box-align:center;align-items:center;-webkit-box-pack:end;justify-content:flex-end}._misclick-prevention .Status{pointer-events:none}._misclick-prevention .Status .attachments{pointer-events:initial;cursor:initial}.Status .left-side{margin-right:var(--status-margin)}.Status .right-side{-webkit-box-flex:1;flex:1;min-width:0}.Status .usercard{margin-bottom:var(--status-margin)}.Status .status-username{white-space:nowrap;overflow:hidden;max-width:85%;font-weight:700;flex-shrink:1;margin-right:.4em;text-overflow:ellipsis;--_still_image-label-scale: .25;--emoji-size: 1em}.Status .status-favicon{height:1.2em;width:1.2em;margin-right:.4em;object-fit:contain}.Status .status-heading{margin-bottom:.5em}.Status .heading-name-row{display:-webkit-box;display:flex;-webkit-box-pack:justify;justify-content:space-between;line-height:1.3}.Status .heading-name-row a{display:inline-block;white-space:nowrap;text-overflow:ellipsis;overflow-x:hidden;width:100%}.Status .account-name{display:inline-block;min-width:1em;margin-right:.4em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;-webkit-box-flex:1;flex:1 1 0}.Status .account-name.unknown{min-width:8em}.Status .heading-left{display:-webkit-box;display:flex;min-width:0}.Status .heading-right{display:-webkit-box;display:flex;flex-shrink:0;align-self:baseline}.Status .heading-right .button-unstyled{padding:.2em;margin:-.2em}.Status .heading-right .svg-inline--fa{margin-left:.25em}.Status .glued-label{display:-webkit-inline-box;display:inline-flex;white-space:nowrap}.Status .timeago{margin-right:.2em}.Status .heading-reply-row,.Status .heading-edited-row{position:relative;align-content:baseline;font-size:.85em;margin-top:.2em;line-height:130%;max-width:100%;-webkit-box-align:stretch;align-items:stretch}.Status .reply-to-popover,.Status .reply-to-no-popover,.Status .mentions{min-width:0;margin-right:.4em;flex-shrink:0}.Status .reply-glued-label{margin-right:.5em}.Status .reply-to-popover .reply-to:hover:before{content:"";display:block;position:absolute;bottom:0;width:100%;border-bottom:1px solid var(--faint);pointer-events:none}.Status .reply-to-popover .faint-link:hover{text-decoration:none}.Status .reply-to-popover.-strikethrough .reply-to:after{content:"";display:block;position:absolute;top:50%;width:100%;border-bottom:1px solid var(--faint);pointer-events:none}.Status .mentions,.Status .reply-to{white-space:nowrap;position:relative}.Status .mentions-text,.Status .reply-to-text{color:var(--faint);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.Status .mentions-line{display:inline}.Status .replies{margin-top:.25em;line-height:1.3;font-size:.85em;display:-webkit-box;display:flex;flex-wrap:wrap}.Status .replies>*{margin-right:.4em}.Status .reply-link{height:17px}.Status .repeat-info{padding:.4em var(--status-margin)}.Status .repeat-info .repeat-icon{color:var(--cGreen)}.Status .repeater-avatar{border-radius:var(--roundness);margin-left:2em;width:1.5em;height:1.5em}.Status .repeater-name{text-overflow:ellipsis;margin-right:0}.Status .repeater-name .emoji{width:1em;height:1em;vertical-align:middle;object-fit:contain}.Status .status-fadein{-webkit-animation-duration:.4s;animation-duration:.4s;-webkit-animation-name:fadein;animation-name:fadein}@-webkit-keyframes fadein{0%{opacity:0}to{opacity:1}}@keyframes fadein{0%{opacity:0}to{opacity:1}}.Status .status-actions{position:relative;width:100%;display:grid;grid-template-columns:1fr;grid-auto-columns:1fr;grid-auto-flow:column;margin-top:var(--status-margin)}.Status .muted{padding:.25em .6em;height:1.2em;line-height:1.2em;text-overflow:ellipsis;overflow:hidden;display:-webkit-box;display:flex;flex-wrap:nowrap;gap:1ex}.Status .muted .status-username,.Status .muted .mute-reason{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.Status .muted .status-username{font-weight:400;-webkit-box-flex:0;flex:0 1 auto;margin-right:.2em;font-size:smaller;display:-webkit-box;display:flex}.Status .muted .unmute{-webkit-box-flex:0;flex:0 0 auto;margin-left:auto;display:block}.Status .reply-form{padding-top:0;padding-bottom:0}.Status .reply-body{-webkit-box-flex:1;flex:1}.Status .favs-repeated-users{margin-top:var(--status-margin)}.Status .stats{width:100%;display:-webkit-box;display:flex;line-height:1em}.Status .avatar-row{-webkit-box-flex:1;flex:1;position:relative;display:-webkit-box;display:flex;-webkit-box-align:center;align-items:center;overflow:hidden}.Status .avatar-row:before{content:"";position:absolute;height:100%;width:1px;left:0;background-color:var(--textFaint)}.Status .stat-count{margin-right:var(--status-margin);-webkit-user-select:none;user-select:none}.Status .stat-count .stat-title{color:var(--textFaint);font-size:.85em;text-transform:uppercase;position:relative}.Status .stat-count .stat-number{font-weight:bolder;font-size:1.1em;line-height:1em;color:var(--text)}.Status .stat-count:hover .stat-title{text-decoration:underline}@media all and (width <= 800px){.Status .repeater-avatar{margin-left:20px}.Status .post-avatar{width:40px;height:40px}.Status .post-avatar.-compact{width:32px;height:32px}}.Status .quoted-status{margin-top:.5em;border:1px solid var(--border);border-radius:var(--roundness)}.Status .quoted-status.-unavailable-prompt{padding:.5em}.Status .display-quoted-status-button{margin:.5em}.Status .display-quoted-status-button-icon{color:inherit}.Report .report-content,.Report .report-state{margin:.5em 0 1em}.Report .reported-status{border:1px solid var(--border);border-radius:var(--roundness);display:block;padding:.5em;margin:.5em 0}.Report .reported-status .status-content{pointer-events:none}.Report .reported-status .reported-status-heading{display:-webkit-box;display:flex;width:100%;-webkit-box-pack:justify;justify-content:space-between;margin-bottom:.2em}.Report .reported-status .reported-status-name{font-weight:700}.Report .note{width:100%;margin-bottom:.5em}.Notification{border-bottom:1px solid;border-color:var(--border);overflow-wrap:break-word;text-wrap:pretty;--emoji-size: 1em}.Notification .status-content{cursor:pointer}.Notification.Status{background-color:transparent!important}.Notification:hover{--_still-image-img-visibility: visible;--_still-image-canvas-visibility: hidden;--_still-image-label-visibility: hidden}.Notification.-muted{padding:.25em .6em;height:1.2em;line-height:1.2em;text-overflow:ellipsis;overflow:hidden;display:-webkit-box;display:flex;flex-wrap:nowrap;gap:1ex}.Notification.-muted .status-username,.Notification.-muted .mute-thread,.Notification.-muted .mute-words{white-space:nowrap}.Notification.-muted .status-username,.Notification.-muted .mute-words{text-overflow:ellipsis;overflow:hidden}.Notification.-muted .status-username{font-weight:400;-webkit-box-flex:0;flex:0 1 auto;margin-right:.2em;font-size:smaller}.Notification.-muted .mute-thread{-webkit-box-flex:0;flex:0 0 auto}.Notification.-muted .mute-words{-webkit-box-flex:1;flex:1 0 5em;margin-left:.2em}.Notification.-muted .mute-words:before{content:" "}.Notification.-muted .unmute{-webkit-box-flex:0;flex:0 0 auto;margin-left:auto;display:block}.Notification .type-icon{margin:0 .1em}.Notification.-type--repeat .type-icon{color:var(--cGreen)}.Notification.-type--follow .type-icon,.Notification.-type--follow-request .type-icon{color:var(--cBlue)}.Notification.-type--like .type-icon{color:var(--cOrange)}.Notification.-type--move .type-icon{color:var(--cBlue)}.ExtraNotifications{width:100%;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;-webkit-box-align:stretch;align-items:stretch}.ExtraNotifications .notification{width:100%;border-bottom:1px solid;border-color:var(--border);display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;-webkit-box-align:stretch;align-items:stretch}.ExtraNotifications .extra-notification{padding:1em}.ExtraNotifications .icon{margin-right:.5em}.ExtraNotifications .tip{display:inline}.Notifications:not(.minimal){padding-bottom:15em}.Notifications .loadmore-error{color:var(--text)}.Notifications .notification{position:relative}.Notifications .notification .notification-overlay{position:absolute;top:0;right:0;bottom:0;left:0;pointer-events:none}.Notifications .notification.unseen .notification-overlay{background-image:-webkit-linear-gradient(315deg,var(--badgeNotification) 4px,transparent 10px);background-image:linear-gradient(135deg,var(--badgeNotification) 4px,transparent 10px)}.notification{box-sizing:border-box}.notification .Status{-webkit-box-flex:1;flex:1}.notification:hover .animated.Avatar canvas{display:none}.notification:hover .animated.Avatar img{visibility:visible}.notification:last-child .Notification{border-bottom:none}.notification .non-mention{display:-webkit-box;display:flex;-webkit-box-flex:1;flex:1;flex-wrap:nowrap;padding:.6em;min-width:0}.notification .non-mention .avatar-container{width:32px;height:32px}.notification .follow-request-accept:hover{color:var(--text)}.notification .follow-request-reject:hover{color:var(--cRed)}.notification .follow-text,.notification .move-text{padding:.5em 0;overflow-wrap:break-word;display:-webkit-box;display:flex;-webkit-box-pack:justify;justify-content:space-between}.notification .follow-text .follow-name,.notification .move-text .follow-name{display:block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.notification time{white-space:nowrap}.notification .notification-right{-webkit-box-flex:1;flex:1;padding-left:.8em;min-width:0}.notification .notification-right .timeago{min-width:3em;text-align:right}.notification .notification-right .timeago-link{margin-right:.2em}.notification .notification-right .expand-icon .svg-inline--fa{margin-left:.25em}.notification .emoji-reaction-emoji{font-size:1.3em;max-width:1.25em;height:1.25em;width:auto}.notification .emoji-reaction-emoji-image{vertical-align:middle;object-fit:contain}.notification .notification-details{min-width:0;line-height:var(--post-line-height);position:relative;overflow:hidden;width:100%;-webkit-box-flex:1;flex:1 1 0;display:-webkit-box;display:flex;flex-wrap:nowrap;-webkit-box-pack:justify;justify-content:space-between}.notification .notification-details .name-and-action{-webkit-box-flex:1;flex:1;overflow:hidden;text-overflow:ellipsis}.notification .notification-details .username{font-weight:bolder;max-width:100%;text-overflow:ellipsis;white-space:nowrap}.notification .notification-details .timeago{margin-right:.2em}.notification .notification-details .status-content{margin:0;max-height:300px}.notification .notification-details h1{overflow-wrap:break-word;text-wrap:pretty;margin:0 0 .3em;padding:0;font-size:1em;line-height:1.5}.notification .notification-details h1 small{font-weight:lighter}.notification .notification-details p{margin:0 0 .3em}.MobileNav{z-index:var(--ZI_navbar)}.MobileNav .mobile-nav{display:grid;line-height:var(--navbar-height);grid-template-rows:var(--navbar-height);grid-template-columns:2fr auto;width:100%;box-sizing:border-box}.MobileNav .mobile-nav a{color:var(--link)}.MobileNav .mobile-inner-nav{width:100%;display:-webkit-box;display:flex;-webkit-box-align:center;align-items:center}.MobileNav .mobile-nav-button{display:inline-block;text-align:center;padding:0 1em;position:relative;cursor:pointer}.MobileNav .site-name{padding:0 .3em;display:inline-block}.MobileNav .item{display:-webkit-box;display:flex}.MobileNav .mobile-notifications-drawer{width:100%;height:100vh;overflow-x:hidden;position:fixed;top:0;left:0;box-shadow:var(--shadow);-webkit-transition-property:-webkit-transform;transition-property:-webkit-transform;transition-property:transform;transition-property:transform,-webkit-transform;-webkit-transition-duration:.25s;transition-duration:.25s;-webkit-transform:translateX(0);transform:translate(0);z-index:var(--ZI_navbar);-webkit-overflow-scrolling:touch;background:var(--background)}.MobileNav .mobile-notifications-drawer.-closed{-webkit-transform:translateX(100%);transform:translate(100%);box-shadow:none}.MobileNav .mobile-notifications-header{display:-webkit-box;display:flex;-webkit-box-align:center;align-items:center;-webkit-box-pack:justify;justify-content:space-between;z-index:calc(var(--ZI_navbar) + 100);width:100%;height:3.5em;line-height:3.5em;position:absolute;box-shadow:var(--shadow)}.MobileNav .mobile-notifications-header .spacer{-webkit-box-flex:1;flex:1}.MobileNav .mobile-notifications-header .title{font-size:1.3em;margin-left:.6em;white-space:nowrap;overflow-x:hidden;text-overflow:ellipsis}.MobileNav .pins{-webkit-box-flex:1;flex:1}.MobileNav .pins .pinned-item{-webkit-box-flex:1;flex-grow:1}.MobileNav .mobile-notifications{margin-top:3.5em;width:100vw;height:calc(100vh - var(--navbar-height));overflow:hidden scroll}.MobileNav .mobile-notifications .notifications{padding:0;border-radius:0;box-shadow:none}.MobileNav .mobile-notifications .notifications .panel{border-radius:0;margin:0;box-shadow:none}.MobileNav .mobile-notifications .notifications .panel:after{border-radius:0}.MobileNav .mobile-notifications .notifications .panel .panel-heading{border-radius:0;box-shadow:none}.MobileNav .confirm-modal.dark-overlay:before{z-index:3000}.MobileNav .confirm-modal.dark-overlay .dialog-modal.panel{z-index:3001}.SearchBar{display:-webkit-inline-box;display:inline-flex;-webkit-box-align:baseline;align-items:baseline;vertical-align:baseline;-webkit-box-pack:end;justify-content:flex-end}.SearchBar.-expanded{width:100%}.SearchBar .search-bar-input,.SearchBar .search-button{height:29px}.SearchBar .search-bar-input{-webkit-box-flex:1;flex:1 0 auto}.SearchBar .cancel-search{height:50px}.SearchBar .cancel-icon{color:var(--text)}.DesktopNav{width:100%;z-index:var(--ZI_navbar)}.DesktopNav input{color:var(--inputTopbarText, var(--inputText))}.DesktopNav .inner-nav{display:grid;grid-template-rows:var(--navbar-height);grid-template-columns:2fr auto 2fr;grid-template-areas:"sitename logo actions";box-sizing:border-box;padding:0 1.2em;margin:auto;max-width:980px}.DesktopNav.-column-stretch .inner-nav{--miniColumn: 25rem;--maxiColumn: 45rem;--columnGap: 1em;max-width:calc(var(--sidebarColumnWidth, var(--miniColumn)) + var(--contentColumnWidth, var(--maxiColumn)) + var(--columnGap))}.DesktopNav.-logoLeft .inner-nav{grid-template-columns:auto 2fr 2fr;grid-template-areas:"logo sitename actions"}.DesktopNav.-column-stretch.-wide .inner-nav{max-width:calc(var(--sidebarColumnWidth, var(--miniColumn)) + var(--contentColumnWidth, var(--maxiColumn)) + var(--notifsColumnWidth, var(--miniColumn)) + var(--columnGap))}.DesktopNav .button-default,.DesktopNav .button-default svg{color:var(--text)}.DesktopNav .logo{grid-area:logo;position:relative;-webkit-transition:opacity;transition:opacity;-webkit-transition-timing-function:ease-out;transition-timing-function:ease-out;-webkit-transition-duration:.1s;transition-duration:.1s}@media all and (width >= 800px){.DesktopNav .logo{opacity:1!important}}.DesktopNav .logo .mask{-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-position:center;mask-position:center;-webkit-mask-size:contain;mask-size:contain;background-color:var(--text);position:absolute;top:0;right:0;bottom:0;left:0}.DesktopNav .logo img{display:inline-block;height:var(--navbar-height)}.DesktopNav .nav-icon{margin-left:.2em;width:2em;height:100%;text-align:center}.DesktopNav .sitename{grid-area:sitename}.DesktopNav .actions{grid-area:actions}.DesktopNav .item{-webkit-box-flex:1;flex:1;line-height:var(--navbar-height);height:var(--navbar-height);overflow:hidden;display:-webkit-box;display:flex;flex-wrap:wrap}.DesktopNav .item.right{-webkit-box-pack:end;justify-content:flex-end;text-align:right}.DesktopNav .spacer{width:1em}.user-reporting-panel{width:90vw;max-width:50rem;min-height:20vh;max-height:80vh}.user-reporting-panel .user-link{display:inline}.user-reporting-panel .panel-body{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:reverse;flex-direction:column-reverse;border-top:1px solid;border-color:var(--border);overflow:hidden}.user-reporting-panel-left{padding:1.1em .7em .7em;line-height:var(--post-line-height);box-sizing:border-box}.user-reporting-panel-left>div{margin-bottom:1em}.user-reporting-panel-left>div:last-child{margin-bottom:0}.user-reporting-panel-left p{margin-top:0}.user-reporting-panel-left textarea.form-control{line-height:1.1;resize:none;overflow:hidden;-webkit-transition:min-height .2s .1s;transition:min-height .2s .1s;min-height:44px;width:100%}.user-reporting-panel-left .btn{min-width:10em;padding:0 2em}.user-reporting-panel-left .alert{margin:1em 0 0;line-height:1.3em}.user-reporting-panel-right{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;overflow-y:auto}.user-reporting-panel-sitem{display:-webkit-box;display:flex;-webkit-box-pack:justify;justify-content:space-between}.user-reporting-panel-sitem>.Status{-webkit-box-flex:1;flex:1}.user-reporting-panel-sitem>.checkbox{margin:.75em}@media all and (width >= 801px){.user-reporting-panel .panel-body{-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-direction:row}.user-reporting-panel-left{width:50%;max-width:320px;border-right:1px solid;border-color:var(--border);padding:1.1em}.user-reporting-panel-left>div{margin-bottom:2em}.user-reporting-panel-right{width:50%;-webkit-box-flex:1;flex:1 1 auto;margin-bottom:12px}}.modal-view.edit-form-modal-view{-webkit-box-align:start;align-items:flex-start}.edit-form-modal-panel{flex-shrink:0;margin-top:25%;margin-bottom:2em;width:100%;max-width:700px}@media (orientation: landscape){.edit-form-modal-panel{margin-top:8%}}.edit-form-modal-panel .form-bottom-left{max-width:6.5em}.edit-form-modal-panel .form-bottom-left .emoji-icon{-webkit-box-pack:right;justify-content:right}.modal-view.post-form-modal-view{-webkit-box-align:start;align-items:flex-start}.post-form-modal-panel{flex-shrink:0;margin-top:25%;margin-bottom:2em;width:100%;max-width:700px}@media (orientation: landscape){.post-form-modal-panel{margin-top:8%}}.modal-view.status-history-modal-view{-webkit-box-align:start;align-items:flex-start}.status-history-modal-panel{flex-shrink:0;margin-top:25%;margin-bottom:2em;width:100%;max-width:700px}@media (orientation: landscape){.status-history-modal-panel{margin-top:8%}}.global-notice-list{position:fixed;top:calc(var(--navbar-height) + .5em);width:100%;pointer-events:none;z-index:var(--ZI_modals_popovers);display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;-webkit-box-align:center;align-items:center}.global-notice-list .global-notice{pointer-events:auto;text-align:center;width:40em;max-width:calc(100% - 3em);display:-webkit-box;display:flex;padding-left:1.5em;line-height:2;margin-bottom:.5em}.global-notice-list .global-notice .notice-message{-webkit-box-flex:1;flex:1 1 100%}.global-notice-list .close-notice{padding-right:.2em}:root,:host{--fa-font-solid: normal 900 1em/1 "Font Awesome 6 Free";--fa-font-regular: normal 400 1em/1 "Font Awesome 6 Free";--fa-font-light: normal 300 1em/1 "Font Awesome 6 Pro";--fa-font-thin: normal 100 1em/1 "Font Awesome 6 Pro";--fa-font-duotone: normal 900 1em/1 "Font Awesome 6 Duotone";--fa-font-duotone-regular: normal 400 1em/1 "Font Awesome 6 Duotone";--fa-font-duotone-light: normal 300 1em/1 "Font Awesome 6 Duotone";--fa-font-duotone-thin: normal 100 1em/1 "Font Awesome 6 Duotone";--fa-font-brands: normal 400 1em/1 "Font Awesome 6 Brands";--fa-font-sharp-solid: normal 900 1em/1 "Font Awesome 6 Sharp";--fa-font-sharp-regular: normal 400 1em/1 "Font Awesome 6 Sharp";--fa-font-sharp-light: normal 300 1em/1 "Font Awesome 6 Sharp";--fa-font-sharp-thin: normal 100 1em/1 "Font Awesome 6 Sharp";--fa-font-sharp-duotone-solid: normal 900 1em/1 "Font Awesome 6 Sharp Duotone";--fa-font-sharp-duotone-regular: normal 400 1em/1 "Font Awesome 6 Sharp Duotone";--fa-font-sharp-duotone-light: normal 300 1em/1 "Font Awesome 6 Sharp Duotone";--fa-font-sharp-duotone-thin: normal 100 1em/1 "Font Awesome 6 Sharp Duotone"}svg.svg-inline--fa:not(:root),svg.svg-inline--fa:not(:host){overflow:visible;box-sizing:content-box}.svg-inline--fa{display:var(--fa-display, inline-block);height:1em;overflow:visible;vertical-align:-.125em}.svg-inline--fa.fa-2xs{vertical-align:.1em}.svg-inline--fa.fa-xs{vertical-align:0em}.svg-inline--fa.fa-sm{vertical-align:-.07143em}.svg-inline--fa.fa-lg{vertical-align:-.2em}.svg-inline--fa.fa-xl{vertical-align:-.25em}.svg-inline--fa.fa-2xl{vertical-align:-.3125em}.svg-inline--fa.fa-pull-left{margin-right:var(--fa-pull-margin, .3em);width:auto}.svg-inline--fa.fa-pull-right{margin-left:var(--fa-pull-margin, .3em);width:auto}.svg-inline--fa.fa-li{width:var(--fa-li-width, 2em);top:.25em}.svg-inline--fa.fa-fw{width:var(--fa-fw-width, 1.25em)}.fa-layers svg.svg-inline--fa{bottom:0;left:0;margin:auto;position:absolute;right:0;top:0}.fa-layers-counter,.fa-layers-text{display:inline-block;position:absolute;text-align:center}.fa-layers{display:inline-block;height:1em;position:relative;text-align:center;vertical-align:-.125em;width:1em}.fa-layers svg.svg-inline--fa{-webkit-transform-origin:center center;transform-origin:center center}.fa-layers-text{left:50%;top:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);-webkit-transform-origin:center center;transform-origin:center center}.fa-layers-counter{background-color:var(--fa-counter-background-color, #ff253a);border-radius:var(--fa-counter-border-radius, 1em);box-sizing:border-box;color:var(--fa-inverse, #fff);line-height:var(--fa-counter-line-height, 1);max-width:var(--fa-counter-max-width, 5em);min-width:var(--fa-counter-min-width, 1.5em);overflow:hidden;padding:var(--fa-counter-padding, .25em .5em);right:var(--fa-right, 0);text-overflow:ellipsis;top:var(--fa-top, 0);-webkit-transform:scale(var(--fa-counter-scale, .25));transform:scale(var(--fa-counter-scale, .25));-webkit-transform-origin:top right;transform-origin:top right}.fa-layers-bottom-right{bottom:var(--fa-bottom, 0);right:var(--fa-right, 0);top:auto;-webkit-transform:scale(var(--fa-layers-scale, .25));transform:scale(var(--fa-layers-scale, .25));-webkit-transform-origin:bottom right;transform-origin:bottom right}.fa-layers-bottom-left{bottom:var(--fa-bottom, 0);left:var(--fa-left, 0);right:auto;top:auto;-webkit-transform:scale(var(--fa-layers-scale, .25));transform:scale(var(--fa-layers-scale, .25));-webkit-transform-origin:bottom left;transform-origin:bottom left}.fa-layers-top-right{top:var(--fa-top, 0);right:var(--fa-right, 0);-webkit-transform:scale(var(--fa-layers-scale, .25));transform:scale(var(--fa-layers-scale, .25));-webkit-transform-origin:top right;transform-origin:top right}.fa-layers-top-left{left:var(--fa-left, 0);right:auto;top:var(--fa-top, 0);-webkit-transform:scale(var(--fa-layers-scale, .25));transform:scale(var(--fa-layers-scale, .25));-webkit-transform-origin:top left;transform-origin:top left}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-2xs{font-size:.625em;line-height:.1em;vertical-align:.225em}.fa-xs{font-size:.75em;line-height:.08333em;vertical-align:.125em}.fa-sm{font-size:.875em;line-height:.07143em;vertical-align:.05357em}.fa-lg{font-size:1.25em;line-height:.05em;vertical-align:-.075em}.fa-xl{font-size:1.5em;line-height:.04167em;vertical-align:-.125em}.fa-2xl{font-size:2em;line-height:.03125em;vertical-align:-.1875em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:var(--fa-li-margin, 2.5em);padding-left:0}.fa-ul>li{position:relative}.fa-li{left:calc(-1 * var(--fa-li-width, 2em));position:absolute;text-align:center;width:var(--fa-li-width, 2em);line-height:inherit}.fa-border{border-color:var(--fa-border-color, #eee);border-radius:var(--fa-border-radius, .1em);border-style:var(--fa-border-style, solid);border-width:var(--fa-border-width, .08em);padding:var(--fa-border-padding, .2em .25em .15em)}.fa-pull-left{float:left;margin-right:var(--fa-pull-margin, .3em)}.fa-pull-right{float:right;margin-left:var(--fa-pull-margin, .3em)}.fa-beat{-webkit-animation-name:fa-beat;animation-name:fa-beat;-webkit-animation-delay:var(--fa-animation-delay, 0s);animation-delay:var(--fa-animation-delay, 0s);-webkit-animation-direction:var(--fa-animation-direction, normal);animation-direction:var(--fa-animation-direction, normal);-webkit-animation-duration:var(--fa-animation-duration, 1s);animation-duration:var(--fa-animation-duration, 1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count, infinite);animation-iteration-count:var(--fa-animation-iteration-count, infinite);-webkit-animation-timing-function:var(--fa-animation-timing, ease-in-out);animation-timing-function:var(--fa-animation-timing, ease-in-out)}.fa-bounce{-webkit-animation-name:fa-bounce;animation-name:fa-bounce;-webkit-animation-delay:var(--fa-animation-delay, 0s);animation-delay:var(--fa-animation-delay, 0s);-webkit-animation-direction:var(--fa-animation-direction, normal);animation-direction:var(--fa-animation-direction, normal);-webkit-animation-duration:var(--fa-animation-duration, 1s);animation-duration:var(--fa-animation-duration, 1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count, infinite);animation-iteration-count:var(--fa-animation-iteration-count, infinite);-webkit-animation-timing-function:var(--fa-animation-timing, cubic-bezier(.28, .84, .42, 1));animation-timing-function:var(--fa-animation-timing, cubic-bezier(.28, .84, .42, 1))}.fa-fade{-webkit-animation-name:fa-fade;animation-name:fa-fade;-webkit-animation-delay:var(--fa-animation-delay, 0s);animation-delay:var(--fa-animation-delay, 0s);-webkit-animation-direction:var(--fa-animation-direction, normal);animation-direction:var(--fa-animation-direction, normal);-webkit-animation-duration:var(--fa-animation-duration, 1s);animation-duration:var(--fa-animation-duration, 1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count, infinite);animation-iteration-count:var(--fa-animation-iteration-count, infinite);-webkit-animation-timing-function:var(--fa-animation-timing, cubic-bezier(.4, 0, .6, 1));animation-timing-function:var(--fa-animation-timing, cubic-bezier(.4, 0, .6, 1))}.fa-beat-fade{-webkit-animation-name:fa-beat-fade;animation-name:fa-beat-fade;-webkit-animation-delay:var(--fa-animation-delay, 0s);animation-delay:var(--fa-animation-delay, 0s);-webkit-animation-direction:var(--fa-animation-direction, normal);animation-direction:var(--fa-animation-direction, normal);-webkit-animation-duration:var(--fa-animation-duration, 1s);animation-duration:var(--fa-animation-duration, 1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count, infinite);animation-iteration-count:var(--fa-animation-iteration-count, infinite);-webkit-animation-timing-function:var(--fa-animation-timing, cubic-bezier(.4, 0, .6, 1));animation-timing-function:var(--fa-animation-timing, cubic-bezier(.4, 0, .6, 1))}.fa-flip{-webkit-animation-name:fa-flip;animation-name:fa-flip;-webkit-animation-delay:var(--fa-animation-delay, 0s);animation-delay:var(--fa-animation-delay, 0s);-webkit-animation-direction:var(--fa-animation-direction, normal);animation-direction:var(--fa-animation-direction, normal);-webkit-animation-duration:var(--fa-animation-duration, 1s);animation-duration:var(--fa-animation-duration, 1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count, infinite);animation-iteration-count:var(--fa-animation-iteration-count, infinite);-webkit-animation-timing-function:var(--fa-animation-timing, ease-in-out);animation-timing-function:var(--fa-animation-timing, ease-in-out)}.fa-shake{-webkit-animation-name:fa-shake;animation-name:fa-shake;-webkit-animation-delay:var(--fa-animation-delay, 0s);animation-delay:var(--fa-animation-delay, 0s);-webkit-animation-direction:var(--fa-animation-direction, normal);animation-direction:var(--fa-animation-direction, normal);-webkit-animation-duration:var(--fa-animation-duration, 1s);animation-duration:var(--fa-animation-duration, 1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count, infinite);animation-iteration-count:var(--fa-animation-iteration-count, infinite);-webkit-animation-timing-function:var(--fa-animation-timing, linear);animation-timing-function:var(--fa-animation-timing, linear)}.fa-spin{-webkit-animation-name:fa-spin;animation-name:fa-spin;-webkit-animation-delay:var(--fa-animation-delay, 0s);animation-delay:var(--fa-animation-delay, 0s);-webkit-animation-direction:var(--fa-animation-direction, normal);animation-direction:var(--fa-animation-direction, normal);-webkit-animation-duration:var(--fa-animation-duration, 2s);animation-duration:var(--fa-animation-duration, 2s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count, infinite);animation-iteration-count:var(--fa-animation-iteration-count, infinite);-webkit-animation-timing-function:var(--fa-animation-timing, linear);animation-timing-function:var(--fa-animation-timing, linear)}.fa-spin-reverse{--fa-animation-direction: reverse}.fa-pulse,.fa-spin-pulse{-webkit-animation-name:fa-spin;animation-name:fa-spin;-webkit-animation-direction:var(--fa-animation-direction, normal);animation-direction:var(--fa-animation-direction, normal);-webkit-animation-duration:var(--fa-animation-duration, 1s);animation-duration:var(--fa-animation-duration, 1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count, infinite);animation-iteration-count:var(--fa-animation-iteration-count, infinite);-webkit-animation-timing-function:var(--fa-animation-timing, steps(8));animation-timing-function:var(--fa-animation-timing, steps(8))}@media (prefers-reduced-motion: reduce){.fa-beat,.fa-bounce,.fa-fade,.fa-beat-fade,.fa-flip,.fa-pulse,.fa-shake,.fa-spin,.fa-spin-pulse{-webkit-animation-delay:-1ms;animation-delay:-1ms;-webkit-animation-duration:1ms;animation-duration:1ms;-webkit-animation-iteration-count:1;animation-iteration-count:1;-webkit-transition-delay:0s;transition-delay:0s;-webkit-transition-duration:0s;transition-duration:0s}}@-webkit-keyframes fa-beat{0%,90%{-webkit-transform:scale(1);transform:scale(1)}45%{-webkit-transform:scale(var(--fa-beat-scale, 1.25));transform:scale(var(--fa-beat-scale, 1.25))}}@keyframes fa-beat{0%,90%{-webkit-transform:scale(1);transform:scale(1)}45%{-webkit-transform:scale(var(--fa-beat-scale, 1.25));transform:scale(var(--fa-beat-scale, 1.25))}}@-webkit-keyframes fa-bounce{0%{-webkit-transform:scale(1,1) translateY(0);transform:scale(1) translateY(0)}10%{-webkit-transform:scale(var(--fa-bounce-start-scale-x, 1.1),var(--fa-bounce-start-scale-y, .9)) translateY(0);transform:scale(var(--fa-bounce-start-scale-x, 1.1),var(--fa-bounce-start-scale-y, .9)) translateY(0)}30%{-webkit-transform:scale(var(--fa-bounce-jump-scale-x, .9),var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -.5em));transform:scale(var(--fa-bounce-jump-scale-x, .9),var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -.5em))}50%{-webkit-transform:scale(var(--fa-bounce-land-scale-x, 1.05),var(--fa-bounce-land-scale-y, .95)) translateY(0);transform:scale(var(--fa-bounce-land-scale-x, 1.05),var(--fa-bounce-land-scale-y, .95)) translateY(0)}57%{-webkit-transform:scale(1,1) translateY(var(--fa-bounce-rebound, -.125em));transform:scale(1) translateY(var(--fa-bounce-rebound, -.125em))}64%{-webkit-transform:scale(1,1) translateY(0);transform:scale(1) translateY(0)}to{-webkit-transform:scale(1,1) translateY(0);transform:scale(1) translateY(0)}}@keyframes fa-bounce{0%{-webkit-transform:scale(1,1) translateY(0);transform:scale(1) translateY(0)}10%{-webkit-transform:scale(var(--fa-bounce-start-scale-x, 1.1),var(--fa-bounce-start-scale-y, .9)) translateY(0);transform:scale(var(--fa-bounce-start-scale-x, 1.1),var(--fa-bounce-start-scale-y, .9)) translateY(0)}30%{-webkit-transform:scale(var(--fa-bounce-jump-scale-x, .9),var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -.5em));transform:scale(var(--fa-bounce-jump-scale-x, .9),var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -.5em))}50%{-webkit-transform:scale(var(--fa-bounce-land-scale-x, 1.05),var(--fa-bounce-land-scale-y, .95)) translateY(0);transform:scale(var(--fa-bounce-land-scale-x, 1.05),var(--fa-bounce-land-scale-y, .95)) translateY(0)}57%{-webkit-transform:scale(1,1) translateY(var(--fa-bounce-rebound, -.125em));transform:scale(1) translateY(var(--fa-bounce-rebound, -.125em))}64%{-webkit-transform:scale(1,1) translateY(0);transform:scale(1) translateY(0)}to{-webkit-transform:scale(1,1) translateY(0);transform:scale(1) translateY(0)}}@-webkit-keyframes fa-fade{50%{opacity:var(--fa-fade-opacity, .4)}}@keyframes fa-fade{50%{opacity:var(--fa-fade-opacity, .4)}}@-webkit-keyframes fa-beat-fade{0%,to{opacity:var(--fa-beat-fade-opacity, .4);-webkit-transform:scale(1);transform:scale(1)}50%{opacity:1;-webkit-transform:scale(var(--fa-beat-fade-scale, 1.125));transform:scale(var(--fa-beat-fade-scale, 1.125))}}@keyframes fa-beat-fade{0%,to{opacity:var(--fa-beat-fade-opacity, .4);-webkit-transform:scale(1);transform:scale(1)}50%{opacity:1;-webkit-transform:scale(var(--fa-beat-fade-scale, 1.125));transform:scale(var(--fa-beat-fade-scale, 1.125))}}@-webkit-keyframes fa-flip{50%{-webkit-transform:rotate3d(var(--fa-flip-x, 0),var(--fa-flip-y, 1),var(--fa-flip-z, 0),var(--fa-flip-angle, -180deg));transform:rotate3d(var(--fa-flip-x, 0),var(--fa-flip-y, 1),var(--fa-flip-z, 0),var(--fa-flip-angle, -180deg))}}@keyframes fa-flip{50%{-webkit-transform:rotate3d(var(--fa-flip-x, 0),var(--fa-flip-y, 1),var(--fa-flip-z, 0),var(--fa-flip-angle, -180deg));transform:rotate3d(var(--fa-flip-x, 0),var(--fa-flip-y, 1),var(--fa-flip-z, 0),var(--fa-flip-angle, -180deg))}}@-webkit-keyframes fa-shake{0%{-webkit-transform:rotate(-15deg);transform:rotate(-15deg)}4%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}8%,24%{-webkit-transform:rotate(-18deg);transform:rotate(-18deg)}12%,28%{-webkit-transform:rotate(18deg);transform:rotate(18deg)}16%{-webkit-transform:rotate(-22deg);transform:rotate(-22deg)}20%{-webkit-transform:rotate(22deg);transform:rotate(22deg)}32%{-webkit-transform:rotate(-12deg);transform:rotate(-12deg)}36%{-webkit-transform:rotate(12deg);transform:rotate(12deg)}40%,to{-webkit-transform:rotate(0deg);transform:rotate(0)}}@keyframes fa-shake{0%{-webkit-transform:rotate(-15deg);transform:rotate(-15deg)}4%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}8%,24%{-webkit-transform:rotate(-18deg);transform:rotate(-18deg)}12%,28%{-webkit-transform:rotate(18deg);transform:rotate(18deg)}16%{-webkit-transform:rotate(-22deg);transform:rotate(-22deg)}20%{-webkit-transform:rotate(22deg);transform:rotate(22deg)}32%{-webkit-transform:rotate(-12deg);transform:rotate(-12deg)}36%{-webkit-transform:rotate(12deg);transform:rotate(12deg)}40%,to{-webkit-transform:rotate(0deg);transform:rotate(0)}}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.fa-rotate-90{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-webkit-transform:scale(-1,1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scale(1,-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1,-1);transform:scale(-1)}.fa-rotate-by{-webkit-transform:rotate(var(--fa-rotate-angle, 0));transform:rotate(var(--fa-rotate-angle, 0))}.fa-stack{display:inline-block;vertical-align:middle;height:2em;position:relative;width:2.5em}.fa-stack-1x,.fa-stack-2x{bottom:0;left:0;margin:auto;position:absolute;right:0;top:0;z-index:var(--fa-stack-z-index, auto)}.svg-inline--fa.fa-stack-1x{height:1em;width:1.25em}.svg-inline--fa.fa-stack-2x{height:2em;width:2.5em}.fa-inverse{color:var(--fa-inverse, #fff)}.sr-only,.fa-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.sr-only-focusable:not(:focus),.fa-sr-only-focusable:not(:focus){position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.svg-inline--fa .fa-primary{fill:var(--fa-primary-color, currentColor);opacity:var(--fa-primary-opacity, 1)}.svg-inline--fa .fa-secondary{fill:var(--fa-secondary-color, currentColor);opacity:var(--fa-secondary-opacity, .4)}.svg-inline--fa.fa-swap-opacity .fa-primary{opacity:var(--fa-secondary-opacity, .4)}.svg-inline--fa.fa-swap-opacity .fa-secondary{opacity:var(--fa-primary-opacity, 1)}.svg-inline--fa mask .fa-primary,.svg-inline--fa mask .fa-secondary{fill:#000}pinch-zoom{display:block;overflow:hidden;touch-action:none;--scale: 1;--x: 0;--y: 0}pinch-zoom>*{-webkit-transform:translate(var(--x),var(--y)) scale(var(--scale));transform:translate(var(--x),var(--y)) scale(var(--scale));-webkit-transform-origin:0 0;transform-origin:0 0;will-change:transform}.panel{--__panel-background: var(--background);--__panel-backdrop-filter: var(--backdrop-filter);--_shadow: var(--shadow);position:relative;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column}.panel .tab-switcher .tabs{background:var(--__panel-background);-webkit-backdrop-filter:var(--__panel-backdrop-filter);backdrop-filter:var(--__panel-backdrop-filter)}.panel .panel-heading{background-color:inherit}.panel:after,.panel{border-radius:var(--roundness)}.panel:after{content:"";position:absolute;top:0;right:0;bottom:0;left:0;z-index:5;box-shadow:var(--_shadow);pointer-events:none}.panel-body{padding:var(--panel-body-padding, 0);background:var(--background);-webkit-backdrop-filter:var(--__panel-backdrop-filter);backdrop-filter:var(--__panel-backdrop-filter)}.panel-body .tab-switcher .tabs{background:none;-webkit-backdrop-filter:none;backdrop-filter:none}.panel-body:empty:before{content:"¯\\_(ツ)_/¯";display:block;padding:1em;text-align:center}.panel-body>p{line-height:1.3;padding:1em;margin:0}.panel-heading,.panel-footer{--panel-heading-height-padding: calc(var(--panel-header-height) * .2);--__panel-heading-gap: calc(var(--panel-header-height) * .1565);--__panel-heading-height: var(--panel-header-height);--__panel-heading-height-inner: calc(var(--__panel-heading-height) - 2 * var(--panel-heading-height-padding, 0));font-size:calc(var(--panelHeaderSize) / 3.2);-webkit-backdrop-filter:var(--__panel-backdrop-filter);backdrop-filter:var(--__panel-backdrop-filter);position:relative;box-sizing:border-box;display:grid;grid-auto-flow:column;grid-template-columns:minmax(50%,1fr);grid-auto-columns:auto;grid-column-gap:var(--__panel-heading-gap);-webkit-box-flex:0;flex:none;background-size:cover;padding:var(--panel-heading-height-padding);height:var(--__panel-heading-height);line-height:var(--__panel-heading-height-inner);z-index:4}.panel-heading.-flexible-height,.panel-footer.-flexible-height{--__panel-heading-height: auto}.panel-heading.-flexible-height:after,.panel-heading.-flexible-height:before,.panel-footer.-flexible-height:after,.panel-footer.-flexible-height:before{display:none}.panel-heading.-stub,.panel-heading.-stub:after,.panel-footer.-stub,.panel-footer.-stub:after{border-radius:var(--roundness)}.panel-heading.-sticky,.panel-footer.-sticky{position:-webkit-sticky;position:sticky;top:var(--navbar-height)}.panel-heading:after,.panel-heading:before,.panel-footer:after,.panel-footer:before{content:"";position:absolute;top:0;right:0;bottom:0;left:0;pointer-events:none}.panel-heading .title,.panel-footer .title{font-size:1.3em;margin:0;font-weight:400}.panel-heading .alert,.panel-footer .alert{white-space:nowrap;text-overflow:ellipsis;overflow-x:hidden}.panel-heading:not(.-flexible-height)>.button-default,.panel-heading:not(.-flexible-height)>.alert,.panel-footer:not(.-flexible-height)>.button-default,.panel-footer:not(.-flexible-height)>.alert{height:var(--__panel-heading-height-inner);min-height:0;box-sizing:border-box;margin:0;min-width:1px;padding-top:0;padding-bottom:0;align-self:stretch}.panel-heading:not(.-flexible-height)>.alert,.panel-footer:not(.-flexible-height)>.alert{line-height:calc(var(--__panel-heading-height-inner) - 2px)}.panel-heading{border-radius:var(--roundness) var(--roundness) 0 0;border-width:0 0 1px;-webkit-box-align:start;align-items:start;background-image:-webkit-linear-gradient(top,var(--background),var(--background)),-webkit-linear-gradient(top,var(--__panel-background),var(--__panel-background));background-image:linear-gradient(to bottom,var(--background),var(--background)),linear-gradient(to bottom,var(--__panel-background),var(--__panel-background));--_shadow: var(--shadow)}.panel-heading:after{background-color:var(--background);z-index:-2;border-radius:var(--roundness) var(--roundness) 0 0;box-shadow:var(--_shadow)}.panel-heading:not(.-flexible-height)>.button-default{flex-shrink:0}.panel-heading .rightside-button{align-self:stretch;text-align:center;width:var(--__panel-heading-height);height:var(--__panel-heading-height);margin:calc(-1 * var(--panel-heading-height-padding)) 0;margin-right:calc(-1 * var(--__panel-heading-gap))}.panel-heading .rightside-button>button{box-sizing:border-box;padding:calc(1 * var(--panel-heading-height-padding)) 0;height:100%;width:100%;text-align:center}.panel-heading .rightside-button>button svg{font-size:1.2em}.panel-heading .rightside-icon{align-self:stretch;text-align:center;width:var(--__panel-heading-height);margin-right:calc(-1 * var(--__panel-heading-gap))}.panel-heading .rightside-icon svg{font-size:1.2em}.panel-footer{border-top-left-radius:0;border-top-right-radius:0;-webkit-box-align:center;align-items:center;border-width:1px 0 0;border-style:solid;border-color:var(--border);background-color:var(--__panel-background)}:root{--status-margin: .75em;--post-line-height: 1.4;--ZI_media_modal: 9000;--ZI_modals_popovers: 8500;--ZI_modals: 8000;--ZI_navbar_popovers: 7500;--ZI_navbar: 7000;--ZI_popovers: 6000;--background: var(--bg)}html{font-size:var(--textSize, 1rem);--navbar-height: var(--navbarSize, 3.5rem);--emoji-size: var(--emojiSize, 32px);--panel-header-height: var(--panelHeaderSize, 3.2rem)}body{font-family:sans-serif;font-family:var(--font);margin:0;padding:0;color:var(--text);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;overscroll-behavior-y:none;overflow:clip scroll}body.hidden{display:none}@media (any-pointer: fine){*{scrollbar-color:var(--fg) transparent}*::-webkit-scrollbar{background:transparent}*::-webkit-scrollbar-corner{background:transparent}*::-webkit-resizer{background-color:transparent!important;background-image:-webkit-linear-gradient(315deg,transparent calc(50% - 1px),var(--textFaint) 50%,transparent calc(50% + 1px),transparent calc(75% - 1px),var(--textFaint) 75%,transparent calc(75% + 1px));background-image:linear-gradient(135deg,transparent calc(50% - 1px),var(--textFaint) 50%,transparent calc(50% + 1px),transparent calc(75% - 1px),var(--textFaint) 75%,transparent calc(75% + 1px))}*::-webkit-scrollbar-button,*::-webkit-scrollbar-thumb{box-shadow:var(--shadow);border-radius:var(--roundness)}*::-webkit-scrollbar-button{--___bgPadding: 2px;color:var(--text);background-repeat:no-repeat,no-repeat}*::-webkit-scrollbar-button:horizontal{background-size:50% calc(50% - var(--___bgPadding)),50% calc(50% - var(--___bgPadding))}*::-webkit-scrollbar-button:horizontal:increment{background-image:-webkit-linear-gradient(45deg,var(--text) 50%,transparent 51%),-webkit-linear-gradient(135deg,transparent 50%,var(--text) 51%);background-image:linear-gradient(45deg,var(--text) 50%,transparent 51%),linear-gradient(-45deg,transparent 50%,var(--text) 51%);background-position:top var(--___bgPadding) left 50%,right 50% bottom var(--___bgPadding)}*::-webkit-scrollbar-button:horizontal:decrement{background-image:-webkit-linear-gradient(45deg,transparent 50%,var(--text) calc(50% + 1px)),-webkit-linear-gradient(135deg,var(--text) 50%,transparent 51%);background-image:linear-gradient(45deg,transparent 50%,var(--text) calc(50% + 1px)),linear-gradient(-45deg,var(--text) 50%,transparent 51%);background-position:bottom var(--___bgPadding) right 50%,left 50% top var(--___bgPadding)}*::-webkit-scrollbar-button:vertical{background-size:calc(50% - var(--___bgPadding)) 50%,calc(50% - var(--___bgPadding)) 50%}*::-webkit-scrollbar-button:vertical:increment{background-image:-webkit-linear-gradient(135deg,transparent 50%,var(--text) 51%),-webkit-linear-gradient(45deg,transparent 50%,var(--text) 51%);background-image:linear-gradient(-45deg,transparent 50%,var(--text) 51%),linear-gradient(45deg,transparent 50%,var(--text) 51%);background-position:right var(--___bgPadding) top 50%,left var(--___bgPadding) top 50%}*::-webkit-scrollbar-button:vertical:decrement{background-image:-webkit-linear-gradient(135deg,var(--text) 50%,transparent 51%),-webkit-linear-gradient(45deg,var(--text) 50%,transparent 51%);background-image:linear-gradient(-45deg,var(--text) 50%,transparent 51%),linear-gradient(45deg,var(--text) 50%,transparent 51%);background-position:left var(--___bgPadding) top 50%,right var(--___bgPadding) top 50%}html{scrollbar-color:var(--fg) var(--wallpaper);background:var(--wallpaper)}}a{text-decoration:none;color:var(--link)}h4{margin:0}.iconLetter{display:inline-block;text-align:center;font-weight:1000}i[class*=icon-],.svg-inline--fa,.iconLetter{color:var(--icon)}nav{z-index:var(--ZI_navbar);box-shadow:var(--shadow);box-sizing:border-box;height:var(--navbar-height);font-size:calc(var(--navbar-height) / 3.5);position:fixed}#sidebar{grid-area:sidebar}#modal{position:absolute;z-index:var(--ZI_modals)}.column.-scrollable{top:var(--navbar-height);position:-webkit-sticky;position:sticky}#main-scroller{grid-area:content;position:relative}#notifs-column{grid-area:notifs}.app-bg-wrapper{position:fixed;height:100%;top:var(--navbar-height);z-index:-1000;left:0;right:-20px;background-size:cover;background-repeat:no-repeat;background-color:var(--wallpaper);background-image:var(--body-background-image);background-position:50%}.underlay{grid-column:1/span 3;grid-row:1/1;pointer-events:none;background-color:var(--underlay);z-index:-1000}.app-layout{--miniColumn: 25rem;--maxiColumn: 45rem;--columnGap: 1rem;--effectiveSidebarColumnWidth: minmax(var(--miniColumn), var(--sidebarColumnWidth, var(--miniColumn)));--effectiveNotifsColumnWidth: minmax(var(--miniColumn), var(--notifsColumnWidth, var(--miniColumn)));--effectiveContentColumnWidth: minmax(var(--miniColumn), var(--contentColumnWidth, var(--maxiColumn)));position:relative;display:grid;grid-template-columns:var(--effectiveSidebarColumnWidth) var(--effectiveContentColumnWidth);grid-template-areas:"sidebar content";grid-template-rows:1fr;box-sizing:border-box;margin:0 auto;place-content:flex-start center;flex-wrap:wrap;min-height:100vh;overflow-x:clip}.app-layout .column{--___columnMargin: var(--columnGap);display:grid;grid-template-columns:100%;box-sizing:border-box;grid-row:1/1;margin:0 calc(var(--___columnMargin) / 2);padding:calc(var(--___columnMargin)) 0;row-gap:var(--___columnMargin);align-content:start}.app-layout .column:not(.-scrollable){margin-top:var(--navbar-height)}.app-layout .column:hover{z-index:2}.app-layout .column.-full-height{margin-bottom:0;padding-top:0;padding-bottom:0}.app-layout .column.-scrollable{--___paddingIncrease: calc(var(--columnGap) / 2);position:-webkit-sticky;position:sticky;top:var(--navbar-height);max-height:calc(100vh - var(--navbar-height));overflow:hidden auto;margin-left:calc(var(--___paddingIncrease) * -1);padding-left:calc(var(--___paddingIncrease) + var(--___columnMargin) / 2)}@supports (scrollbar-width: none) or (-webkit-text-fill-color: initial){.app-layout .column.-scrollable:not(.-show-scrollbar){scrollbar-width:none;margin-right:calc(var(--___paddingIncrease) * -1);padding-right:calc(var(--___paddingIncrease) + var(--___columnMargin) / 2)}.app-layout .column.-scrollable:not(.-show-scrollbar)::-webkit-scrollbar{display:block;width:0}}.app-layout .column.-scrollable .panel-heading.-sticky{top:calc(var(--columnGap) / -1)}.app-layout.-has-new-post-button .column{padding-bottom:10rem}.app-layout.-no-sticky-headers .column .panel-heading.-sticky{position:relative;top:0}.app-layout .column-inner{display:grid;grid-template-columns:100%;box-sizing:border-box;row-gap:1em;align-content:start}.app-layout.-reverse:not(.-wide,.-mobile){grid-template-columns:var(--effectiveContentColumnWidth) var(--effectiveSidebarColumnWidth);grid-template-areas:"content sidebar"}.app-layout.-wide{grid-template-columns:var(--effectiveSidebarColumnWidth) var(--effectiveContentColumnWidth) var(--effectiveNotifsColumnWidth);grid-template-areas:"sidebar content notifs"}.app-layout.-wide.-reverse{grid-template-columns:var(--effectiveNotifsColumnWidth) var(--effectiveContentColumnWidth) var(--effectiveSidebarColumnWidth);grid-template-areas:"notifs content sidebar"}.app-layout.-mobile{grid-template-columns:100vw;grid-template-areas:"content";padding:0}.app-layout.-mobile .column{padding-top:0;margin:var(--navbar-height) 0 0 0}.app-layout.-mobile .panel-heading,.app-layout.-mobile .panel-heading:after,.app-layout.-mobile .panel-heading:before,.app-layout.-mobile .panel,.app-layout.-mobile .panel:after{border-top-left-radius:0;border-top-right-radius:0}.app-layout.-mobile #sidebar,.app-layout.-mobile #notifs-column,.app-layout.-normal #notifs-column{display:none}.text-center{text-align:center}.button-default{-webkit-user-select:none;user-select:none;color:var(--text);border:none;cursor:pointer;background-color:var(--background);box-shadow:var(--shadow);font-size:1em;font-family:sans-serif;font-family:var(--font)}.button-default.-transparent{-webkit-backdrop-filter:blur(.125em) contrast(60%);backdrop-filter:blur(.125em) contrast(60%)}.button-default::-moz-focus-inner{border:none}.button-default:disabled{cursor:not-allowed}.button-default:active{-webkit-transform:translate(1px,1px);transform:translate(1px,1px)}.menu-item{line-height:var(--__line-height);font-family:inherit;font-weight:400;font-size:100%;cursor:pointer}.menu-item a,.menu-item button:not(.button-default){color:var(--text);font-size:100%}.menu-item.disabled{cursor:not-allowed}.list-item{border-color:var(--border);border-style:solid;border-width:0;border-top-width:1px}.list-item.-active,.list-item:hover{border-top-width:1px;border-bottom-width:1px}.list-item.-active+.list-item,.list-item:hover+.list-item{border-top-width:0}.list-item:hover+.menu-item-collapsible:not(.-expanded)+.list-item,.list-item.-active+.menu-item-collapsible:not(.-expanded)+.list-item{border-top-width:0}.list-item[aria-expanded=true]{border-bottom-width:1px}.list-item:first-child{border-top-right-radius:var(--roundness);border-top-left-radius:var(--roundness);border-top-width:0}.list-item:last-child{border-bottom-right-radius:var(--roundness);border-bottom-left-radius:var(--roundness);border-bottom-width:0}.menu-item,.list-item{display:block;box-sizing:border-box;border:none;outline:none;text-align:initial;color:inherit;clear:both;position:relative;white-space:nowrap;width:100%;padding:var(--__vertical-gap) var(--__horizontal-gap);background:transparent;--__line-height: 1.5em;--__horizontal-gap: .75em;--__vertical-gap: .5em}.menu-item.-non-interactive,.list-item.-non-interactive{cursor:auto}.menu-item a,.menu-item button:not(.button-default),.list-item a,.list-item button:not(.button-default){text-align:initial;padding:0;background:none;border:none;outline:none;display:inline;font-family:inherit;line-height:unset}.button-unstyled{border:none;outline:none;display:inline;text-align:initial;font-size:100%;font-family:inherit;box-shadow:var(--shadow);background-color:transparent;padding:0;line-height:unset;cursor:pointer;box-sizing:content-box;color:inherit}.button-unstyled.-link{color:var(--link)!important}label.-disabled{color:var(--textFaint)}input,textarea{border:none;display:inline-block;outline:none}.input{--_padding: .5em;border:none;background-color:var(--background);color:var(--text);box-shadow:var(--shadow);font-family:var(--font);font-size:1em;margin:0;box-sizing:border-box;display:inline-block;position:relative;line-height:2;-webkit-hyphens:none;hyphens:none;padding:0 var(--_padding)}.input.unstyled{border-radius:0;background:none!important;box-shadow:none;height:unset}.input::-webkit-input-placeholder{color:var(--textFaint)}.input::placeholder{color:var(--textFaint)}.input:disabled,.input[disabled=disabled],.input.disabled{cursor:not-allowed;color:var(--textFaint);background-color:transparent!important}.input[type=range]{background:none;border:none;margin:0;box-shadow:none;-webkit-box-flex:1;flex:1}.input[type=radio]{display:none}.input[type=radio]:checked+label:before{box-shadow:var(--shadow);background-color:var(--background);color:var(--text)}.input[type=radio]:disabled{background-color:var(--background)}.input[type=radio]:disabled,.input[type=radio]:disabled+label,.input[type=radio]:disabled+label:before{opacity:.5}.input[type=radio]+label:before{flex-shrink:0;display:inline-block;content:"•";-webkit-transition:box-shadow .2s;transition:box-shadow .2s;width:1.1em;height:1.1em;border-radius:100%;background-color:var(--background);box-shadow:var(--shadow);margin-right:.5em;vertical-align:top;text-align:center;line-height:1.1;font-size:1.1em;box-sizing:border-box;color:transparent;overflow:hidden}.input[type=checkbox]:checked+label:before{color:var(--text);background-color:var(--background);box-shadow:var(--shadow)}.input[type=checkbox]:disabled,.input[type=checkbox]:disabled+label,.input[type=checkbox]:disabled+label:before{opacity:.5}.input[type=checkbox]+label:before{flex-shrink:0;display:inline-block;content:"✓";-webkit-transition:color .2s;transition:color .2s;width:1.1em;height:1.1em;border-radius:var(--roundness);box-shadow:var(--shadow);margin-right:.5em;vertical-align:top;text-align:center;line-height:1.1;font-size:1.1em;box-sizing:border-box;color:transparent;overflow:hidden}.input.resize-height{resize:vertical}.input,.button-default{--_roundness-left: var(--roundness);--_roundness-right: var(--roundness);border-top-left-radius:var(--_roundness-left);border-bottom-left-radius:var(--_roundness-left);border-top-right-radius:var(--_roundness-right);border-bottom-right-radius:var(--_roundness-right)}textarea.input{padding:var(--_padding);line-height:var(--post-line-height)}option{color:var(--text);background-color:var(--background)}.hide-number-spinner{-webkit-appearance:textfield;-moz-appearance:textfield;appearance:textfield}.hide-number-spinner[type=number]::-webkit-inner-spin-button,.hide-number-spinner[type=number]::-webkit-outer-spin-button{opacity:0;display:none}.cards-list{list-style:none;display:grid;grid-auto-flow:row dense;grid-template-columns:repeat(auto-fit,minmax(20em,1fr));grid-gap:.5em}.cards-list li{border:1px solid var(--border);border-radius:var(--roundness);padding:.5em;margin:.25em}.btn-group{position:relative;display:-webkit-inline-box;display:inline-flex;vertical-align:middle}.btn-group>*,.btn-group>* .button-default{--_roundness-left: 0;--_roundness-right: 0;position:relative}.btn-group>*:first-child,.btn-group>*:first-child .button-default{--_roundness-left: var(--roundness)}.btn-group>*:last-child,.btn-group>*:last-child .button-default{--_roundness-right: var(--roundness)}.fa{color:gray}.mobile-shown{display:none}.badge{box-sizing:border-box;display:inline-block;border-radius:99px;max-width:10em;min-width:1.7em;height:1.3em;padding:.15em;vertical-align:middle;font-weight:400;font-style:normal;font-size:.9em;line-height:1;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.badge.-dot,.badge.-counter{margin:0;position:absolute}.badge.-dot{min-height:.6em;max-height:.6em;min-width:.6em;max-width:.6em;left:calc(50% + .5em);top:calc(50% - 1em);line-height:0;padding:0;margin:0}.badge.-counter{border-radius:var(--roundness);font-size:.75em;line-height:1;text-align:right;padding:.2em;min-width:0;left:calc(50% - .5em);top:calc(50% - .4em);margin-left:.7em;margin-top:-1em}.alert{margin:0 .35em;padding:0 .25em;border-radius:var(--roundness);border:1px solid var(--border)}.faint{--text: var(--textFaint);--link: var(--linkFaint);color:var(--text)}.notice-dismissible{padding-right:4rem;position:relative}.notice-dismissible .dismiss{position:absolute;top:0;right:0;padding:.5em;color:inherit}.fa-scale-110.svg-inline--fa,.fa-scale-110.iconLetter{font-size:1.1em}.fa-scale-110.svg-inline--fa{vertical-align:-.15em}.fa-old-padding.iconLetter,.fa-old-padding.svg-inline--fa,.fa-old-padding-layer{padding:0 .3em}.veryfaint{opacity:.25}.timeago{--link: var(--text);--linkFaint: var(--textFaint)}.login-hint{text-align:center}@media all and (width >= 801px){.login-hint{display:none}}.login-hint a{display:inline-block;padding:1em 0;width:100%}.btn.button-default{min-height:2em}.new-status-notification{position:relative;font-size:1.1em;z-index:1;-webkit-box-flex:1;flex:1}@media all and (width <= 800px){.mobile-hidden{display:none}}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@-webkit-keyframes shakeError{0%{-webkit-transform:translateX(0);transform:translate(0)}15%{-webkit-transform:translateX(.375rem);transform:translate(.375rem)}30%{-webkit-transform:translateX(-.375rem);transform:translate(-.375rem)}45%{-webkit-transform:translateX(.375rem);transform:translate(.375rem)}60%{-webkit-transform:translateX(-.375rem);transform:translate(-.375rem)}75%{-webkit-transform:translateX(.375rem);transform:translate(.375rem)}90%{-webkit-transform:translateX(-.375rem);transform:translate(-.375rem)}to{-webkit-transform:translateX(0);transform:translate(0)}}@keyframes shakeError{0%{-webkit-transform:translateX(0);transform:translate(0)}15%{-webkit-transform:translateX(.375rem);transform:translate(.375rem)}30%{-webkit-transform:translateX(-.375rem);transform:translate(-.375rem)}45%{-webkit-transform:translateX(.375rem);transform:translate(.375rem)}60%{-webkit-transform:translateX(-.375rem);transform:translate(-.375rem)}75%{-webkit-transform:translateX(.375rem);transform:translate(.375rem)}90%{-webkit-transform:translateX(-.375rem);transform:translate(-.375rem)}to{-webkit-transform:translateX(0);transform:translate(0)}}.fade-enter-active,.fade-leave-active{-webkit-transition:opacity .3s;transition:opacity .3s}.fade-enter-from,.fade-leave-active{opacity:0}.visible-for-screenreader-only{display:block;width:1px;height:1px;margin:-1px;overflow:hidden;visibility:visible;clip:rect(0 0 0 0);padding:0;position:absolute}*::selection{color:var(--selectionText);background-color:var(--selectionBackground)}#splash{pointer-events:none}#splash #status.css-ok:before{display:inline-block;content:"CSS OK"}#splash #status .initial-text{display:none}#splash #throbber{-webkit-animation-duration:3s;animation-duration:3s;-webkit-animation-name:bounce;animation-name:bounce;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-direction:normal;animation-direction:normal;-webkit-transform-origin:bottom center;transform-origin:bottom center}#splash #throbber.dead{-webkit-animation-name:dead;animation-name:dead;-webkit-animation-duration:.5s;animation-duration:.5s;-webkit-animation-iteration-count:1;animation-iteration-count:1;-webkit-transform:rotateX(90deg) rotateY(0) rotateZ(-45deg);transform:rotateX(90deg) rotateY(0) rotate(-45deg)}@-webkit-keyframes dead{0%{-webkit-transform:rotateX(0) rotateY(0) rotateZ(0);transform:rotateX(0) rotateY(0) rotate(0)}5%{-webkit-transform:rotateX(0) rotateY(0) rotateZ(1deg);transform:rotateX(0) rotateY(0) rotate(1deg)}10%{-webkit-transform:rotateX(0) rotateY(0) rotateZ(-2deg);transform:rotateX(0) rotateY(0) rotate(-2deg)}15%{-webkit-transform:rotateX(0) rotateY(0) rotateZ(3deg);transform:rotateX(0) rotateY(0) rotate(3deg)}20%{-webkit-transform:rotateX(0) rotateY(0) rotateZ(0);transform:rotateX(0) rotateY(0) rotate(0)}25%{-webkit-transform:rotateX(0) rotateY(0) rotateZ(0);transform:rotateX(0) rotateY(0) rotate(0)}30%{-webkit-transform:rotateX(10deg) rotateY(0) rotateZ(0);transform:rotateX(10deg) rotateY(0) rotate(0)}35%{-webkit-transform:rotateX(-10deg) rotateY(0) rotateZ(0);transform:rotateX(-10deg) rotateY(0) rotate(0)}40%{-webkit-transform:rotateX(10deg) rotateY(0) rotateZ(0);transform:rotateX(10deg) rotateY(0) rotate(0)}45%{-webkit-transform:rotateX(-10deg) rotateY(0) rotateZ(0);transform:rotateX(-10deg) rotateY(0) rotate(0)}50%{-webkit-transform:rotateX(10deg) rotateY(0) rotateZ(0);transform:rotateX(10deg) rotateY(0) rotate(0)}to{-webkit-transform:rotateX(90deg) rotateY(0) rotateZ(-45deg);transform:rotateX(90deg) rotateY(0) rotate(-45deg);-webkit-transition-timing-function:cubic-bezier(.755,.05,.855,.06);transition-timing-function:cubic-bezier(.755,.05,.855,.06)}}@keyframes dead{0%{-webkit-transform:rotateX(0) rotateY(0) rotateZ(0);transform:rotateX(0) rotateY(0) rotate(0)}5%{-webkit-transform:rotateX(0) rotateY(0) rotateZ(1deg);transform:rotateX(0) rotateY(0) rotate(1deg)}10%{-webkit-transform:rotateX(0) rotateY(0) rotateZ(-2deg);transform:rotateX(0) rotateY(0) rotate(-2deg)}15%{-webkit-transform:rotateX(0) rotateY(0) rotateZ(3deg);transform:rotateX(0) rotateY(0) rotate(3deg)}20%{-webkit-transform:rotateX(0) rotateY(0) rotateZ(0);transform:rotateX(0) rotateY(0) rotate(0)}25%{-webkit-transform:rotateX(0) rotateY(0) rotateZ(0);transform:rotateX(0) rotateY(0) rotate(0)}30%{-webkit-transform:rotateX(10deg) rotateY(0) rotateZ(0);transform:rotateX(10deg) rotateY(0) rotate(0)}35%{-webkit-transform:rotateX(-10deg) rotateY(0) rotateZ(0);transform:rotateX(-10deg) rotateY(0) rotate(0)}40%{-webkit-transform:rotateX(10deg) rotateY(0) rotateZ(0);transform:rotateX(10deg) rotateY(0) rotate(0)}45%{-webkit-transform:rotateX(-10deg) rotateY(0) rotateZ(0);transform:rotateX(-10deg) rotateY(0) rotate(0)}50%{-webkit-transform:rotateX(10deg) rotateY(0) rotateZ(0);transform:rotateX(10deg) rotateY(0) rotate(0)}to{-webkit-transform:rotateX(90deg) rotateY(0) rotateZ(-45deg);transform:rotateX(90deg) rotateY(0) rotate(-45deg);-webkit-transition-timing-function:cubic-bezier(.755,.05,.855,.06);transition-timing-function:cubic-bezier(.755,.05,.855,.06)}}@-webkit-keyframes bounce{0%{scale:1 1;translate:0 0;-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}10%{scale:1.2 .8;translate:0 0;-webkit-transform:rotateZ(var(--defaultZ));transform:rotate(var(--defaultZ));-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}30%{scale:.9 1.1;translate:0 -40%;-webkit-transform:rotateZ(var(--defaultZ));transform:rotate(var(--defaultZ));-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{scale:1.1 .9;translate:0 -50%;-webkit-transform:rotateZ(var(--defaultZ));transform:rotate(var(--defaultZ));-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}45%{scale:.9 1.1;translate:0 -45%;-webkit-transform:rotateZ(var(--defaultZ));transform:rotate(var(--defaultZ));-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}50%{scale:1.05 .95;translate:0 -40%;-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}55%{scale:.985 1.025;translate:0 -35%;-webkit-transform:rotateZ(var(--defaultZ));transform:rotate(var(--defaultZ));-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}60%{scale:1.0125 .9985;translate:0 -30%;-webkit-transform:rotateZ(var(--defaultZ));transform:rotate(var(--defaultZ));-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}80%{scale:1.0063 .9938;translate:0 -10%;-webkit-transform:rotateZ(var(--defaultZ));transform:rotate(var(--defaultZ));-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}90%{scale:1.2 .8;translate:0 0;-webkit-transform:rotateZ(var(--defaultZ));transform:rotate(var(--defaultZ));-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}to{scale:1 1;translate:0 0;-webkit-transform:rotateZ(var(--defaultZ));transform:rotate(var(--defaultZ));-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}@keyframes bounce{0%{scale:1 1;translate:0 0;-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}10%{scale:1.2 .8;translate:0 0;-webkit-transform:rotateZ(var(--defaultZ));transform:rotate(var(--defaultZ));-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}30%{scale:.9 1.1;translate:0 -40%;-webkit-transform:rotateZ(var(--defaultZ));transform:rotate(var(--defaultZ));-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{scale:1.1 .9;translate:0 -50%;-webkit-transform:rotateZ(var(--defaultZ));transform:rotate(var(--defaultZ));-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}45%{scale:.9 1.1;translate:0 -45%;-webkit-transform:rotateZ(var(--defaultZ));transform:rotate(var(--defaultZ));-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}50%{scale:1.05 .95;translate:0 -40%;-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}55%{scale:.985 1.025;translate:0 -35%;-webkit-transform:rotateZ(var(--defaultZ));transform:rotate(var(--defaultZ));-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}60%{scale:1.0125 .9985;translate:0 -30%;-webkit-transform:rotateZ(var(--defaultZ));transform:rotate(var(--defaultZ));-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}80%{scale:1.0063 .9938;translate:0 -10%;-webkit-transform:rotateZ(var(--defaultZ));transform:rotate(var(--defaultZ));-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}90%{scale:1.2 .8;translate:0 0;-webkit-transform:rotateZ(var(--defaultZ));transform:rotate(var(--defaultZ));-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}to{scale:1 1;translate:0 0;-webkit-transform:rotateZ(var(--defaultZ));transform:rotate(var(--defaultZ));-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}@property --shadow{syntax: "*"; inherits: false;}.thread-tree-replies{margin-left:var(--status-margin);border-left:2px solid var(--border)}.thread-tree-replies-hidden{padding:var(--status-margin);display:-webkit-box;display:flex;-webkit-box-align:stretch;align-items:stretch;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column}.Conversation{z-index:1}.Conversation.-hidden{background:var(--__panel-background);-webkit-backdrop-filter:var(--__panel-backdrop-filter);backdrop-filter:var(--__panel-backdrop-filter)}.Conversation .conversation-dive-to-top-level-box{padding:var(--status-margin);border-bottom:1px solid var(--border);border-radius:0;display:-webkit-box;display:flex;-webkit-box-align:stretch;align-items:stretch;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column}.Conversation .thread-ancestors{margin-left:var(--status-margin);border-left:2px solid var(--border)}.Conversation .thread-ancestor.-faded .RichContent{--text: var(--textFaint) !important;--link: var(--linkFaint) !important;--funtextGreentext: var(--funtextGreentextFaint) !important;--funtextCyantext: var(--funtextCyantextFaint) !important}.Conversation .thread-ancestor-dive-box{padding-left:var(--status-margin);border-bottom:1px solid var(--border);border-radius:0}.Conversation .thread-ancestor-dive-box,.Conversation .thread-ancestor-dive-box-inner{display:-webkit-box;display:flex;-webkit-box-align:stretch;align-items:stretch;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column}.Conversation .thread-ancestor-dive-box-inner{padding:var(--status-margin)}.Conversation .conversation-status{border-bottom:1px solid var(--border);border-radius:0}.Conversation .thread-ancestor-has-other-replies .conversation-status,.Conversation:last-child:not(.-expanded) .conversation-status,.Conversation.-expanded .conversation-status:last-child,.Conversation .thread-ancestor:last-child .conversation-status,.Conversation .thread-ancestor:last-child .thread-ancestor-dive-box,.Conversation.-expanded .thread-tree .conversation-status{border-bottom:none}.Conversation .thread-ancestors+.thread-tree>.conversation-status{border-top:1px solid var(--border)}.Conversation.status-fadein.-expanded .thread-body{border-left:4px solid var(--cRed);border-radius:var(--roundness);border-top-left-radius:0;border-top-right-radius:0;border-bottom:1px solid var(--border)}.Conversation.-expanded.status-fadein{--___margin: calc(var(--status-margin) / 2);background:var(--background);margin:var(--___margin)}.Conversation.-expanded.status-fadein:before{z-index:-1;content:"";display:block;position:absolute;inset:calc(var(--___margin) * -1);background:var(--background);-webkit-backdrop-filter:var(--__panel-backdrop-filter);backdrop-filter:var(--__panel-backdrop-filter)}.timeline-menu-popover{min-width:24rem;max-width:100vw;margin-top:.6rem;font-size:1rem;border-top-right-radius:0;border-top-left-radius:0}.timeline-menu-popover ul{list-style:none;margin:0;padding:0}.TimelineMenu{margin-right:auto;min-width:0}.TimelineMenu .popover-trigger-button{vertical-align:bottom}.TimelineMenu .panel:after{border-top-right-radius:0;border-top-left-radius:0}.TimelineMenu .timeline-menu-title{margin:0;cursor:pointer;-webkit-user-select:none;user-select:none;width:100%;display:-webkit-box;display:flex}.TimelineMenu .timeline-menu-title .timeline-menu-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.TimelineMenu .timeline-menu-title svg{margin-left:.6em;-webkit-transition:-webkit-transform .1s;transition:-webkit-transform .1s;transition:transform .1s;transition:transform .1s,-webkit-transform .1s}.TimelineMenu .timeline-menu-title .click-blocker{cursor:default;-webkit-box-flex:1;flex-grow:1}.TimelineMenu.open .timeline-menu-title svg{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.TimelineMenu .panel{box-shadow:var(--popoverShadow)}.scroll-to-top{display:none}.-scrolled .scroll-to-top{display:inline-block}.Timeline .timeline-body{background:none;-webkit-backdrop-filter:none;backdrop-filter:none}.Timeline .alert-badge{font-size:.75em;line-height:1;text-align:right;border-radius:var(--roundness);position:absolute;left:calc(50% - .5em);top:calc(50% - .4em);padding:.2em;margin-left:.7em;margin-top:-1em}.Timeline .loadmore-button{position:relative}.Timeline.-blocked{cursor:progress}.Timeline .conversation-heading{top:calc(var(--__panel-heading-height) * var(--currentPanelStack, 1) + var(--navbar-height));z-index:2}.Timeline.-embedded .timeline-heading{text-align:center;line-height:2.75em;padding:0 .5em}.Timeline.-embedded .timeline-heading:empty:before{content:initial}.Timeline.-embedded .timeline-heading .button-default,.Timeline.-embedded .timeline-heading .alert{line-height:2em;width:100%}.tab-switcher{display:-webkit-box;display:flex}.tab-switcher .tab-icon{margin:.2em auto;display:block}.tab-switcher.top-tabs{-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column}.tab-switcher.top-tabs>.tabs{width:100%;overflow:auto hidden;padding-top:5px;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-direction:row;-webkit-box-flex:0;flex:0 0 auto}.tab-switcher.top-tabs>.tabs:after,.tab-switcher.top-tabs>.tabs:before{content:"";-webkit-box-flex:1;flex:1 1 auto;border-bottom:1px solid;border-bottom-color:var(--border)}.tab-switcher.top-tabs>.tabs .tab-wrapper{height:2em}.tab-switcher.top-tabs>.tabs .tab-wrapper:not(.active):after{left:0;right:0;bottom:0;border-bottom:1px solid;border-bottom-color:var(--border)}.tab-switcher.top-tabs>.tabs .tab{width:100%;min-width:1px;border-bottom-left-radius:0;border-bottom-right-radius:0;padding-bottom:99px;margin-bottom:-93px}.tab-switcher.top-tabs .contents.scrollable-tabs{flex-basis:0}.tab-switcher .contents{-webkit-box-flex:1;flex:1 0 auto;min-height:0}.tab-switcher .contents .hidden{display:none}.tab-switcher .contents .-full-height:not(.hidden){height:100%;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column}.tab-switcher .contents .-full-height:not(.hidden)>*:not(.mobile-label){-webkit-box-flex:1;flex:1}.tab-switcher .contents .-full-width:not(.hidden){display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column}.tab-switcher .contents .-full-width:not(.hidden)>*:not(.mobile-label){width:auto}.tab-switcher .contents.scrollable-tabs{overflow-y:auto}.tab-switcher .tab{-webkit-user-select:none;user-select:none;color:var(--text);border:none;cursor:pointer;box-shadow:var(--shadow);font-size:1em;font-family:var(--font);border-radius:var(--roundness);background-color:var(--background);position:relative;white-space:nowrap;padding:6px 1em}.tab-switcher .tab:not(.active){z-index:4;margin-top:.25em}.tab-switcher .tab:not(.active):hover{z-index:6}.tab-switcher .tab.active{background:transparent;z-index:5}.tab-switcher .tab img{max-height:1.9em;vertical-align:top;margin-top:-.3em}.tab-switcher .tabs{display:-webkit-box;display:flex;position:relative;box-sizing:border-box}.tab-switcher .tabs:after,.tab-switcher .tabs:before{display:block;-webkit-box-flex:1;flex:1 1 auto}.tab-switcher .tab-wrapper{position:relative;display:-webkit-box;display:flex;-webkit-box-flex:0;flex:0 0 auto}.tab-switcher .tab-wrapper:not(.active):after{content:"";position:absolute;z-index:7}.tab-switcher .mobile-label{padding-left:.3em;padding-bottom:.25em;margin-top:.5em;margin-left:.2em;margin-bottom:.25em;border-bottom:1px solid var(--border)}@media all and (width >= 800px){.tab-switcher .mobile-label{display:none}}.chat-title{display:-webkit-box;display:flex;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;--emoji-size: 1em}.chat-title .username{max-width:100%;text-overflow:ellipsis;white-space:nowrap;display:inline;overflow:hidden}.chat-title .avatar-container{align-self:center;line-height:1}.chat-title .titlebar-avatar{margin-right:.5em;height:1.5em;width:1.5em;border-radius:var(--roundness)}.chat-title .titlebar-avatar.animated:before{display:none}.chat-list-item{display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-direction:row;overflow:hidden;box-sizing:border-box;cursor:pointer}.chat-list-item :focus{outline:none}.chat-list-item .chat-list-item-left{margin-right:1em}.chat-list-item .chat-list-item-center{width:100%;box-sizing:border-box;overflow:hidden}.chat-list-item .heading{width:100%;display:-webkit-box;display:flex;-webkit-box-pack:justify;justify-content:space-between;line-height:1em}.chat-list-item .heading-right{white-space:nowrap}.chat-list-item .name-and-account-name{text-overflow:ellipsis;white-space:nowrap;overflow:hidden;flex-shrink:1;line-height:var(--post-line-height)}.chat-list-item .chat-preview{display:-webkit-box;display:flex;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;margin:.35em 0;color:var(--textFaint);width:100%}.chat-list-item a{color:var(--linkFaint);text-decoration:none;pointer-events:none}.chat-list-item:hover .animated.avatar canvas{display:none}.chat-list-item:hover .animated.avatar img{visibility:visible}.chat-list-item .chat-preview-body{--emoji-size: 1.4em;padding-right:1em}.chat-list-item .time-wrapper{line-height:var(--post-line-height)}.basic-user-card{display:-webkit-box;display:flex;-webkit-box-flex:1;flex:1 0;margin:0;--emoji-size: 1em}.basic-user-card-collapsed-content{margin-left:.7em;text-align:left;-webkit-box-flex:1;flex:1;min-width:0}.basic-user-card-user-name img{object-fit:contain;height:16px;width:16px;vertical-align:middle}.basic-user-card-user-name-value,.basic-user-card-screen-name{display:inline-block;max-width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.basic-user-card-expanded-content{-webkit-box-flex:1;flex:1;margin-left:.7em;min-width:0}.chat-new .input-wrap{display:-webkit-box;display:flex;margin:.7em .5em}.chat-new .input-wrap input{width:100%}.chat-new .search-icon{margin-right:.3em}.chat-new .member-list{padding-bottom:.7rem}.chat-new .go-back-button{text-align:center;line-height:1;height:100%;align-self:start;width:var(--__panel-heading-height-inner)}.chat-list{min-height:25em;margin-bottom:0}.empty-chat-list-alert{padding:3em;font-size:1.2em;display:-webkit-box;display:flex;-webkit-box-pack:center;justify-content:center;color:var(--textFaint)}.chat-message-wrapper.hovered-message-chain .animated.Avatar canvas{display:none}.chat-message-wrapper.hovered-message-chain .animated.Avatar img{visibility:visible}.chat-message-wrapper .chat-message-menu{-webkit-transition:opacity .1s;transition:opacity .1s;opacity:0;position:absolute;top:-.8em}.chat-message-wrapper .chat-message-menu button{padding-top:.2em;padding-bottom:.2em}.chat-message-wrapper .menu-icon{cursor:pointer}.chat-message-wrapper .popover{width:12em}.chat-message-wrapper .chat-message{display:-webkit-box;display:flex;padding-bottom:.5em}.chat-message-wrapper .chat-message .status-body:hover{--_still-image-img-visibility: visible;--_still-image-canvas-visibility: hidden;--_still-image-label-visibility: hidden}.chat-message-wrapper .avatar-wrapper{margin-right:.72em;width:32px}.chat-message-wrapper .link-preview,.chat-message-wrapper .attachments{margin-bottom:1em}.chat-message-wrapper .status{background-color:var(--background);color:var(--text);border-radius:var(--roundness);display:-webkit-box;display:flex;padding:.75em;border:1px solid var(--border)}.chat-message-wrapper .created-at{position:relative;float:right;font-size:.8em;margin:-1em 0 -.5em;font-style:italic;opacity:.8}.chat-message-wrapper .without-attachment .message-content .RichContent:after{margin-right:5.4em;content:" ";display:inline-block}.chat-message-wrapper .pending .status-content.media-body,.chat-message-wrapper .pending .created-at{color:var(--faint)}.chat-message-wrapper .error .status-content.media-body,.chat-message-wrapper .error .created-at{color:var(--badgeNotification)}.chat-message-wrapper .chat-message-inner{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;-webkit-box-align:start;align-items:flex-start;max-width:80%;min-width:10em;width:100%}.chat-message-wrapper .outgoing{display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row wrap;place-content:end flex-end}.chat-message-wrapper .outgoing .chat-message-inner{-webkit-box-align:end;align-items:flex-end}.chat-message-wrapper .outgoing .chat-message-menu{right:.4rem}.chat-message-wrapper .incoming .chat-message-menu{left:.4rem}.chat-message-wrapper .chat-message-inner.with-media,.chat-message-wrapper .chat-message-inner.with-media .status{width:100%}.chat-message-wrapper .visible{opacity:1}.chat-message-date-separator{text-align:center;margin:1.4em 0;font-size:.9em;-webkit-user-select:none;user-select:none;color:var(--textFaint)}.chat-view{display:-webkit-box;display:flex;height:100%}.chat-view .chat-view-inner{height:auto;width:100%;overflow:visible;display:-webkit-box;display:flex}.chat-view .chat-view-body{box-sizing:border-box;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;width:100%;overflow:visible;min-height:calc(100vh - var(--navbar-height));margin:0;border-radius:var(--roundness);border-bottom-left-radius:0;border-bottom-right-radius:0}.chat-view .chat-view-body:after{border-radius:0}.chat-view .message-list{padding:0 .8em;height:100%;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;-webkit-box-pack:end;justify-content:end}.chat-view .footer{position:-webkit-sticky;position:sticky;bottom:0;z-index:1}.chat-view .chat-view-heading{grid-template-columns:auto minmax(50%,1fr)}.chat-view .go-back-button{text-align:center;line-height:1;height:100%;align-self:start;width:var(--__panel-heading-height-inner)}.chat-view .jump-to-bottom-button{width:2.5em;height:2.5em;border-radius:100%;position:absolute;right:1.3em;top:-3.2em;display:-webkit-box;display:flex;-webkit-box-pack:center;justify-content:center;-webkit-box-align:center;align-items:center;box-shadow:0 1px 1px #0000004d,0 2px 4px #0000004d;z-index:10;-webkit-transition:.35s all;transition:.35s all;-webkit-transition-timing-function:cubic-bezier(0,1,.5,1);transition-timing-function:cubic-bezier(0,1,.5,1);opacity:0;visibility:hidden;cursor:pointer}.chat-view .jump-to-bottom-button.visible{opacity:1;visibility:visible}.chat-view .jump-to-bottom-button .unread-message-count{font-size:.8em;left:50%;margin-top:-1rem;padding:.1em;border-radius:50px;position:absolute}.chat-view .jump-to-bottom-button .chat-loading-error{width:100%;display:-webkit-box;display:flex;-webkit-box-align:end;align-items:flex-end;height:100%}.chat-view .jump-to-bottom-button .chat-loading-error .error{width:100%}.follow-card-content-container{flex-shrink:0;display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row wrap;-webkit-box-pack:justify;justify-content:space-between;line-height:1.5em}.follow-card-button{margin-top:.5em;padding:0 1.5em;margin-left:1em}.follow-card-follow-button{margin-top:.5em;margin-left:auto;width:10em}.with-load-more-footer{padding:.9em;text-align:center;border-top:1px solid;border-top-color:var(--border)}.with-load-more-footer .error{font-size:1rem}.with-load-more-footer a{cursor:pointer}.user-profile{-webkit-box-flex:2;flex:2;--currentPanelStack: 0}.user-profile .card-wrapper{border-top-left-radius:var(--roundness);border-top-right-radius:var(--roundness)}.user-profile .panel-footer{border-bottom-left-radius:var(--roundness);border-bottom-right-radius:var(--roundness)}.user-profile .userlist-placeholder{display:-webkit-box;display:flex;-webkit-box-pack:center;justify-content:center;-webkit-box-align:center;align-items:center;padding:2em}.user-profile .user-info{margin:1.2em}.user-profile-placeholder .panel-body{display:-webkit-box;display:flex;-webkit-box-pack:center;justify-content:center;-webkit-box-align:center;align-items:center;padding:7em}.user-profile-placeholder .alert{padding:.75em 5em;border-width:2px}.user-profile-placeholder .alert .error-message{color:var(--text);font-weight:700}.search-result-heading{color:var(--faint);padding:.75rem;text-align:center}@media all and (width <= 800px){.search-nav-heading .tab-switcher .tabs .tab-wrapper{display:block;-webkit-box-pack:center;justify-content:center;-webkit-box-flex:1;flex:1 1 auto;text-align:center}}.search-result{box-sizing:border-box;border-bottom:1px solid;border-color:var(--border)}.search-input-container{padding:.8rem;display:-webkit-box;display:flex;-webkit-box-pack:center;justify-content:center}.search-input-container .search-input{width:100%;line-height:1.125rem;font-size:1rem;padding:.5rem;box-sizing:border-box}.search-input-container .search-button{margin-left:.5em}.loading-icon{padding:1em}.trend{display:-webkit-box;display:flex;-webkit-box-align:center;align-items:center}.trend .hashtag{-webkit-box-flex:1;flex:1 1 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.trend .hashtag .name{color:var(--link)}.trend .count{-webkit-box-flex:0;flex:0 0 auto;width:2rem;font-size:1.5rem;line-height:2.25rem;font-weight:500;text-align:center;color:var(--text)}.more-statuses-button{height:3.5em;line-height:3.5em;width:100%}.ProfileSettingIndicator{display:inline-block;position:relative}.profilesetting-tooltip{margin:.5em 1em;min-width:10em;text-align:center}.interface-language-switcher .setting-list .setting-item{display:grid;grid-template-columns:subgrid}.interface-language-switcher .add-button{display:block;text-align:center;padding-bottom:1em}.interface-language-switcher .add-button .default-button{display:block;width:auto}.-mobile .interface-language-switcher li.setting-item{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;gap:.5em;-webkit-box-align:stretch;align-items:stretch;border-bottom:none}.-mobile .interface-language-switcher .add-button{border-bottom:1px solid var(--border)}.tos-content{margin:1em}.registration-form{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;margin:1em}.registration-form .container{display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-direction:row}.registration-form .container>*{min-width:0}.registration-form .terms-of-service{-webkit-box-flex:0;flex:0 1 50%;margin:.6em 0 0 .8em}.registration-form .text-fields{margin-top:.6em;-webkit-box-flex:1;flex:1 0;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column}.registration-form textarea{min-height:100px;resize:vertical}.registration-form .form-group{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;padding:.3em 0;line-height:2;margin-bottom:1em}.registration-form .form-group--error{-webkit-animation-name:shakeError;animation-name:shakeError;-webkit-animation-duration:.6s;animation-duration:.6s;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}.registration-form .form-group--error .form--label{color:var(--cRed)}.registration-form .form-error{margin-top:-.7em;text-align:left}.registration-form .form-error span{font-size:.85em}.registration-form .form-error ul{list-style:none;padding:0 0 0 5px;margin-top:0}.registration-form .form-error ul li:before{content:"• "}.registration-form form textarea{line-height:1;resize:vertical}.registration-form .captcha{max-width:350px;margin-bottom:.4em}.registration-form .btn{margin-top:.6em;height:2em}.registration-form .error{text-align:center}.registration-notice{margin:.6em}@media all and (width <= 800px){.registration-form .container{-webkit-box-orient:vertical;-webkit-box-direction:reverse;flex-direction:column-reverse}.registration-form .container .terms-of-service{margin:0}}.password-reset-form{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;-webkit-box-align:center;align-items:center;margin:.6em}.password-reset-form .container{display:-webkit-box;display:flex;-webkit-box-flex:1;flex:1 0;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;margin-top:.6em;max-width:18rem}.password-reset-form .container>*{min-width:0}.password-reset-form .form-group{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;margin-bottom:1em;padding:.3em 0;line-height:1.85em}.password-reset-form .error{text-align:center;-webkit-animation-name:shakeError;animation-name:shakeError;-webkit-animation-duration:.4s;animation-duration:.4s;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}.password-reset-form .alert{padding:.5em;margin:.3em 0 1em}.password-reset-form .notice-dismissible{padding-right:2rem}.password-reset-form .dismiss{cursor:pointer}.follow-request-card-content-container{display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row wrap}.follow-request-card-content-container button{margin-top:.5em;margin-right:.5em;-webkit-box-flex:1;flex:1 1;max-width:12em;min-width:8em}.follow-request-card-content-container button:last-child{margin-right:0}.staff-group{padding:1em}.staff-group .basic-user-card{padding-left:0}.mrf-section{margin:1em}.mrf-section table{width:100%;text-align:left;padding-left:.5em;padding-bottom:1.1em}.mrf-section table th,.mrf-section table td{width:11em;max-width:25em;overflow:hidden;vertical-align:text-top}.mrf-section table th+th,.mrf-section table td+td{width:auto}.list-card{display:-webkit-box;display:flex}.list-name{-webkit-box-flex:1;flex-grow:1}.list-name,.button-list-edit{margin:0;padding:1em;color:var(--link)}.Lists .new-list-button{padding:0 .5em}.ListsUserSearch .input-wrap{display:-webkit-box;display:flex;margin:.7em .5em}.ListsUserSearch .input-wrap input{width:100%}.ListsUserSearch .search-icon{margin-right:.3em}.ListEdit{--panel-body-padding: .5em;height:calc(100vh - var(--navbar-height));overflow:hidden;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column}.ListEdit .list-edit-heading{grid-template-columns:auto minmax(50%,1fr)}.ListEdit .panel-body{display:-webkit-box;display:flex;-webkit-box-flex:1;flex:1;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;overflow:hidden}.ListEdit .list-member-management{-webkit-box-flex:1;flex:1 0 auto}.ListEdit .search-icon{margin-right:.3em}.ListEdit .users-list{padding-bottom:.7rem;overflow-y:auto}.ListEdit .search-list,.ListEdit .members-list{overflow:hidden;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;min-height:0}.ListEdit .go-back-button{text-align:center;line-height:1;height:100%;align-self:start;width:var(--__panel-heading-height-inner)}.ListEdit .btn{margin:0 .5em}.ListEdit .panel-footer{grid-template-columns:minmax(10%,1fr)}.ListEdit .panel-footer .footer-button{min-width:9em}.announcement-editor{display:-webkit-box;display:flex;-webkit-box-align:stretch;align-items:stretch;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column}.announcement-editor .announcement-metadata{margin-top:.5em}.announcement-editor .post-textarea{resize:vertical;height:10em;overflow:visible;box-sizing:content-box}.announcement{border-bottom:1px solid var(--border);border-radius:0;padding:var(--status-margin)}.announcement .heading,.announcement .body{margin-bottom:var(--status-margin)}.announcement .footer,.announcement .footer .times{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column}.announcement .footer .actions{display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-direction:row;-webkit-box-pack:space-evenly;justify-content:space-evenly}.announcement .footer .actions .btn{-webkit-box-flex:1;flex:1;margin:1em;max-width:10em}.announcements-page .post-form{padding:var(--status-margin)}.announcements-page .post-form .heading,.announcements-page .post-form .body{margin-bottom:var(--status-margin)}.announcements-page .post-form .post-button{min-width:10em}.Draft{position:relative}.Draft .status-content{padding:.5em;margin:.5em 0}.Draft .status-preview{display:grid;grid-template-columns:1fr;grid-auto-columns:10em;grid-auto-flow:column;grid-gap:.5em;-webkit-box-align:start;align-items:start;max-width:100%}.Draft .status-preview p{white-space:normal;overflow-x:hidden}.Draft .status-preview .poll-indicator-container{border-radius:var(--roundness);display:grid;place-items:center center;align-self:start;height:0;padding-bottom:62.5%;position:relative}.Draft .status-preview .poll-indicator{box-sizing:border-box;border:1px solid var(--border);position:absolute;top:0;right:0;bottom:0;left:0;display:grid;place-items:center center;width:100%;height:100%}.Draft .actions{display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-direction:row;-webkit-box-pack:space-evenly;justify-content:space-evenly}.Draft .actions .btn{-webkit-box-flex:1;flex:1;margin-left:1em;margin-right:1em}.draft{margin:1em 0}.empty-drafs-list-alert{padding:3em;font-size:1.2em;display:-webkit-box;display:flex;-webkit-box-pack:center;justify-content:center;color:var(--textFaint)}.bookmark-folder-card{display:-webkit-box;display:flex;-webkit-box-align:center;align-items:center}a.bookmark-folder-name{display:-webkit-box;display:flex;-webkit-box-align:center;align-items:center;-webkit-box-flex:1;flex-grow:1}a.bookmark-folder-name .icon,a.bookmark-folder-name .iconLetter,a.bookmark-folder-name .iconEmoji{display:inline-block;height:2.5rem;width:2.5rem;margin-right:.5rem}a.bookmark-folder-name .icon,a.bookmark-folder-name .iconLetter{font-size:1.5rem;line-height:2.5rem;text-align:center}a.bookmark-folder-name .iconEmoji{text-align:center;object-fit:contain;vertical-align:middle}a.bookmark-folder-name .iconEmoji>span{font-size:1.5rem;line-height:2.5rem}a.bookmark-folder-name img.iconEmoji{padding:.25em;box-sizing:border-box}.bookmark-folder-name,.button-folder-edit{margin:0;padding:1em;color:var(--link)}.Bookmark-folders .new-folder-button{padding:0 .5em}.BookmarkFolderEdit{--panel-body-padding: .5em;overflow:hidden;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column}.BookmarkFolderEdit .folder-edit-heading{grid-template-columns:auto minmax(50%,1fr)}.BookmarkFolderEdit .panel-body{display:-webkit-box;display:flex;gap:.5em}.BookmarkFolderEdit .emoji-picker-panel{position:absolute;z-index:20;margin-top:2px}.BookmarkFolderEdit .emoji-picker-panel.hide{display:none}.BookmarkFolderEdit .input-emoji{height:2.5em;width:2.5em;padding:0}.BookmarkFolderEdit .input-emoji .iconEmoji{display:inline-block;text-align:center;object-fit:contain;vertical-align:middle;height:2.5em;width:2.5em}.BookmarkFolderEdit .input-emoji .iconEmoji>span{font-size:1.5rem;line-height:2.5rem}.BookmarkFolderEdit .input-emoji img.iconEmoji{padding:.25em;box-sizing:border-box}.BookmarkFolderEdit .input-wrap{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;gap:.5em}.BookmarkFolderEdit .go-back-button{text-align:center;line-height:1;height:100%;align-self:start;width:var(--__panel-heading-height-inner)}.BookmarkFolderEdit .btn{margin:0 .5em}.BookmarkFolderEdit .panel-footer{grid-template-columns:minmax(10%,1fr)}.BookmarkFolderEdit .panel-footer .footer-button{min-width:9em} diff --git a/priv/static/static/css/media_modal.Bc6XAjzi.css b/priv/static/static/css/media_modal.Bc6XAjzi.css new file mode 100644 index 000000000..1d4082591 --- /dev/null +++ b/priv/static/static/css/media_modal.Bc6XAjzi.css @@ -0,0 +1 @@ +@keyframes media-fadein{0%{opacity:0}to{opacity:1}}.media-modal-view .modal-image-container{flex-direction:column;flex-grow:1;justify-content:center;align-items:center;width:100%;max-width:100%;height:100%;max-height:100%;display:flex;overflow:hidden}.media-modal-view .modal-image-container-inner{flex-direction:column;flex-grow:1;justify-content:center;align-items:center;width:100%;height:100%;display:flex}.media-modal-view .description,.media-modal-view .counter{color:#fff;text-shadow:0 0 1em #000,0 0 1em #000,0 0 1em #000;margin:1em 2em;overflow:hidden}.media-modal-view .description+.counter{margin-top:0}.media-modal-view .description{flex:none;max-width:80ch;max-height:9.5em}.media-modal-view .description summary{pointer-events:none;margin-bottom:.5em;font-weight:700;display:inline-block}.media-modal-view .description span{text-wrap:pretty;white-space:pre-wrap;scrollbar-color:white transparent;min-height:1em;max-height:10.5em;line-height:1.5;display:block;overflow-y:auto}.media-modal-view .description span::-webkit-scrollbar-button{background-color:#fff}.media-modal-view .description span::-webkit-scrollbar-thumb{background-color:#fff}.media-modal-view .modal-image{image-orientation:from-image;max-width:100%;max-height:100%;animation:.1s cubic-bezier(.7,0,1,.6) media-fadein}.media-modal-view .modal-image.loading{opacity:.5}.media-modal-view .loading-spinner{pointer-events:none;justify-content:center;align-items:center;width:100%;height:100%;display:flex;position:absolute}.media-modal-view .loading-spinner svg{color:#fff}.media-modal-view .modal-view-button{opacity:0;box-shadow:none;appearance:none;cursor:pointer;background:0 0;border:0;width:3em;height:3em;padding:0;transition:opacity .333s cubic-bezier(.4,0,.22,1);overflow:visible}.media-modal-view .modal-view-button .button-icon{color:#fff;text-align:center;background-color:#0000004d;width:3em;height:3em;font-size:1rem;line-height:3em;position:absolute}.media-modal-view .modal-view-button-arrow{width:3em;height:3em;margin-top:1.5em;display:block;position:absolute;top:50%}.media-modal-view .modal-view-button-arrow .arrow-icon{color:#fff;text-align:center;background-color:#0000004d;line-height:3em;position:absolute;top:0}.media-modal-view .modal-view-button-arrow--prev{left:0}.media-modal-view .modal-view-button-arrow--prev .arrow-icon{left:.5em}.media-modal-view .modal-view-button-arrow--next{right:0}.media-modal-view .modal-view-button-arrow--next .arrow-icon{right:.5em}.media-modal-view .modal-view-button-hide{position:absolute;top:0;right:0}.media-modal-view .modal-view-button-hide .button-icon{top:.5em;right:.5em}.modal-view.media-modal-view{z-index:var(--ZI_media_modal);flex-direction:column}.modal-view.media-modal-view .modal-view-button-arrow,.modal-view.media-modal-view .modal-view-button-hide{opacity:.75}.modal-view.media-modal-view .modal-view-button-arrow:focus,.modal-view.media-modal-view .modal-view-button-arrow:hover,.modal-view.media-modal-view .modal-view-button-hide:focus,.modal-view.media-modal-view .modal-view-button-hide:hover{box-shadow:none;outline:none}.modal-view.media-modal-view .modal-view-button-arrow:hover,.modal-view.media-modal-view .modal-view-button-hide:hover{opacity:1}.modal-view.media-modal-view{overflow:hidden} diff --git a/priv/static/static/css/modal.Dv3V-p-W.css b/priv/static/static/css/modal.Dv3V-p-W.css new file mode 100644 index 000000000..c80b0d06d --- /dev/null +++ b/priv/static/static/css/modal.Dv3V-p-W.css @@ -0,0 +1 @@ +.modal-view{z-index:var(--ZI_modals);pointer-events:none;opacity:0;justify-content:center;align-items:center;animation-name:modal-background-fadein;animation-duration:.2s;display:flex;position:fixed;inset:0;overflow:auto}.modal-view>*{pointer-events:initial}.modal-view.modal-background{pointer-events:initial;background-color:#00000080}.modal-view.open{opacity:1}@keyframes modal-background-fadein{0%{background-color:#0000}to{background-color:#00000080}} diff --git a/priv/static/static/css/moderation_tools.DoZVYsfl.css b/priv/static/static/css/moderation_tools.DoZVYsfl.css new file mode 100644 index 000000000..42b811333 --- /dev/null +++ b/priv/static/static/css/moderation_tools.DoZVYsfl.css @@ -0,0 +1 @@ +.moderation-tools-popover{height:100%}.moderation-tools-popover .trigger{height:100%;display:flex!important}.moderation-tools-button{white-space:nowrap}.moderation-tools-button svg,.moderation-tools-button i{font-size:.8em} diff --git a/priv/static/static/css/mute_confirm.p-qSlYOr.css b/priv/static/static/css/mute_confirm.p-qSlYOr.css new file mode 100644 index 000000000..9fa4ab9a4 --- /dev/null +++ b/priv/static/static/css/mute_confirm.p-qSlYOr.css @@ -0,0 +1 @@ +.expiry-amount{text-align:right;width:4em} diff --git a/priv/static/static/css/notifications.CW0-QQtF.css b/priv/static/static/css/notifications.CW0-QQtF.css new file mode 100644 index 000000000..179f47722 --- /dev/null +++ b/priv/static/static/css/notifications.CW0-QQtF.css @@ -0,0 +1 @@ +.ExtraNotifications{flex-direction:column;align-items:stretch;width:100%;display:flex}.ExtraNotifications.panel-body:before{content:"";padding:0}.ExtraNotifications .notification{border-bottom:1px solid;border-color:var(--border);flex-direction:column;align-items:stretch;width:100%;display:flex}.ExtraNotifications .extra-notification{padding:1em}.ExtraNotifications .icon{margin-right:.5em}.ExtraNotifications .tip{display:inline}.Report .report-content,.Report .report-state{margin:.5em 0 1em}.Report .reported-status{border:1px solid var(--border);border-radius:var(--roundness);margin:.5em 0;padding:.5em;display:block}.Report .reported-status .status-content{pointer-events:none}.Report .reported-status .reported-status-heading{justify-content:space-between;width:100%;margin-bottom:.2em;display:flex}.Report .reported-status .reported-status-name{font-weight:700}.Report .note{width:100%;margin-bottom:.5em}.Notification{border-bottom:1px solid;border-color:var(--border);overflow-wrap:break-word;text-wrap:pretty}.Notification .status-content{cursor:pointer}.Notification{--emoji-size:1em}.Notification:hover{--_still-image-img-visibility:visible;--_still-image-canvas-visibility:hidden;--_still-image-label-visibility:hidden}.Notification.-muted{text-overflow:ellipsis;flex-wrap:nowrap;gap:1ex;height:1.2em;padding:.25em .6em;line-height:1.2em;display:flex;overflow:hidden}.Notification.-muted .status-username,.Notification.-muted .mute-thread,.Notification.-muted .mute-words{white-space:nowrap}.Notification.-muted .status-username,.Notification.-muted .mute-words{text-overflow:ellipsis;overflow:hidden}.Notification.-muted .status-username{flex:0 auto;margin-right:.2em;font-size:smaller;font-weight:400}.Notification.-muted .mute-thread{flex:none}.Notification.-muted .mute-words{flex:1 0 5em;margin-left:.2em}.Notification.-muted .mute-words:before{content:" "}.Notification.-muted .unmute{flex:none;margin-left:auto;display:block}.Notification .type-icon{margin:0 .1em}.Notification.-type--repeat .type-icon{color:var(--cGreen)}.Notification.-type--follow .type-icon,.Notification.-type--follow-request .type-icon{color:var(--cBlue)}.Notification.-type--like .type-icon{color:var(--cOrange)}.Notification.-type--move .type-icon{color:var(--cBlue)}.Notifications:not(.minimal){padding-bottom:15em}.Notifications .loadmore-error{color:var(--text)}.Notifications .notification{position:relative}.Notifications .notification .notification-overlay{pointer-events:none;position:absolute;inset:0}.Notifications .notification.unseen .notification-overlay{background-image:-webkit-linear-gradient(315deg, var(--badgeNotification) 4px, transparent 10px);background-image:linear-gradient(135deg, var(--badgeNotification) 4px, transparent 10px)}.notification{box-sizing:border-box}.notification .Status{flex:1}.notification:hover .animated.Avatar canvas{display:none}.notification:hover .animated.Avatar img{visibility:visible}.notification:last-child .Notification{border-bottom:none}.notification .non-mention{flex-wrap:nowrap;flex:1;min-width:0;padding:.6em;display:flex}.notification .non-mention .avatar-container{width:32px;height:32px}.notification .follow-request-accept:hover{color:var(--text)}.notification .follow-request-reject:hover{color:var(--cRed)}.notification .follow-text,.notification .move-text{overflow-wrap:break-word;justify-content:space-between;padding:.5em 0;display:flex}.notification .follow-text .follow-name,.notification .move-text .follow-name{text-overflow:ellipsis;white-space:nowrap;max-width:100%;display:block;overflow:hidden}.notification time{white-space:nowrap}.notification .notification-right{flex:1;min-width:0;padding-left:.8em}.notification .notification-right .timeago{text-align:right;min-width:3em}.notification .notification-right .timeago-link{margin-right:.2em}.notification .notification-right .expand-icon .svg-inline--fa{margin-left:.25em}.notification .emoji-reaction-emoji{width:auto;max-width:1.25em;height:1.25em;font-size:1.3em}.notification .emoji-reaction-emoji.-wide{max-width:3.75em}.notification .emoji-reaction-emoji-image{vertical-align:middle;object-fit:contain}.notification .notification-details{min-width:0;line-height:var(--post-line-height);flex-wrap:nowrap;flex:1 1 0;justify-content:space-between;width:100%;display:flex;position:relative;overflow:hidden}.notification .notification-details .name-and-action{text-overflow:ellipsis;flex:1;overflow:hidden}.notification .notification-details .username{text-overflow:ellipsis;white-space:nowrap;max-width:100%;font-weight:bolder}.notification .notification-details .timeago{margin-right:.2em}.notification .notification-details .status-content{max-height:300px;margin:0}.notification .notification-details h1{overflow-wrap:break-word;text-wrap:pretty;margin:0 0 .3em;padding:0;font-size:1em;line-height:1.5}.notification .notification-details h1 small{font-weight:lighter}.notification .notification-details p{margin:0 0 .3em} diff --git a/priv/static/static/css/panel_loading.DQYYmPMZ.css b/priv/static/static/css/panel_loading.DQYYmPMZ.css new file mode 100644 index 000000000..0cc8a88d6 --- /dev/null +++ b/priv/static/static/css/panel_loading.DQYYmPMZ.css @@ -0,0 +1 @@ +.panel-loading{height:100%;color:var(--text);justify-content:center;align-items:center;font-size:2em;display:flex}.panel-loading .loading-text svg{vertical-align:middle;color:var(--text);line-height:0} diff --git a/priv/static/static/css/password_reset.BLv39QBH.css b/priv/static/static/css/password_reset.BLv39QBH.css new file mode 100644 index 000000000..a7b561aae --- /dev/null +++ b/priv/static/static/css/password_reset.BLv39QBH.css @@ -0,0 +1 @@ +.password-reset-form{flex-direction:column;align-items:center;margin:.6em;display:flex}.password-reset-form .container{flex-direction:column;flex:1 0;max-width:18rem;margin-top:.6em;display:flex}.password-reset-form .container>*{min-width:0}.password-reset-form .form-group{flex-direction:column;margin-bottom:1em;padding:.3em 0;line-height:1.85em;display:flex}.password-reset-form .error{text-align:center;animation-name:shakeError;animation-duration:.4s;animation-timing-function:ease-in-out}.password-reset-form .alert{margin:.3em 0 1em;padding:.5em}.password-reset-form .dismiss{cursor:pointer} diff --git a/priv/static/static/css/poll_form.s0YHqxUy.css b/priv/static/static/css/poll_form.s0YHqxUy.css new file mode 100644 index 000000000..511272c0b --- /dev/null +++ b/priv/static/static/css/poll_form.s0YHqxUy.css @@ -0,0 +1 @@ +.poll-form{flex-direction:column;padding:0 .5em .5em;display:flex}.poll-form .add-option{align-self:flex-start;padding-top:.25em;padding-left:.1em}.poll-form .poll-option{justify-content:space-between;align-items:baseline;margin-bottom:.25em;display:flex}.poll-form .input-container{width:100%}.poll-form .input-container input{width:100%;padding-right:2.5em}.poll-form .delete-option{z-index:1;width:1.5em;margin-left:-1.5em}.poll-form .poll-type-expiry{width:100%;margin-top:.5em;display:flex}.poll-form .poll-type{flex:60%;margin-right:.75em}.poll-form .poll-type .poll-type-select{padding-right:.75em}.poll-form .poll-expiry{display:flex}.poll-form .poll-expiry .expiry-amount{text-align:right;width:3em} diff --git a/priv/static/static/css/popover.BlbX_Uwp.css b/priv/static/static/css/popover.BlbX_Uwp.css new file mode 100644 index 000000000..a42fbcb86 --- /dev/null +++ b/priv/static/static/css/popover.BlbX_Uwp.css @@ -0,0 +1 @@ +.popover-trigger-button{display:inline-block}.popover{z-index:var(--ZI_popover_override,var(--ZI_popovers));--_shadow:var(--shadow);min-width:0;max-width:calc(100vw - 20px);position:fixed}.popover-default:after{content:"";box-shadow:var(--_shadow);pointer-events:none;position:absolute;inset:-1px}.popover-default{border-radius:var(--roundness);border-color:var(--border);background-color:var(--background);border-style:solid;border-width:1px}.dropdown-menu{text-align:left;max-width:100vw;z-index:var(--ZI_popover_override,var(--ZI_popovers));white-space:nowrap;background-color:var(--background);padding:0;font-size:1em;list-style:none;display:block}.dropdown-menu .dropdown-divider{border-top:1px solid var(--border);height:0;margin:.5rem 0;overflow:hidden}.dropdown-menu .dropdown-item{grid-gap:.5em;grid-template-columns:1fr;grid-auto-columns:auto;grid-auto-flow:column;padding:0;display:grid}.dropdown-menu .dropdown-item .popover-wrapper{box-sizing:border-box;grid-template-columns:1fr;display:grid}.dropdown-menu .dropdown-item .extra-button{border-left:1px solid;border-image-source:linear-gradient(to bottom, transparent 0%, var(--icon) var(--__horizontal-gap) calc(100% - var(--__horizontal-gap)), transparent 100%);padding-left:calc(var(--__horizontal-gap) - 1px);padding-right:var(--__horizontal-gap);padding-top:var(--__horizontal-gap);padding-bottom:var(--__horizontal-gap);border-image-slice:1;max-width:fit-content}.dropdown-menu .dropdown-item .main-button{width:100%;padding:var(--__horizontal-gap) var(--__horizontal-gap);grid-gap:var(--__horizontal-gap);grid-template-columns:1fr var(--__line-height);grid-auto-columns:auto;grid-auto-flow:column}.dropdown-menu .dropdown-item .main-button .menu-checkbox{vertical-align:middle;min-width:calc(var(--__line-height) + 1px);max-width:calc(var(--__line-height) + 1px);min-height:calc(var(--__line-height) + 1px);max-height:calc(var(--__line-height) + 1px);line-height:var(--__line-height);text-align:center;box-shadow:var(--shadow);margin-right:var(--__horizontal-gap);border-radius:0;display:inline-block}.dropdown-menu .dropdown-item .main-button .menu-checkbox.menu-checkbox-checked:after{content:"✓";font-size:1.25em}.dropdown-menu .dropdown-item .main-button .menu-checkbox.menu-checkbox-indeterminate:after{content:"–";font-size:1.25em}.dropdown-menu .dropdown-item .main-button .menu-checkbox.-radio{border-radius:9999px}.dropdown-menu .dropdown-item .main-button .menu-checkbox.-radio.menu-checkbox-checked:after{content:"•";font-size:2em}.dropdown-menu .dropdown-item .main-button .menu-checkbox.-radio.menu-checkbox-indeterminate:after{content:"–";font-size:2em}.dropdown-menu .dropdown-item .main-button,.dropdown-menu .dropdown-item .extra-button{box-sizing:border-box;align-items:center;display:grid}.dropdown-menu .dropdown-item .main-button.disabled,.dropdown-menu .dropdown-item .extra-button.disabled{cursor:not-allowed}.dropdown-menu .dropdown-item .main-button:not(.disabled),.dropdown-menu .dropdown-item .extra-button:not(.disabled){cursor:pointer}.dropdown-menu .dropdown-item.-icon .main-button{grid-template-columns:var(--__line-height) 1fr}.dropdown-menu .dropdown-item.-icon-space .main-button{padding-left:calc(var(--__line-height) + var(--__horizontal-gap) * 2)}.dropdown-menu .dropdown-item.-icon-double .main-button{grid-template-columns:var(--__line-height) var(--__line-height) 1fr} diff --git a/priv/static/static/css/post_status_modal.C4svyRW2.css b/priv/static/static/css/post_status_modal.C4svyRW2.css new file mode 100644 index 000000000..7f1a2bc8f --- /dev/null +++ b/priv/static/static/css/post_status_modal.C4svyRW2.css @@ -0,0 +1 @@ +.modal-view.post-form-modal-view{align-items:flex-start}.post-form-modal-panel{flex-shrink:0;width:100%;max-width:700px;margin-top:25%;margin-bottom:2em}@media (orientation:landscape){.post-form-modal-panel{margin-top:8%}} diff --git a/priv/static/static/css/quote.DAX2X1-h.css b/priv/static/static/css/quote.DAX2X1-h.css new file mode 100644 index 000000000..36c83f3a1 --- /dev/null +++ b/priv/static/static/css/quote.DAX2X1-h.css @@ -0,0 +1 @@ +.quoted-status{border:1px solid var(--border);border-radius:var(--roundness);margin-top:.5em}.quoted-status.-unavailable-prompt{padding:.5em}.quoted-status .display-quoted-status-button{margin:.5em}.quoted-status .display-quoted-status-button-icon{color:inherit} diff --git a/priv/static/static/css/quote_form.ix5T3mDK.css b/priv/static/static/css/quote_form.ix5T3mDK.css new file mode 100644 index 000000000..a30ec5318 --- /dev/null +++ b/priv/static/static/css/quote_form.ix5T3mDK.css @@ -0,0 +1 @@ +.quote-form{flex-direction:column;padding:0 .5em .5em;display:flex}.quote-form .input-container{flex-direction:row;width:100%;display:flex}.quote-form .input-container input{width:100%}.quote-form .input-container .checkbox{white-space:pre;-webkit-column-gap:.5em;-webkit-user-select:none;user-select:none;flex-flow:row-reverse;align-items:center;column-gap:.5em;line-height:2;display:flex} diff --git a/priv/static/static/css/registration.BpinXGMn.css b/priv/static/static/css/registration.BpinXGMn.css new file mode 100644 index 000000000..a3bf58a09 --- /dev/null +++ b/priv/static/static/css/registration.BpinXGMn.css @@ -0,0 +1 @@ +.registration-form{flex-direction:column;margin:1em;display:flex}.registration-form .container{flex-direction:row;display:flex}.registration-form .container>*{min-width:0}.registration-form .terms-of-service{flex:0 50%;margin:.6em 0 0 .8em}.registration-form .text-fields{flex-direction:column;flex:1 0;margin-top:.6em;display:flex}.registration-form textarea{resize:vertical;min-height:100px}.registration-form .form-group{flex-direction:column;margin-bottom:1em;padding:.3em 0;line-height:2;display:flex}.registration-form .form-group--error{animation-name:shakeError;animation-duration:.6s;animation-timing-function:ease-in-out}.registration-form .form-group--error .form--label{color:var(--cRed)}.registration-form .form-error{text-align:left;margin-top:-.7em}.registration-form .form-error span{font-size:.85em}.registration-form .form-error ul{margin-top:0;padding:0 0 0 5px;list-style:none}.registration-form .form-error ul li:before{content:"• "}.registration-form form textarea{resize:vertical;line-height:1}.registration-form .captcha{max-width:350px;margin-bottom:.4em}.registration-form .btn{height:2em;margin-top:.6em}.registration-form .error{text-align:center}.registration-notice{margin:.6em}@media (width<=800px){.registration-form .container{flex-direction:column-reverse}.registration-form .container .terms-of-service{margin:0}} diff --git a/priv/static/static/css/remote_follow.BrTyl75C.css b/priv/static/static/css/remote_follow.BrTyl75C.css new file mode 100644 index 000000000..5dcf7a920 --- /dev/null +++ b/priv/static/static/css/remote_follow.BrTyl75C.css @@ -0,0 +1 @@ +.remote-follow{max-width:220px}.remote-follow .remote-button{width:100%;min-height:2em} diff --git a/priv/static/static/css/search.BMRqSZdz.css b/priv/static/static/css/search.BMRqSZdz.css new file mode 100644 index 000000000..d69ac9cd2 --- /dev/null +++ b/priv/static/static/css/search.BMRqSZdz.css @@ -0,0 +1 @@ +.search-result-heading{color:var(--faint);text-align:center;padding:.75rem}@media (width<=800px){.search-nav-heading .tab-switcher .tabs .tab-wrapper{text-align:center;flex:auto;justify-content:center;display:block}}.search-result{box-sizing:border-box;border-bottom:1px solid;border-color:var(--border)}.search-input-container{justify-content:center;padding:.8rem;display:flex}.search-input-container .search-input{box-sizing:border-box;width:100%;padding:.5rem;font-size:1rem;line-height:1.125rem}.search-input-container .search-button{margin-left:.5em}.loading-icon{padding:1em}.trend{align-items:center;display:flex}.trend .hashtag{text-overflow:ellipsis;white-space:nowrap;flex:auto;overflow:hidden}.trend .hashtag .name{color:var(--link)}.trend .count{text-align:center;width:2rem;color:var(--text);flex:none;font-size:1.5rem;font-weight:500;line-height:2.25rem}.more-statuses-button{width:100%;height:3.5em;line-height:3.5em} diff --git a/priv/static/static/css/select.BF2ygbkJ.css b/priv/static/static/css/select.BF2ygbkJ.css new file mode 100644 index 000000000..7b87419bd --- /dev/null +++ b/priv/static/static/css/select.BF2ygbkJ.css @@ -0,0 +1 @@ +label.Select{padding:0;display:flex}label.Select select{appearance:none;color:var(--text);font-family:var(--font);z-index:1;background:0 0;border:none;width:100%;margin:0;padding:0 2em 0 .5em;font-size:1em;line-height:2}label.Select select[multiple],label.Select select[size]{height:100%;padding:.2em}label.Select select[multiple] option,label.Select select[size] option{background-color:#0000}label.Select select[multiple] option:checked,label.Select select[multiple] option.-active,label.Select select[size] option:checked,label.Select select[size] option.-active{color:var(--selectionText);background-color:var(--selectionBackground)}label.Select.disabled,label.Select:disabled{background-color:var(--background);opacity:1;color:var(--faint)}label.Select.disabled select[multiple] option.-active,label.Select.disabled select[size] option.-active,label.Select:disabled select[multiple] option.-active,label.Select:disabled select[size] option.-active{color:var(--faint);background:0 0}label.Select .select-down-icon{width:.875em;height:100%;font-family:var(--font);z-index:1;pointer-events:none;line-height:2;position:absolute;top:0;bottom:0;right:.5em} diff --git a/priv/static/static/css/settings_modal._8lsImIr.css b/priv/static/static/css/settings_modal._8lsImIr.css new file mode 100644 index 000000000..137a4352e --- /dev/null +++ b/priv/static/static/css/settings_modal._8lsImIr.css @@ -0,0 +1 @@ +.async-component-error{justify-content:center;align-items:center;height:100%;display:flex}.async-component-error .btn{margin:.5em;padding:.5em 2em}.settings-modal{overflow:hidden}.settings-modal h2{margin-top:1em;margin-bottom:1em;margin-right:1em;font-size:1.3rem;font-weight:500}.settings-modal h3{border-bottom:1px solid var(--border);box-sizing:border-box;margin-top:1em;margin-bottom:.5em;margin-right:1em;padding-bottom:.25em;padding-left:.5em;font-size:1.2rem;font-weight:500}.settings-modal h4{margin:1em 1em .5em;font-size:1.1rem}.settings-modal h5{margin-top:.75em;margin-bottom:.25em;margin-left:1em;font-size:1rem}.settings-modal .toolbar{flex-wrap:wrap;align-items:end;gap:.5em;display:flex}.settings-modal .toolbar .header-buttons{flex:1 0 auto;justify-content:end;align-items:end;display:flex}.settings-modal .toolbar .header-buttons:not(.btn-group){gap:.5em}.settings-modal .toolbar .header-buttons .header-text{flex:1 0 auto}.settings-modal .toolbar .button-default{flex:none;padding:.5em;font-size:1rem}.settings-modal .toolbar .popover-wrapper{display:flex}.settings-modal p{margin-left:2em;line-height:1.5}.settings-modal .sidenote{margin-top:.25em;margin-left:5em;padding:.25em 1em}.settings-modal .setting-description{margin-top:.2em;margin-bottom:0;margin-left:0;font-size:80%}.settings-modal .setting-item{-webkit-column-gap:.5em;grid-template-columns:4fr 5fr;grid-template-areas:"label control""label desc"".draft";align-items:baseline;column-gap:.5em;padding:.5em 0;line-height:1.5em;display:grid}.settings-modal .setting-item .setting-label{text-align:right;grid-area:label}.settings-modal .setting-item .ModifiedIndicator,.settings-modal .setting-item .LocalSettingIndicator{grid-area:indicator}.settings-modal .setting-item .setting-control{grid-area:control}.settings-modal .setting-item .setting-control.textarea{align-self:baseline;overflow:auto}.settings-modal .setting-item .setting-control.checkbox{grid-template-columns:subgrid;display:grid}.settings-modal .setting-item .setting-control.checkbox .label{text-align:right;grid-area:label}.settings-modal .setting-item .setting-control.checkbox .checkbox-indicator{grid-area:control;align-self:baseline;height:1.5em;line-height:1.5em}.-mobile .settings-modal .setting-item .setting-control.checkbox .label{text-align:left}.settings-modal .setting-item .setting-control.setting-label{text-align:left;grid-column:1/3}.settings-modal .setting-item .setting-description{grid-area:desc}.settings-modal .setting-item .DraftButtons{grid-area:draft}.settings-modal .vertical-tab-switcher{height:100%}.settings-modal .setting-list,.settings-modal .option-list{margin:0;padding-left:0;list-style-type:none}.settings-modal .setting-list.suboptions,.settings-modal .option-list.suboptions{border-top:1px dotted var(--border);border-bottom:1px dotted var(--border);margin-left:2em}.settings-modal .setting-list .btn:not(.dropdown-button),.settings-modal .option-list .btn:not(.dropdown-button){padding:0 2em}.settings-modal .setting-list .btn-group .button-default,.settings-modal .option-list .btn-group .button-default{flex:0 auto}.settings-modal .setting-list.two-column,.settings-modal .option-list.two-column{border-bottom:1px solid var(--border);grid-template-columns:1fr 1fr;margin-bottom:1em;margin-left:2em;padding-bottom:.5em;display:grid}.settings-modal .setting-list.two-column .setting-item,.settings-modal .option-list.two-column .setting-item{grid-template-columns:3fr 1fr}.settings-modal .setting-list.two-column>li,.settings-modal .option-list.two-column>li{-webkit-column-break-inside:avoid;break-inside:avoid;margin:0}.settings-modal .settings-modal-panel{transition:transform;width:70em;max-width:90vw;height:90vh;transition-duration:.3s;transition-timing-function:ease-in-out;overflow:hidden}@media (width<=800px){.settings-modal .settings-modal-panel{max-width:100vw;height:100%}}.settings-modal .settings-modal-panel>.panel-body{height:100%;overflow-y:hidden}.settings-modal .settings-modal-panel>.panel-body .btn{min-height:2em}.settings-modal .settings-footer{flex-wrap:wrap;line-height:2;display:flex}.settings-modal .settings-footer>*{margin-right:.5em}.settings-modal .settings-footer .extra-content{flex-grow:1;display:flex}.settings-modal.-mobile .tabs .menu-item{padding-top:.75em;padding-bottom:.75em;font-size:1.2em}.settings-modal.-mobile li .sidenote{margin-left:1em}.settings-modal.-mobile .setting-item{grid-template-columns:1fr -webkit-min-content;-webkit-column-gap:.5em;grid-template-columns:1fr min-content;align-items:center;column-gap:.5em;padding:1em}.settings-modal.-mobile .setting-item .setting-label{text-align:left}.settings-modal.-mobile .setting-item .checkbox .label{text-align:left;order:2}.settings-modal.-mobile .setting-item .checkbox .checkbox-indicator{order:1}.settings-modal.-mobile ul{padding:0}.settings-modal.-mobile ul li:not(:first-child) .setting-item{border-top:1px solid var(--border)}.settings-modal.-mobile .setting-list:not(.suboptions).two-column,.settings-modal.-mobile .option-list.two-column{grid-template-columns:1fr}.settings-modal.-mobile .UnitSetting{padding-right:.5em}.settings-modal.minimize .settings-modal-panel{transform:translateY(calc(50vh + 50% - 50px))}@media (width<=800px){.settings-modal.minimize .settings-modal-panel{transform:translateY(calc(100% - 50px))}} diff --git a/priv/static/static/css/settings_modal.dQ5YFZGx.css b/priv/static/static/css/settings_modal.dQ5YFZGx.css deleted file mode 100644 index 7dfd4e1c4..000000000 --- a/priv/static/static/css/settings_modal.dQ5YFZGx.css +++ /dev/null @@ -1 +0,0 @@ -.async-component-error{display:-webkit-box;display:flex;height:100%;-webkit-box-align:center;align-items:center;-webkit-box-pack:center;justify-content:center}.async-component-error .btn{margin:.5em;padding:.5em 2em}.settings-modal{overflow:hidden}.settings-modal h2{font-size:1.3rem;font-weight:500;margin-top:1em;margin-bottom:1em;margin-right:1em}.settings-modal h3{font-size:1.2rem;font-weight:500;margin-top:1em;margin-bottom:.5em;margin-right:1em;border-bottom:1px solid var(--border);padding-bottom:.25em;box-sizing:border-box;padding-left:.5em}.settings-modal h4{font-size:1.1rem;margin:1em 1em .5em}.settings-modal h5{font-size:1rem;margin-left:1em;margin-bottom:.25em;margin-top:.75em}.settings-modal p{line-height:1.5}.settings-modal .suboptions{margin-left:1em}.settings-modal .sidenote{margin-left:5em;padding:.25em 1em;margin-top:.25em}.settings-modal .setting-description{margin-top:.2em;margin-bottom:0;font-size:80%}.settings-modal .setting-item{display:grid;grid-template-areas:"label control" "label desc" ". draft";grid-template-columns:4fr 5fr;-webkit-column-gap:.5em;column-gap:.5em;-webkit-box-align:baseline;align-items:baseline;padding:.5em 0}.settings-modal .setting-item .setting-label{grid-area:label;text-align:right}.settings-modal .setting-item .ModifiedIndicator,.settings-modal .setting-item .ProfileSettingIndicator{grid-area:indicator}.settings-modal .setting-item .setting-control{grid-area:control}.settings-modal .setting-item .setting-control.textarea{align-self:baseline;overflow:auto}.settings-modal .setting-item .setting-control.checkbox{display:grid;grid-template-columns:subgrid}.settings-modal .setting-item .setting-control.checkbox .label{grid-area:label;text-align:right}.settings-modal .setting-item .setting-control.checkbox .checkbox-indicator{grid-area:control}.-mobile .settings-modal .setting-item .setting-control.checkbox .label{text-align:left}.settings-modal .setting-item .setting-control.setting-label{grid-column:1/3;text-align:left}.settings-modal .setting-item .setting-description{grid-area:desc}.settings-modal .setting-item .DraftButtons{grid-area:draft}.settings-modal .vertical-tab-switcher{height:100%}.settings-modal .setting-list,.settings-modal .option-list{list-style-type:none;padding-left:0;margin:0}.settings-modal .setting-list .btn:not(.dropdown-button),.settings-modal .option-list .btn:not(.dropdown-button){padding:0 2em}.settings-modal .setting-list .btn-group .button-default,.settings-modal .option-list .btn-group .button-default{-webkit-box-flex:0;flex:0 1 auto}.settings-modal .setting-list.two-column,.settings-modal .option-list.two-column{display:grid;grid-template-columns:1fr 1fr;margin-left:2em;border-bottom:1px solid var(--border);padding-bottom:.5em;margin-bottom:1em}.settings-modal .setting-list.two-column .setting-item,.settings-modal .option-list.two-column .setting-item{grid-template-columns:3fr 1fr}.settings-modal .setting-list.two-column>li,.settings-modal .option-list.two-column>li{margin:0;-webkit-column-break-inside:avoid;break-inside:avoid}.settings-modal .settings-modal-panel{overflow:hidden;-webkit-transition:-webkit-transform;transition:-webkit-transform;transition:transform;transition:transform,-webkit-transform;-webkit-transition-timing-function:ease-in-out;transition-timing-function:ease-in-out;-webkit-transition-duration:.3s;transition-duration:.3s;width:70em;max-width:90vw;height:90vh}@media all and (width <= 800px){.settings-modal .settings-modal-panel{max-width:100vw;height:100%}}.settings-modal .settings-modal-panel>.panel-body{height:100%;overflow-y:hidden}.settings-modal .settings-modal-panel>.panel-body .btn{min-height:2em}.settings-modal .settings-footer{display:-webkit-box;display:flex;flex-wrap:wrap;line-height:2}.settings-modal .settings-footer>*{margin-right:.5em}.settings-modal .settings-footer .extra-content{display:-webkit-box;display:flex;-webkit-box-flex:1;flex-grow:1}.settings-modal.-mobile .tabs .menu-item{font-size:1.2em;padding-top:.75em;padding-bottom:.75em}.settings-modal.-mobile .setting-item{grid-template-columns:1fr -webkit-min-content;grid-template-columns:1fr min-content;-webkit-column-gap:.5em;column-gap:.5em;padding:1em;-webkit-box-align:center;align-items:center}.settings-modal.-mobile .setting-item .setting-label{text-align:left}.settings-modal.-mobile .setting-item .checkbox .label{text-align:left;margin-left:0}.settings-modal.-mobile ul{padding:0}.settings-modal.-mobile ul li:not(:first-child) .setting-item{border-top:1px solid var(--border)}.settings-modal.-mobile .setting-list:not(.suboptions).two-column,.settings-modal.-mobile .option-list.two-column{grid-template-columns:1fr}.settings-modal.peek .settings-modal-panel{-webkit-transform:translateY(calc((100vh - 100%)/2 + 100% - 50px));transform:translateY(calc((100vh - 100%)/2 + 100% - 50px))}@media all and (width <= 800px){.settings-modal.peek .settings-modal-panel{-webkit-transform:translateY(calc(100% - 50px));transform:translateY(calc(100% - 50px))}} diff --git a/priv/static/static/css/settings_modal_admin_content.CKTEQiM8.css b/priv/static/static/css/settings_modal_admin_content.CKTEQiM8.css new file mode 100644 index 000000000..0e18eace4 --- /dev/null +++ b/priv/static/static/css/settings_modal_admin_content.CKTEQiM8.css @@ -0,0 +1 @@ +.AttachmentSetting .attachment{width:100%;height:15em;margin-bottom:.5em;display:block}.AttachmentSetting .attachment-input{flex-direction:column;width:20em;margin-left:1em;display:flex}.AttachmentSetting.-compact .attachment-input{flex-direction:row;align-items:flex-end}.AttachmentSetting.-compact .attachment{flex:0;order:0;align-self:center;min-width:4em;height:4em;margin-bottom:0;display:block}.AttachmentSetting.-compact .control-field{order:1;min-width:12em;margin-left:.5em}.AttachmentSetting.-compact .control-upload{order:2;min-width:12em;padding:0 .5em}.AttachmentSetting .controls{margin-bottom:.5em}.AttachmentSetting .controls input,.AttachmentSetting .controls button{width:100%}.ListSetting .setting-list .checkbox{padding:.5em 0}.MapSetting .btn-group{display:flex}.MapSetting dl{grid-template-columns:auto auto;gap:.5em;display:inline-grid}.MapSetting dl dt{font-weight:800;display:inline}.MapSetting dl dt:after{content:":"}.MapSetting dl dd{margin:0;display:inline}.emoji-tab-edit-popover{padding-bottom:.5em;padding-left:.5em;padding-right:.5em}.emoji-tab-edit-popover .emoji-tab-popover-new-upload{margin-bottom:2em}.emoji-tab-edit-popover .emoji{width:2.3em;height:2.3em}.emoji-tab-edit-popover .Select{display:inline-block}.emoji-tab-edit-popover h4{margin-top:1em;margin-bottom:1em}.EmojiTab .setting-list{margin:.5em 2em}.EmojiTab .setting-section{margin-left:.5em;margin-right:.5em}.EmojiTab .toolbar{flex-wrap:wrap;gap:.5em;display:flex}.EmojiTab .toolbar .header-buttons{flex:1 0 auto;justify-content:end;align-items:end;display:flex}.EmojiTab .toolbar .header-buttons:not(.btn-group){gap:.5em}.EmojiTab .toolbar .header-buttons .header-text{flex:1 0 auto}.EmojiTab .toolbar .button-default{flex:none;padding:.5em;font-size:1rem}.EmojiTab .toolbar .popover-wrapper{display:flex}.EmojiTab .selector-buttons,.EmojiTab .meta-buttons{flex-wrap:wrap;gap:.5em;margin-left:1em;display:flex}.EmojiTab h5{margin-top:1em;margin-bottom:.25em}.EmojiTab h3.toolbar{align-items:end}.EmojiTab .emoji-info-input{width:100%}.EmojiTab .emoji-data-input{width:40%;margin-left:.5em;margin-right:.5em}.EmojiTab .emoji{width:2em;height:2em}.EmojiTab .emoji-unsaved{box-shadow:0 3px 5px var(--cBlue)}.EmojiTab .emoji-list{flex-wrap:wrap;gap:1em;display:flex}.EmojiTab .emoji-list .button-unstyled{display:flex}.EmojiTab .emoji-list .emoji-item,.EmojiTab .emoji-list .placeholder{opacity:.5;width:2em;height:2em}.EmojiTab .emoji-list .placeholder{background:var(--textFaint);border-radius:.5em}.emoji-tab-popover-button:not(:first-child){margin-left:.5em}.emoji-tab-popover-input{margin-bottom:.5em}.emoji-tab-popover-input label{margin-bottom:.5em;display:block}.emoji-tab-popover-input input{width:20em}.emoji-tab-popover-input .emoji-tab-popover-file{padding-top:3px}.emoji-tab-popover-input .warning{color:var(--cOrange)}.ListTupleSetting .btn-group{display:flex}.ListTupleSetting dl{grid-template-columns:auto auto;align-items:baseline;gap:.5em;display:inline-grid}.ListTupleSetting dl dt{font-weight:800;display:inline}.ListTupleSetting dl dt:after{content:":"}.ListTupleSetting dl dd{margin:0;display:inline}.FrontendsTab{padding:0 1em}.FrontendsTab .cards-list{padding:0}.FrontendsTab li.frontend-card{flex-direction:column;margin:0;display:flex}.FrontendsTab .frontend-buttons{flex-wrap:wrap;flex:1 0 auto;justify-content:end;align-items:end;gap:.5em;margin-top:.5em;display:flex}.FrontendsTab .relative{position:relative}.FrontendsTab .overlay{background:var(--bg);z-index:2;opacity:.9;position:absolute;inset:0}.FrontendsTab h5{text-transform:capitalize;margin:0;font-size:1.15em}.FrontendsTab dl{margin-left:1em}.FrontendsTab dt{text-overflow:ellipsis;white-space:nowrap;margin-top:.5em;overflow-x:hidden}.FrontendsTab dd{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.ColorSetting.setting-item{grid-template-areas:"label control"".desc"".draft";display:grid}.ColorSetting.setting-item .setting-label{text-align:right;align-self:center}.ColorSetting.setting-item .setting-control{align-self:end}.ColorSetting .color-setting-input{align-self:baseline}div.PWAManifestIconsSetting{margin-left:3em}div.PWAManifestIconsSetting.setting-item{grid-template:"label"2em"desc""control"1fr"draft"/1fr;display:grid}div.PWAManifestIconsSetting .pwa-label.setting-label{text-align:left;align-self:end}div.PWAManifestIconsSetting .buttons{gap:.5em;-webkit-box-pack:right;justify-content:right;margin-top:.5em;display:flex}div.PWAManifestIconsSetting .buttons button{line-height:2}div.PWAManifestIconsSetting ul{grid-gap:2em;grid-template-columns:repeat(auto-fit,12em);list-style:none;display:grid}div.PWAManifestIconsSetting dl{grid-template-columns:1fr;align-items:baseline;gap:.25em;margin-top:.5em;display:grid}div.PWAManifestIconsSetting dl dt{font-weight:800}div.PWAManifestIconsSetting dl dt:after{content:":"}div.PWAManifestIconsSetting dl dd{margin:0}div.PWAManifestIconsSetting .src-field{grid-template-columns:auto;justify-items:center;gap:.5em;display:grid}div.PWAManifestIconsSetting .src-field .src-attachment{width:10em;height:10em;margin-bottom:.5em;display:block}div.PWAManifestIconsSetting .src-field .src-upload{width:100%;display:block}div.PWAManifestIconsSetting .src-field .src-url{flex-direction:column;gap:.25em;width:100%;display:flex}div.PWAManifestIconsSetting .src-field .src-url label{display:block}.RateSetting.setting-item{grid-template-areas:"label control""desc control"".draft";display:grid}.RateSetting.setting-item .setting-label{text-align:right;align-self:center}.RateSetting.setting-item .setting-description{text-align:right}.RateSetting.setting-item .setting-control{align-self:end}.RateSetting table{margin-top:.5em}.RateSetting th{font-weight:400}.RateSetting td input{width:15ch}.RateSetting{margin-bottom:2em}.AdminUserCard details{white-space:normal}.AdminUserCard details summary{font-weight:700}.AdminUserCard details span{resize:vertical;max-height:6.5em;display:block;overflow-y:auto}.AdminUserCard .right-side{flex-wrap:wrap;justify-content:end;align-items:baseline;gap:.5em;margin-top:.5em;display:flex}.AdminUserCard .right-side .alert{margin:0}.UsersTab{flex-direction:column;height:100%;padding:0;display:flex}.UsersTab h3{margin-left:.5em;margin-right:.5em}.UsersTab .splitter{height:100%;display:flex;overflow-y:hidden}.UsersTab .filters-section{border-right:1px solid var(--border);flex-direction:column;flex:none;gap:1em;margin-right:1em;padding:1em;display:flex;overflow:auto visible}.UsersTab .filters-section>div{flex:0 auto}.UsersTab .filters-section .filter{min-width:14em;display:block}.UsersTab .filters-section .filter .query-label{margin-bottom:.5em}.UsersTab .filters-section .filter>input{width:100%}.UsersTab .users-list{flex:1 0 30em}.settings-admin-content .setting-item{grid-template-columns:1fr 3fr} diff --git a/priv/static/static/css/settings_modal_admin_content.d1pRGdhX.css b/priv/static/static/css/settings_modal_admin_content.d1pRGdhX.css deleted file mode 100644 index cc2da389c..000000000 --- a/priv/static/static/css/settings_modal_admin_content.d1pRGdhX.css +++ /dev/null @@ -1 +0,0 @@ -.ColorSetting.setting-item{display:grid;grid-template-areas:"label control" ". desc" ". draft"}.ColorSetting.setting-item .setting-label{text-align:right;align-self:center}.ColorSetting.setting-item .setting-control{align-self:end}.ColorSetting .color-setting-input{align-self:baseline}.AttachmentSetting .attachment{display:block;width:100%;height:15em;margin-bottom:.5em}.AttachmentSetting .attachment-input{margin-left:1em;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;width:20em}.AttachmentSetting.-compact .attachment-input{-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-direction:row;-webkit-box-align:end;align-items:flex-end}.AttachmentSetting.-compact .attachment{-webkit-box-flex:0;flex:0;-webkit-box-ordinal-group:1;order:0;display:block;min-width:4em;height:4em;align-self:center;margin-bottom:0}.AttachmentSetting.-compact .control-field{-webkit-box-ordinal-group:2;order:1;min-width:12em;margin-left:.5em}.AttachmentSetting.-compact .control-upload{-webkit-box-ordinal-group:3;order:2;min-width:12em;padding:0 .5em}.AttachmentSetting .controls{margin-bottom:.5em}.AttachmentSetting .controls input,.AttachmentSetting .controls button{width:100%}.ListSetting .setting-list .checkbox{padding:.5em 0}div.PWAManifestIconsSetting{margin-left:3em}div.PWAManifestIconsSetting.setting-item{display:grid;grid-template-areas:"label" "desc" "control" "draft";grid-template-rows:2em auto 1fr;grid-template-columns:1fr}div.PWAManifestIconsSetting .pwa-label.setting-label{align-self:end;text-align:left}div.PWAManifestIconsSetting .buttons{display:-webkit-box;display:flex;gap:.5em;-webkit-box-pack:right;justify-content:right;margin-top:.5em}div.PWAManifestIconsSetting .buttons button{line-height:2}div.PWAManifestIconsSetting ul{list-style:none;display:grid;grid-template-columns:repeat(auto-fit,12em);grid-gap:2em}div.PWAManifestIconsSetting dl{display:grid;grid-template-columns:1fr;margin-top:.5em;gap:.25em;-webkit-box-align:baseline;align-items:baseline}div.PWAManifestIconsSetting dl dt{font-weight:800}div.PWAManifestIconsSetting dl dt:after{content:":"}div.PWAManifestIconsSetting dl dd{margin:0}div.PWAManifestIconsSetting .src-field{display:grid;grid-template-columns:auto;justify-items:center;gap:.5em}div.PWAManifestIconsSetting .src-field .src-attachment{width:10em;height:10em;display:block;margin-bottom:.5em}div.PWAManifestIconsSetting .src-field .src-upload{display:block;width:100%}div.PWAManifestIconsSetting .src-field .src-url{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;gap:.25em;width:100%}div.PWAManifestIconsSetting .src-field .src-url label{display:block}.MapSetting .btn-group{display:-webkit-box;display:flex}.MapSetting dl{display:inline-grid;grid-template-columns:auto auto;gap:.5em}.MapSetting dl dt{display:inline;font-weight:800}.MapSetting dl dt:after{content:":"}.MapSetting dl dd{display:inline;margin:0}.FrontendsTab .cards-list{padding:0}.FrontendsTab li.frontend-card{display:-webkit-box;display:flex;margin:0;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column}.FrontendsTab .frontend-buttons{margin-top:.5em;display:-webkit-box;display:flex;-webkit-box-pack:end;justify-content:end;gap:.5em;flex-wrap:wrap;-webkit-box-flex:1;flex:1 0 auto;-webkit-box-align:end;align-items:end}.FrontendsTab .relative{position:relative}.FrontendsTab .overlay{position:absolute;background:var(--bg);z-index:2;opacity:.9;top:0;right:0;bottom:0;left:0}.FrontendsTab h5{margin:0;font-size:1.15em}.FrontendsTab dl{margin-left:1em}.FrontendsTab dt{margin-top:.5em;text-overflow:ellipsis;white-space:nowrap;overflow-x:hidden}.FrontendsTab dd{text-overflow:ellipsis;white-space:nowrap;overflow-x:hidden}.emoji-tab-edit-popover{padding-left:.5em;padding-right:.5em;padding-bottom:.5em}.emoji-tab-edit-popover .emoji-tab-popover-new-upload{margin-bottom:2em}.emoji-tab-edit-popover .emoji{width:2.3em;height:2.3em}.emoji-tab-edit-popover .Select{display:inline-block}.emoji-tab-edit-popover h4{margin-bottom:1em;margin-top:1em}.EmojiTab .setting-list{margin:.5em 2em}.EmojiTab .toolbar{display:-webkit-box;display:flex;flex-wrap:wrap;gap:.5em}.EmojiTab .toolbar .header-buttons{display:-webkit-box;display:flex;-webkit-box-flex:1;flex:1 0 auto;-webkit-box-pack:end;justify-content:end;-webkit-box-align:end;align-items:end}.EmojiTab .toolbar .header-buttons:not(.btn-group){gap:.5em}.EmojiTab .toolbar .header-buttons .header-text{-webkit-box-flex:1;flex:1 0 auto}.EmojiTab .toolbar .button-default{-webkit-box-flex:0;flex:0 0 auto;padding:.5em;font-size:1rem}.EmojiTab .toolbar .popover-wrapper{display:-webkit-box;display:flex}.EmojiTab .selector-buttons,.EmojiTab .meta-buttons{display:-webkit-box;display:flex;flex-wrap:wrap;gap:.5em;margin-left:1em}.EmojiTab h5{margin-top:1em;margin-bottom:.25em}.EmojiTab h3.toolbar{-webkit-box-align:end;align-items:end}.EmojiTab .emoji-info-input{width:100%}.EmojiTab .emoji-data-input{width:40%;margin-left:.5em;margin-right:.5em}.EmojiTab .emoji{width:2em;height:2em}.EmojiTab .emoji-unsaved{box-shadow:0 3px 5px var(--cBlue)}.EmojiTab .emoji-list{display:-webkit-box;display:flex;flex-wrap:wrap;gap:1em}.EmojiTab .emoji-list .button-unstyled{display:-webkit-box;display:flex}.EmojiTab .emoji-list .emoji-item,.EmojiTab .emoji-list .placeholder{width:2em;height:2em;opacity:.5}.EmojiTab .emoji-list .placeholder{background:var(--textFaint);border-radius:.5em}.emoji-tab-popover-button:not(:first-child){margin-left:.5em}.emoji-tab-popover-input{margin-bottom:.5em}.emoji-tab-popover-input label{display:block;margin-bottom:.5em}.emoji-tab-popover-input input{width:20em}.emoji-tab-popover-input .emoji-tab-popover-file{padding-top:3px}.emoji-tab-popover-input .warning{color:var(--cOrange)}.RateSetting{margin-bottom:2em}.RateSetting.setting-item{display:grid;grid-template-areas:"label control" "desc control" ". draft"}.RateSetting.setting-item .setting-label{text-align:right;align-self:center}.RateSetting.setting-item .setting-description{text-align:right}.RateSetting.setting-item .setting-control{align-self:end}.RateSetting table{margin-top:.5em}.RateSetting th{font-weight:400}.RateSetting td input{width:15ch}.ListTupleSetting .btn-group{display:-webkit-box;display:flex}.ListTupleSetting dl{display:inline-grid;grid-template-columns:auto auto;gap:.5em;-webkit-box-align:baseline;align-items:baseline}.ListTupleSetting dl dt{display:inline;font-weight:800}.ListTupleSetting dl dt:after{content:":"}.ListTupleSetting dl dd{display:inline;margin:0}.settings-admin-content .setting-item{grid-template-columns:1fr 3fr} diff --git a/priv/static/static/css/settings_modal_user_content.7KpKOwxU.css b/priv/static/static/css/settings_modal_user_content.7KpKOwxU.css deleted file mode 100644 index 7287404d6..000000000 --- a/priv/static/static/css/settings_modal_user_content.7KpKOwxU.css +++ /dev/null @@ -1 +0,0 @@ -.importer-uploading{font-size:1.5em;margin:.25em}.exporter{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column}.exporter-processing{margin:.25em}.data-import-export-tab .importer-exporter{display:-webkit-inline-box;display:inline-flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;gap:.5em}.data-import-export-tab table td,.data-import-export-tab table th{line-height:1.5}.data-import-export-tab table th{padding:0 .5em}.data-import-export-tab table td{padding:.5em}.autosuggest{position:relative}.autosuggest-input{display:block;width:100%}.autosuggest-results{position:absolute;left:0;top:100%;right:0;max-height:400px;background-color:var(--bg);border-style:solid;border-width:1px;border-color:var(--border);border-radius:var(--roundness);border-top-left-radius:0;border-top-right-radius:0;box-shadow:1px 1px 4px #0009;box-shadow:var(--shadow);overflow-y:auto;z-index:1}.block-card-content-container{margin-top:.5em;text-align:right}.block-card-content-container button{width:10em}.mute-card-content-container{margin-top:.5em;text-align:right}.mute-card-content-container button{width:10em}.domain-mute-card{-webkit-box-flex:1;flex:1 0;display:-webkit-box;display:flex;-webkit-box-pack:justify;justify-content:space-between;-webkit-box-align:center;align-items:center;padding:.6em 1em .6em 0}.domain-mute-card-domain{margin-right:1em;overflow:hidden;text-overflow:ellipsis}.domain-mute-card button{width:10em}.autosuggest-results .domain-mute-card{padding-left:1em}.selectable-list{--__line-height: 1.5em;--__horizontal-gap: .75em;--__vertical-gap: .5em}.selectable-list-item-inner{display:-webkit-box;display:flex;-webkit-box-align:center;align-items:center}.selectable-list-item-inner>*{min-width:0}.selectable-list-header{display:-webkit-box;display:flex;-webkit-box-align:center;align-items:center;padding:var(--__vertical-gap) var(--__horizontal-gap);border-bottom:1px solid;border-bottom-color:var(--border)}.selectable-list-header-actions{-webkit-box-flex:1;flex:1}.selectable-list-checkbox-wrapper{padding-right:var(--__horizontal-gap);-webkit-box-flex:0;flex:none}.with-subscription-loading{padding:.7em;text-align:center}.with-subscription-loading .error{font-size:1rem}.mutes-and-blocks-tab{height:100%}.mutes-and-blocks-tab .usersearch-wrapper{padding:1em}.mutes-and-blocks-tab .bulk-actions{text-align:right;padding:0 1em;min-height:2em}.mutes-and-blocks-tab .bulk-action-button{width:10em}.mutes-and-blocks-tab .domain-mute-form{padding:1em;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column}.mutes-and-blocks-tab .domain-mute-button{align-self:flex-end;margin-top:1em;width:10em}.UnitSetting .no-break{display:-webkit-inline-box;display:inline-flex}.UnitSetting .number-input{max-width:6.5em;text-align:right}.UnitSetting .unit-input,.UnitSetting .unit-input select{min-width:4em;width:auto}.HelpIndicator{display:inline-block;position:relative}.help-tooltip{margin:.5em 1em;min-width:10em;max-width:30vw;text-align:center}.filtering-tab .muteFilterContainer{border:1px solid var(--border);border-radius:var(--roundness);height:33vh;overflow-y:auto}.filtering-tab .mute-filter{border:1px solid var(--border);border-radius:var(--roundness);margin:.5em;padding:.5em;display:grid;-webkit-box-align:baseline;align-items:baseline;grid-gap:.5em}.filtering-tab .never{margin-left:.5em}.filtering-tab .filter-name{grid-column:1/span 2;grid-row:1}.filtering-tab .alert,.filtering-tab .button-default{display:inline-block;line-height:2;padding:0 .5em}.filtering-tab .filter-enabled{grid-column:3;grid-row:1;text-align:right;line-height:2}.filtering-tab .filter-field{display:grid;grid-template-columns:subgrid;grid-template-rows:subgrid;grid-column:1/span 3;-webkit-box-align:baseline;align-items:baseline}.filtering-tab .filter-field label{grid-column:1;text-align:right}.filtering-tab .filter-field .filter-field-value{grid-column:2/span 2}.filtering-tab .filter-buttons{grid-column:1/span 3;justify-self:end;display:inline-grid;grid-gap:.5em;grid-template-columns:repeat(4,1fr);max-width:100%;-webkit-box-pack:end;justify-content:end}.filtering-tab .muteFiltersActions,.filtering-tab .muteFiltersActionsBottom{display:grid;-webkit-box-align:baseline;align-items:baseline;grid-auto-flow:column;grid-template-columns:1fr;grid-gap:.5em;margin:.5em 0}.filtering-tab .muteFiltersActions .total,.filtering-tab .muteFiltersActionsBottom .total{-webkit-box-flex:1;flex:1}.settings-modal.-mobile .filtering-tab .filter-buttons{grid-template-columns:repeat(1,1fr)}.mfa-backup-codes .warning{color:var(--cOrange)}.mfa-backup-codes .backup-codes{font-family:var(--monoFont)}.mfa-settings .mfa-heading,.mfa-settings .method-item{display:-webkit-box;display:flex;flex-wrap:wrap;-webkit-box-pack:justify;justify-content:space-between;-webkit-box-align:baseline;align-items:baseline}.mfa-settings .warning{color:var(--cOrange)}.mfa-settings .setup-otp{display:-webkit-box;display:flex;-webkit-box-pack:center;justify-content:center;flex-wrap:wrap}.mfa-settings .setup-otp .qr-code{-webkit-box-flex:1;flex:1;padding-right:.7em}.mfa-settings .setup-otp .verify{-webkit-box-flex:1;flex:1}.mfa-settings .setup-otp .error{margin:.3em 0 0}.mfa-settings .setup-otp .confirm-otp-actions button{width:15em;margin-top:5px}.profile-tab .setting-section.profile-edit{margin:0}.profile-tab .setting-section.profile-edit h2{margin-left:1rem}.profile-tab .setting-section.profile-edit .user-card{padding:1.2em;overflow:hidden}.profile-tab .setting-item .custom-boolean-setting{display:grid;grid-template-columns:subgrid}.profile-tab .setting-item .custom-boolean-setting .label{grid-area:label;text-align:right}.-mobile .profile-tab .setting-item .custom-boolean-setting .label{text-align:left}.profile-tab .setting-item .custom-boolean-setting .checkbox-indicator{grid-area:control}.invalid-tooltip{margin:.5em 1em;min-width:10em;text-align:center}.posts-tab .greentext{color:var(--funtextGreentext)}.PaletteEditor{justify-content:space-around;grid-template-columns:repeat(4,1fr);grid-template-rows:repeat(5,1fr) auto;grid-gap:.5em;-webkit-box-align:baseline;align-items:baseline}.PaletteEditor .buttons{margin-top:.5em;display:grid;gap:.5em}.PaletteEditor .palette{display:grid;grid-template-rows:1fr;grid-auto-flow:row;grid-auto-rows:auto;grid-template-columns:repeat(auto-fill,10em);grid-gap:.5em;margin-bottom:.5em}.PaletteEditor .palette-import-button{grid-column:1/span 2}.PaletteEditor .palette-export-button{grid-column:3/span 2}.PaletteEditor .palette-apply-button{grid-column:1/span 2}.PaletteEditor .color-input.style-control{margin:0}.PaletteEditor.-compact{grid-template-columns:repeat(2,1fr);grid-template-rows:repeat(5,1fr) auto}.PaletteEditor.-compact .palette-import-button{grid-column:1}.PaletteEditor.-compact .palette-export-button{grid-column:2}.PaletteEditor.-compact.-apply{grid-template-rows:repeat(5,1fr) auto auto}.PaletteEditor.-compact.-apply .palette-apply-button,.PaletteEditor.-mobile.-apply .palette-apply-button{grid-column:1/span 2}.PaletteEditor.-mobile .color-input{display:grid;gap:.5em}.PaletteEditor.-mobile .color-input label{-webkit-box-flex:1;flex:1}.theme-preview-container{position:relative;border-top:1px dashed;border-bottom:1px dashed;border-color:var(--border);margin:1em 0;padding:1em;background-color:var(--wallpaper);background-image:var(--body-background-image);background-size:cover;background-position:50% 50%}.theme-preview-container .theme-preview-content{padding:1.5em}.theme-preview-container .dummy .post{font-family:var(--postFont);display:-webkit-box;display:flex}.theme-preview-container .dummy .post .content{-webkit-box-flex:1;flex:1}.theme-preview-container .dummy .post .content h4{margin-bottom:.25em}.theme-preview-container .dummy .post .content .icons{margin-top:.5em;display:-webkit-box;display:flex}.theme-preview-container .dummy .post .content .icons i{margin-right:1em}.theme-preview-container .dummy .after-post{margin-top:1em;display:-webkit-box;display:flex;-webkit-box-align:center;align-items:center}.theme-preview-container .dummy .avatar,.theme-preview-container .dummy .avatar-alt{background:-webkit-linear-gradient(315deg,#b8e1fc 0%,#a9d2f3 10%,#90bae4 25%,#90bcea 37%,#90bff0 50%,#6ba8e5 51%,#a2daf5 83%,#bdf3fd 100%);background:linear-gradient(135deg,#b8e1fc,#a9d2f3 10%,#90bae4 25%,#90bcea 37%,#90bff0 50%,#6ba8e5 51%,#a2daf5 83%,#bdf3fd);color:#000;font-family:sans-serif;text-align:center;margin-right:1em}.theme-preview-container .dummy .avatar-alt{-webkit-box-flex:0;flex:0 auto;margin-left:2em;font-size:.85em;min-width:2.2rem;min-height:2.2rem;line-height:1.5em;align-content:center}.theme-preview-container .dummy .avatar{-webkit-box-flex:0;flex:0 auto;width:3.5em;height:3.5em;font-size:1rem;line-height:3.5em;-webkit-box-pack:center;justify-content:center}.theme-preview-container .dummy .actions{display:-webkit-box;display:flex;-webkit-box-align:baseline;align-items:baseline}.theme-preview-container .dummy .actions .checkbox{margin-right:1em;-webkit-box-flex:1;flex:1}.theme-preview-container .dummy .separator{margin:1em;border-bottom:1px solid;border-color:var(--border)}.theme-preview-container .dummy .btn{min-width:3em}.theme-preview-container .underlay-preview{position:absolute;top:0;right:.9em;bottom:0;left:.9em}.appearance-tab{margin:1em}.appearance-tab h3{border:none}.appearance-tab .palette,.appearance-tab .theme-notice{padding:.5em}.appearance-tab .theme-name{font-weight:900;padding-bottom:.5em}.appearance-tab input[type=file]{padding:.25em;height:auto}.appearance-tab .banner-background{display:-webkit-box;display:flex;gap:1em;flex-wrap:wrap}.appearance-tab .banner-background h4{margin:0}.appearance-tab .banner-background-input{-webkit-box-flex:1;flex:1;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;gap:.5em}.appearance-tab .banner-background-input .custom-bg-control{display:grid;gap:.5em;grid-template-columns:1fr 1fr}.appearance-tab .banner-background-preview{display:-webkit-box;display:flex;max-width:100%;width:300px;position:relative}.appearance-tab .banner-background-preview img{width:100%}.appearance-tab .banner-background-preview .fun-monitor{position:relative;pointer-events:none;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;-webkit-box-align:center;align-items:center}.appearance-tab .banner-background-preview .fun-monitor *{line-height:1}.appearance-tab .banner-background-preview .fun-monitor-display-bezel,.appearance-tab .banner-background-preview .fun-monitor-display-screen{aspect-ratio:16/9;width:16em}.appearance-tab .banner-background-preview .fun-monitor img{object-fit:cover}.appearance-tab .banner-background-preview .fun-monitor .wallpaper{position:absolute;top:0;right:0;bottom:0;left:0;background-color:var(--wallpaper)}.appearance-tab .banner-background-preview .fun-monitor-display-uploading{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;display:-webkit-box;display:flex;place-items:center;place-content:center;background-color:#0009;font-size:4em}.appearance-tab .banner-background-preview .fun-monitor-display-screen{padding:0;overflow:hidden;position:relative}.appearance-tab .banner-background-preview .fun-monitor-display-screen-overlay{background:transparent;position:absolute;top:0;right:0;bottom:0;left:0;z-index:2}.appearance-tab .banner-background-preview .fun-monitor-display-screen-image{aspect-ratio:16/9}.appearance-tab .banner-background-preview .fun-monitor-display-bezel{padding:1em;margin:0;-webkit-box-ordinal-group:2;order:1;z-index:3}.appearance-tab .banner-background-preview .fun-monitor-neck{width:5em;height:3em;margin-top:-1em;margin-bottom:-.5em;-webkit-box-ordinal-group:3;order:2}.appearance-tab .banner-background-preview .fun-monitor-stand{width:8em;height:1em;-webkit-box-ordinal-group:4;order:3;z-index:1}.appearance-tab .palettes-container{height:15em;overflow:hidden auto;scrollbar-gutter:stable;border-radius:var(--roundness);border:1px solid var(--border);margin-bottom:.5em;margin-top:0;padding:.5em}.appearance-tab .palettes{display:grid;grid-template-columns:1fr 1fr;grid-gap:.5em;padding:.5em;width:100%}.appearance-tab .palettes h5{grid-column:1/span 2;margin-bottom:0}.appearance-tab .palette-entry{display:grid;grid-template-columns:1fr auto;-webkit-box-align:center;align-items:center;-webkit-box-pack:justify;justify-content:space-between;padding:0 .5em;height:-webkit-max-content;height:max-content}.appearance-tab .palette-entry .palette-label{height:auto}.appearance-tab .palette-entry .palette-label label{text-align:center}.appearance-tab .palette-entry .palette-square{-webkit-box-flex:0;flex:0 0 auto;display:inline-block;min-width:1em;min-height:1em}.appearance-tab .column-settings{display:-webkit-box;display:flex;-webkit-box-pack:space-evenly;justify-content:space-evenly;flex-wrap:wrap}.appearance-tab .column-settings .size-label{display:block;margin-bottom:.5em;margin-top:.5em}.modal-view.-mobile .appearance-tab .palettes{grid-template-columns:1fr}.modal-view.-mobile .appearance-tab .palette-entry{grid-column:1;-webkit-box-pack:center;justify-content:center}.modal-view.-mobile .appearance-tab .palette-label{line-height:1.5em;margin-top:.5em}.appearance-tab .palette-preview{display:grid;grid-template-columns:1fr 1fr 1fr 1fr;grid-template-rows:1em 1em;margin:.5em 0}.appearance-tab .theme-list{list-style:none;display:-webkit-box;display:flex;flex-wrap:wrap;margin:-.5em 0 1em;height:25em;overflow:hidden auto;scrollbar-gutter:stable;border-radius:var(--roundness);border:1px solid var(--border);padding:0}.appearance-tab .theme-list .theme-preview{font-size:1rem;width:14rem;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;-webkit-box-align:center;align-items:center;margin:.5em}.appearance-tab .theme-list .theme-preview.placeholder{opacity:.2}.appearance-tab .theme-list .theme-preview .theme-preview-container{pointer-events:none;zoom:.5;border:none;border-radius:var(--roundness);text-align:left}.developer-tab .setting-list{margin-left:2em}.developer-tab .cache-buttons{display:grid;grid-template-columns:1fr 1fr;grid-column:1/3;-webkit-column-gap:.5em;column-gap:.5em;margin:0 5em}.-mobile .developer-tab .cache-buttons{grid-template-columns:1fr;grid-template-rows:1fr 1fr;row-gap:.5em;margin:0}.developer-tab dt{font-weight:900}.developer-tab dd{margin-top:.5em;margin-bottom:1em}.SelectMotion{-webkit-box-flex:0;flex:0 0 auto;display:grid;grid-auto-columns:1fr;grid-auto-flow:column;margin-top:.25em}.SelectMotion .button-default{margin:0;padding:0}.ComponentPreview{display:grid;grid-template-columns:1em 1fr 1fr 1em;grid-template-rows:2em 1fr 1fr 1fr 1em 2em -webkit-max-content;grid-template-rows:2em 1fr 1fr 1fr 1em 2em max-content;grid-template-areas:"header header header header " "preview preview preview y-slide" "preview preview preview y-slide" "preview preview preview y-slide" "x-slide x-slide x-slide . " "x-num x-num y-num y-num " "assists assists assists assists";grid-gap:.5em}.ComponentPreview:not(.-shadow-controls){grid-template-areas:"header header header header " "preview preview preview y-slide" "preview preview preview y-slide" "preview preview preview y-slide" "assists assists assists assists";grid-template-rows:2em 1fr 1fr 1fr -webkit-max-content;grid-template-rows:2em 1fr 1fr 1fr max-content}.ComponentPreview .header{grid-area:header;place-self:baseline center;line-height:2}.ComponentPreview .invalid-container{position:absolute;top:0;right:0;bottom:0;left:0;display:grid;place-items:center center;background-color:#64000080}.ComponentPreview .invalid-container .alert{padding:.5em 1em}.ComponentPreview .assists{grid-area:assists;display:grid;grid-auto-flow:row;grid-auto-rows:2em;grid-gap:.5em}.ComponentPreview .input-light-grid{justify-self:center}.ComponentPreview .input-number{min-width:2em}.ComponentPreview .x-shift-number{grid-area:x-num;justify-self:right}.ComponentPreview .y-shift-number{grid-area:y-num;justify-self:left}.ComponentPreview .x-shift-number input,.ComponentPreview .y-shift-number input{max-width:4em}.ComponentPreview .x-shift-slider{grid-area:x-slide;height:auto;align-self:start;min-width:10em}.ComponentPreview .y-shift-slider{grid-area:y-slide;-webkit-writing-mode:vertical-lr;writing-mode:vertical-lr;justify-self:left;min-height:10em}.ComponentPreview .x-shift-slider,.ComponentPreview .y-shift-slider{padding:0}.ComponentPreview .preview-window{--__grid-color1: rgb(102 102 102);--__grid-color2: rgb(153 153 153);--__grid-color1-disabled: rgb(102 102 102 / 20%);--__grid-color2-disabled: rgb(153 153 153 / 20%);position:relative;grid-area:preview;aspect-ratio:1;display:-webkit-box;display:flex;-webkit-box-align:center;align-items:center;-webkit-box-pack:center;justify-content:center;min-width:10em;min-height:10em;background-color:var(--__grid-color2);background-image:-webkit-linear-gradient(45deg,var(--__grid-color1) 25%,transparent 25%),-webkit-linear-gradient(135deg,var(--__grid-color1) 25%,transparent 25%),-webkit-linear-gradient(45deg,transparent 75%,var(--__grid-color1) 75%),-webkit-linear-gradient(135deg,transparent 75%,var(--__grid-color1) 75%);background-image:linear-gradient(45deg,var(--__grid-color1) 25%,transparent 25%),linear-gradient(-45deg,var(--__grid-color1) 25%,transparent 25%),linear-gradient(45deg,transparent 75%,var(--__grid-color1) 75%),linear-gradient(-45deg,transparent 75%,var(--__grid-color1) 75%);background-size:20px 20px;background-position:0 0,0 10px,10px -10px,-10px 0;border-radius:var(--roundness)}.ComponentPreview .preview-window.-light-grid{--__grid-color1: rgb(205 205 205);--__grid-color2: rgb(255 255 255);--__grid-color1-disabled: rgb(205 205 205 / 20%);--__grid-color2-disabled: rgb(255 255 255 / 20%)}.ComponentPreview .preview-window.disabled{background-color:var(--__grid-color2-disabled);background-image:-webkit-linear-gradient(45deg,var(--__grid-color1-disabled) 25%,transparent 25%),-webkit-linear-gradient(135deg,var(--__grid-color1-disabled) 25%,transparent 25%),-webkit-linear-gradient(45deg,transparent 75%,var(--__grid-color1-disabled) 75%),-webkit-linear-gradient(135deg,transparent 75%,var(--__grid-color1-disabled) 75%);background-image:linear-gradient(45deg,var(--__grid-color1-disabled) 25%,transparent 25%),linear-gradient(-45deg,var(--__grid-color1-disabled) 25%,transparent 25%),linear-gradient(45deg,transparent 75%,var(--__grid-color1-disabled) 75%),linear-gradient(-45deg,transparent 75%,var(--__grid-color1-disabled) 75%)}.ComponentPreview .preview-window .preview-block{background:var(--background, var(--bg));display:-webkit-box;display:flex;-webkit-box-pack:center;justify-content:center;-webkit-box-align:center;align-items:center;min-width:33%;min-height:33%;max-width:80%;max-height:80%;border-width:0;border-style:solid;border-color:var(--border);border-radius:var(--roundness);box-shadow:var(--shadow)}.ShadowControl{display:grid;grid-template-columns:10em 1fr 1fr;grid-template-rows:1fr;grid-template-areas:"selector preview tweak";grid-gap:.5em;-webkit-box-pack:stretch;justify-content:stretch}.ShadowControl .style-control{display:-webkit-box;display:flex}.ShadowControl.-compact{grid-template-columns:10em 1fr;grid-template-rows:auto auto;grid-template-areas:"selector preview" "tweak tweak"}.ShadowControl.-compact.-no-preview{grid-template-columns:1fr;grid-template-rows:10em 1fr;grid-template-areas:"selector" "tweak"}.ShadowControl .shadow-switcher{grid-area:selector;-webkit-box-ordinal-group:2;order:1;-webkit-box-flex:1;flex:1 0 6em;min-width:6em;margin-right:.125em;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column}.ShadowControl .shadow-switcher .shadow-list{-webkit-box-flex:1;flex:1 0 auto}.ShadowControl .shadow-tweak{grid-area:tweak;-webkit-box-ordinal-group:4;order:3;-webkit-box-flex:2;flex:2 0 10em;min-width:10em;margin-left:.125em;margin-right:.125em;display:grid;grid-template-rows:auto 1fr;grid-gap:.25em}.ShadowControl .shadow-tweak .input-boolean{-webkit-box-flex:1;flex:1;display:-webkit-box;display:flex}.ShadowControl .shadow-tweak .input-boolean .label{-webkit-box-flex:1;flex:1}.ShadowControl .shadow-tweak .input-string{-webkit-box-flex:1;flex:1 0 5em}.ShadowControl .shadow-tweak .shadow-expression{width:100%;height:100%}.ShadowControl .shadow-tweak .id-control{-webkit-box-align:stretch;align-items:stretch}.ShadowControl .shadow-tweak .id-control .shadow-switcher,.ShadowControl .shadow-tweak .id-control .btn{min-width:1px;margin-right:5px}.ShadowControl .shadow-tweak .id-control .btn{padding:0 .4em;margin:0 .1em}.ShadowControl.-no-preview{grid-template-columns:10em 1fr;grid-template-rows:1fr;grid-template-areas:"selector tweak"}.ShadowControl.-no-preview .shadow-tweak{-webkit-box-ordinal-group:1;order:0;-webkit-box-flex:2;flex:2 0 8em;max-width:100%}.ShadowControl.-no-preview .input-range{min-width:5em}.ShadowControl .inset-alert{padding:.25em .5em}.ShadowControl.disabled .inset-alert{opacity:.2}.ShadowControl .shadow-preview{grid-area:preview;margin-left:.125em;place-self:start center}.inset-tooltip{max-width:30em}.tooltip{margin:.5em 1em}.contrast-ratio{display:-webkit-box;display:flex;-webkit-box-pack:end;justify-content:flex-end;-webkit-box-align:baseline;align-items:baseline}.contrast-ratio .label{margin-right:1em}.contrast-ratio .rating{display:inline-block;margin-left:.5em}.old-theme-tab{min-width:var(--themeEditorMinWidth, -webkit-fit-content);min-width:var(--themeEditorMinWidth, fit-content);margin:1em;padding-bottom:2em}.old-theme-tab .deprecation-warning{padding:.5em;margin:2em}.old-theme-tab .preset-switcher{margin-right:1em}.old-theme-tab .btn{margin-left:.25em;margin-right:.25em}.old-theme-tab .btn-group .btn{margin:0}.old-theme-tab .style-control{display:-webkit-box;display:flex;-webkit-box-align:baseline;align-items:baseline;margin-bottom:5px}.old-theme-tab .style-control .label{margin-right:1em;-webkit-box-flex:1;flex:1;line-height:2}.old-theme-tab .style-control .opt{margin:.5em}.old-theme-tab .style-control .color-input{-webkit-box-flex:0;flex:0 0 0}.old-theme-tab .style-control input,.old-theme-tab .style-control select{min-width:3em;margin:0;-webkit-box-flex:0;flex:0}.old-theme-tab .style-control input[type=number],.old-theme-tab .style-control select[type=number]{min-width:9em}.old-theme-tab .style-control input[type=number].-small,.old-theme-tab .style-control select[type=number].-small{min-width:5em}.old-theme-tab .style-control input[type=range],.old-theme-tab .style-control select[type=range]{-webkit-box-flex:1;flex:1;min-width:9em;align-self:center;margin:0 .5em}.old-theme-tab .style-control input[type=checkbox]+i,.old-theme-tab .style-control select[type=checkbox]+i{height:1.1em;align-self:center}.old-theme-tab .reset-container{flex-wrap:wrap}.old-theme-tab .fonts-container,.old-theme-tab .reset-container,.old-theme-tab .apply-container,.old-theme-tab .radius-container,.old-theme-tab .color-container{display:-webkit-box;display:flex}.old-theme-tab .fonts-container,.old-theme-tab .radius-container{-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column}.old-theme-tab .color-container{flex-wrap:wrap;-webkit-box-pack:justify;justify-content:space-between}.old-theme-tab .color-container>h4{width:99%}.old-theme-tab .fonts-container,.old-theme-tab .color-container,.old-theme-tab .shadow-container,.old-theme-tab .radius-container,.old-theme-tab .presets-container{margin:1em 1em 0}.old-theme-tab .tab-header{display:-webkit-box;display:flex;-webkit-box-pack:justify;justify-content:space-between;-webkit-box-align:baseline;align-items:baseline;width:100%;min-height:2.1em;margin-bottom:1em}.old-theme-tab .tab-header p{-webkit-box-flex:1;flex:1;margin:0 .5em 0 0}.old-theme-tab .tab-header-buttons{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column}.old-theme-tab .tab-header-buttons .btn{min-width:1px;-webkit-box-flex:0;flex:0 auto;padding:0 1em;margin-bottom:.5em}.old-theme-tab .shadow-selector .override{-webkit-box-flex:1;flex:1;margin-left:.5em}.old-theme-tab .shadow-selector .select-container{margin-top:-4px;margin-bottom:-3px}.old-theme-tab .save-load,.old-theme-tab .save-load-options{display:-webkit-box;display:flex;-webkit-box-pack:center;justify-content:center;-webkit-box-align:baseline;align-items:baseline;flex-wrap:wrap}.old-theme-tab .save-load .presets,.old-theme-tab .save-load .import-export,.old-theme-tab .save-load-options .presets,.old-theme-tab .save-load-options .import-export{margin-bottom:.5em}.old-theme-tab .save-load .import-export,.old-theme-tab .save-load-options .import-export{display:-webkit-box;display:flex}.old-theme-tab .save-load .override,.old-theme-tab .save-load-options .override{margin-left:.5em}.old-theme-tab .save-load-options{flex-wrap:wrap;margin-top:.5em;-webkit-box-pack:center;justify-content:center}.old-theme-tab .save-load-options .keep-option{margin:0 .5em .5em;min-width:25%}.old-theme-tab .radius-item{flex-basis:auto}.old-theme-tab .radius-item,.old-theme-tab .color-item{min-width:20em;margin:5px 6px 0 0;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;-webkit-box-flex:1;flex:1 1 0}.old-theme-tab .radius-item.wide,.old-theme-tab .color-item.wide{min-width:60%}.old-theme-tab .radius-item:not(.wide):nth-child(odd),.old-theme-tab .color-item:not(.wide):nth-child(odd){margin-right:7px}.old-theme-tab .radius-item .color,.old-theme-tab .radius-item .opacity,.old-theme-tab .color-item .color,.old-theme-tab .color-item .opacity{display:-webkit-box;display:flex;-webkit-box-align:baseline;align-items:baseline}.old-theme-tab .theme-radius-rn,.old-theme-tab .theme-color-cl{border:0;box-shadow:none;background:transparent;color:var(--textFaint);align-self:stretch}.old-theme-tab .theme-color-cl,.old-theme-tab .theme-radius-in,.old-theme-tab .theme-color-in{margin-left:.3em}.old-theme-tab .theme-radius-in{min-width:1em;max-width:7em;-webkit-box-flex:1;flex:1}.old-theme-tab .theme-radius-lb{max-width:50em}.old-theme-tab .theme-warning{display:-webkit-box;display:flex;-webkit-box-align:baseline;align-items:baseline;margin-bottom:.5em}.old-theme-tab .theme-warning .buttons .btn{margin-bottom:.5em}.extra-content .apply-container{display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-direction:row;justify-content:space-around;-webkit-box-flex:1;flex-grow:1}.extra-content .apply-container .btn{-webkit-box-flex:1;flex-grow:1;min-height:2em;min-width:0;max-width:10em;padding:0}.StyleTab{min-width:var(--themeEditorMinWidth, -webkit-fit-content);min-width:var(--themeEditorMinWidth, fit-content);margin:1em}.StyleTab .style-control{-webkit-box-align:baseline;align-items:baseline;margin-bottom:.5em}.StyleTab .style-control .label{display:inline-block;margin-right:.5em;-webkit-box-flex:1;flex:1 1 auto;line-height:2;min-height:2em}.StyleTab .style-control.suboption{margin-left:1em}.StyleTab .style-control input,.StyleTab .style-control select{min-width:3em;margin:0;-webkit-box-flex:0;flex:0}.StyleTab .style-control input[type=number],.StyleTab .style-control select[type=number]{min-width:9em}.StyleTab .style-control input[type=number].-small,.StyleTab .style-control select[type=number].-small{min-width:5em}.StyleTab .style-control input[type=range],.StyleTab .style-control select[type=range]{-webkit-box-flex:1;flex:1;min-width:9em;align-self:center;margin:0 .25em}.StyleTab .style-control input[type=checkbox]+i,.StyleTab .style-control select[type=checkbox]+i{height:1.1em;align-self:center}.StyleTab .setting-section.heading{padding:0}.StyleTab .meta-preview{display:grid;grid-template:"meta preview";grid-gap:1em;grid-template-columns:-webkit-min-content 6fr;grid-template-columns:min-content 6fr}.StyleTab .meta-preview .theme-preview-container{margin:0}.StyleTab .meta-preview ul.setting-list{padding:0;margin:0;grid-area:meta}.StyleTab .meta-preview ul.setting-list>li{margin:0}.StyleTab .meta-preview ul.setting-list .meta-field{margin:0}.StyleTab .meta-preview ul.setting-list .meta-field .setting-label{display:inline-block;margin-bottom:.5em}.StyleTab .meta-preview #edited-style-preview{grid-area:preview}.StyleTab .setting-section{padding-bottom:0;margin-top:1em}.StyleTab .setting-section .btn{padding:0 .5em}.StyleTab .setting-section:not(:first-child){margin-top:.5em}.StyleTab .setting-section:not(:last-child){margin-bottom:.5em}.StyleTab .list-editor{display:grid;grid-template-areas:"label editor" "selector editor" "movement editor";grid-template-columns:10em 1fr;grid-template-rows:auto 1fr auto;grid-gap:.5em}.StyleTab .list-editor .list-edit-area{grid-area:editor;-webkit-box-align:baseline;align-items:baseline}.StyleTab .list-editor .list-select{grid-area:selector;margin:0}.StyleTab .list-editor .list-select-label{font-weight:700;grid-area:label;margin:0;align-self:baseline}.StyleTab .list-editor .list-select-movement{grid-area:movement;margin:0}.StyleTab .variables-editor .variable-selector{display:grid;grid-template-columns:auto 1fr auto 10em;grid-template-rows:subgrid;-webkit-box-align:baseline;align-items:baseline;grid-gap:0 .5em}.StyleTab .variables-editor .list-edit-area{display:grid;grid-template-rows:subgrid}.StyleTab .variables-editor .shadow-control{grid-row:2/span 2}.StyleTab .component-editor{display:grid;grid-template-columns:6fr 3fr 4fr;grid-template-rows:auto auto 1fr;grid-gap:.5em;grid-template-areas:"component component variant" "state state state" "preview settings settings"}.StyleTab .component-editor .component-selector{grid-area:component;align-self:center}.StyleTab .component-editor .component-selector,.StyleTab .component-editor .state-selector,.StyleTab .component-editor .variant-selector{display:grid;grid-template-columns:1fr minmax(10em,1fr);grid-template-rows:auto;grid-auto-flow:column;grid-gap:.5em;-webkit-box-align:baseline;align-items:baseline}.StyleTab .component-editor .component-selector>label:not(.Select),.StyleTab .component-editor .state-selector>label:not(.Select),.StyleTab .component-editor .variant-selector>label:not(.Select){font-weight:700;justify-self:right}.StyleTab .component-editor .state-selector{grid-area:state;grid-template-columns:minmax(-webkit-min-content,7em) 1fr;grid-template-columns:minmax(min-content,7em) 1fr}.StyleTab .component-editor .variant-selector{grid-area:variant}.StyleTab .component-editor .state-selector-list{display:grid;list-style:none;grid-auto-flow:dense;grid-template-columns:repeat(5,minmax(-webkit-min-content,1fr));grid-template-columns:repeat(5,minmax(min-content,1fr));grid-auto-rows:1fr;grid-gap:.5em;padding:0;margin:0}.StyleTab .component-editor .preview-container{--border: none;--shadow: none;--roundness: none;grid-area:preview}.StyleTab .component-editor .component-settings{grid-area:settings}.StyleTab .component-editor .editor-tab{display:grid;grid-template-columns:1fr 2em;grid-column-gap:.5em;-webkit-box-align:center;align-items:center;grid-auto-rows:-webkit-min-content;grid-auto-rows:min-content;grid-auto-flow:dense;border-left:1px solid var(--border);border-right:1px solid var(--border);border-bottom:1px solid var(--border);padding:.5em}.StyleTab .component-editor .shadow-tab{grid-template-columns:1fr;justify-items:center}.-mobile .StyleTab .meta-preview{grid-template:"meta" "preview"}.-mobile .StyleTab .list-editor{display:grid;grid-template-areas:"label" "selector" "movement" "editor" "editor" "editor" "editor";grid-template-columns:1fr;grid-template-rows:auto 1fr auto}.-mobile .StyleTab .component-editor{grid-template-columns:1fr;grid-template-rows:auto;grid-template-areas:"component" "variant" "state" "preview" "settings"}.-mobile .StyleTab .variable-selector{grid-template-columns:1fr;grid-template-rows:auto;grid-auto-flow:row;grid-gap:.5em}.extra-content .style-actions-container{width:100%;display:-webkit-box;display:flex;-webkit-box-pack:end;justify-content:end}.extra-content .style-actions-container .style-actions{display:grid;grid-template-columns:repeat(4,minmax(7em,1fr));grid-gap:.25em} diff --git a/priv/static/static/css/settings_modal_user_content.DUcdr5Gz.css b/priv/static/static/css/settings_modal_user_content.DUcdr5Gz.css new file mode 100644 index 000000000..bd463121e --- /dev/null +++ b/priv/static/static/css/settings_modal_user_content.DUcdr5Gz.css @@ -0,0 +1 @@ +.tooltip{margin:.5em 1em}.contrast-ratio{justify-content:flex-end;align-items:baseline;display:flex}.contrast-ratio .label{margin-right:1em}.contrast-ratio .rating{margin-left:.5em;display:inline-block}.PaletteEditor{grid-gap:.5em;grid-template-rows:repeat(5,1fr) auto;grid-template-columns:repeat(4,1fr);justify-content:space-around;align-items:baseline}.PaletteEditor .buttons{gap:.5em;margin-top:.5em;display:grid}.PaletteEditor .palette{grid-gap:.5em;grid-template-rows:1fr;grid-template-columns:repeat(auto-fill,10em);grid-auto-rows:auto;grid-auto-flow:row;margin-bottom:.5em;display:grid}.PaletteEditor .palette-import-button{grid-column:1/span 2}.PaletteEditor .palette-export-button{grid-column:3/span 2}.PaletteEditor .palette-apply-button{grid-column:1/span 2}.PaletteEditor .color-input.style-control{margin:0}.PaletteEditor.-compact{grid-template-rows:repeat(5,1fr) auto;grid-template-columns:repeat(2,1fr)}.PaletteEditor.-compact .palette-import-button{grid-column:1}.PaletteEditor.-compact .palette-export-button{grid-column:2}.PaletteEditor.-compact.-apply{grid-template-rows:repeat(5,1fr) auto auto}.PaletteEditor.-compact.-apply .palette-apply-button,.PaletteEditor.-mobile.-apply .palette-apply-button{grid-column:1/span 2}.UnitSetting .no-break{display:inline-flex}.UnitSetting .number-input{text-align:right;max-width:6.5em}.UnitSetting .unit-input,.UnitSetting .unit-input select{width:auto;min-width:4em}.theme-preview-container{border-top:1px dashed;border-bottom:1px dashed;border-color:var(--border);background-color:var(--wallpaper);background-image:var(--body-background-image);background-position:50%;background-size:cover;margin:1em 0;padding:1em;position:relative}.theme-preview-container .theme-preview-content{padding:1.5em}.theme-preview-container .dummy .post{font-family:var(--postFont);display:flex}.theme-preview-container .dummy .post .content{flex:1}.theme-preview-container .dummy .post .content h4{margin-bottom:.25em}.theme-preview-container .dummy .post .content .icons{margin-top:.5em;display:flex}.theme-preview-container .dummy .post .content .icons i{margin-right:1em}.theme-preview-container .dummy .after-post{align-items:center;margin-top:1em;display:flex}.theme-preview-container .dummy .avatar,.theme-preview-container .dummy .avatar-alt{color:#000;text-align:center;background:linear-gradient(135deg,#b8e1fc 0%,#a9d2f3 10%,#90bae4 25%,#90bcea 37%,#90bff0 50%,#6ba8e5 51%,#a2daf5 83%,#bdf3fd 100%);margin-right:1em;font-family:sans-serif}.theme-preview-container .dummy .avatar-alt{flex:0 auto;align-content:center;min-width:2.2rem;min-height:2.2rem;margin-left:2em;font-size:.85em;line-height:1.5em}.theme-preview-container .dummy .avatar{flex:0 auto;justify-content:center;width:3.5em;height:3.5em;font-size:1rem;line-height:3.5em}.theme-preview-container .dummy .actions{align-items:baseline;display:flex}.theme-preview-container .dummy .actions .checkbox{flex:1;margin-right:1em}.theme-preview-container .dummy .separator{border-bottom:1px solid;border-color:var(--border);margin:1em}.theme-preview-container .dummy .btn{min-width:3em}.theme-preview-container .underlay-preview{position:absolute;inset:0 .9em}.appearance-tab{margin:1em}.appearance-tab h3{border:none}.appearance-tab .palette,.appearance-tab .theme-notice{padding:.5em}.appearance-tab .theme-name{padding-bottom:.5em;font-weight:900}.appearance-tab input[type=file]{height:auto;padding:.25em}.appearance-tab .banner-background{flex-wrap:wrap;gap:1em;display:flex}.appearance-tab .banner-background h4{margin:0}.appearance-tab .banner-background-input{flex-direction:column;flex:1;gap:.5em;display:flex}.appearance-tab .banner-background-input .custom-bg-control{grid-template-columns:1fr 1fr;gap:.5em;display:grid}.appearance-tab .banner-background-preview{width:300px;max-width:100%;display:flex;position:relative}.appearance-tab .banner-background-preview img{width:100%}.appearance-tab .banner-background-preview .fun-monitor{pointer-events:none;flex-direction:column;align-items:center;display:flex;position:relative}.appearance-tab .banner-background-preview .fun-monitor *{line-height:1}.appearance-tab .banner-background-preview .fun-monitor-display-bezel,.appearance-tab .banner-background-preview .fun-monitor-display-screen{aspect-ratio:16/9;width:16em}.appearance-tab .banner-background-preview .fun-monitor img{object-fit:cover}.appearance-tab .banner-background-preview .fun-monitor .wallpaper{background-color:var(--wallpaper);position:absolute;inset:0}.appearance-tab .banner-background-preview .fun-monitor-display-uploading{z-index:1;background-color:#0009;place-content:center;place-items:center;font-size:4em;display:flex;position:absolute;inset:0}.appearance-tab .banner-background-preview .fun-monitor-display-screen{padding:0;position:relative;overflow:hidden}.appearance-tab .banner-background-preview .fun-monitor-display-screen-overlay{z-index:2;background:0 0;position:absolute;inset:0}.appearance-tab .banner-background-preview .fun-monitor-display-screen-image{aspect-ratio:16/9}.appearance-tab .banner-background-preview .fun-monitor-display-bezel{z-index:3;order:1;margin:0;padding:1em}.appearance-tab .banner-background-preview .fun-monitor-neck{order:2;width:5em;height:3em;margin-top:-1em;margin-bottom:-.5em}.appearance-tab .banner-background-preview .fun-monitor-stand{z-index:1;order:3;width:8em;height:1em}.appearance-tab .palettes-container{scrollbar-gutter:stable;border-radius:var(--roundness);border:1px solid var(--border);height:15em;margin-top:0;margin-bottom:.5em;padding:.5em;overflow:hidden auto}.appearance-tab .palettes{grid-gap:.5em;grid-template-columns:1fr 1fr;width:100%;padding:.5em;display:grid}.appearance-tab .palettes h5{grid-column:1/span 2;margin-bottom:0}.appearance-tab .palette-entry{grid-template-columns:1fr auto;justify-content:space-between;align-items:center;height:max-content;padding:0 .5em;display:grid}.appearance-tab .palette-entry .palette-label{height:auto}.appearance-tab .palette-entry .palette-label label{text-align:center}.appearance-tab .palette-entry .palette-square{flex:none;min-width:1em;min-height:1em;display:inline-block}.appearance-tab .column-settings{-webkit-box-pack:space-evenly;flex-wrap:wrap;justify-content:space-evenly;display:flex}.appearance-tab .column-settings .size-label{margin-top:.5em;margin-bottom:.5em;display:block}.modal-view.-mobile .appearance-tab .palettes{grid-template-columns:1fr}.modal-view.-mobile .appearance-tab .palette-entry{grid-column:1;justify-content:center}.modal-view.-mobile .appearance-tab .palette-label{margin-top:.5em;line-height:1.5em}.appearance-tab .palette-preview{grid-template-rows:1em 1em;grid-template-columns:1fr 1fr 1fr 1fr;margin:.5em 0;display:grid}.appearance-tab .theme-list{scrollbar-gutter:stable;border-radius:var(--roundness);border:1px solid var(--border);flex-wrap:wrap;height:25em;margin:-.5em 0 1em;padding:0;list-style:none;display:flex;overflow:hidden auto}.appearance-tab .theme-list .theme-preview{flex-direction:column;align-items:center;width:14rem;margin:.5em;font-size:1rem;display:flex}.appearance-tab .theme-list .theme-preview.placeholder{opacity:.2}.appearance-tab .theme-list .theme-preview .theme-preview-container{pointer-events:none;zoom:.5;border-radius:var(--roundness);text-align:left;border:none}.HelpIndicator{display:inline-block;position:relative}.help-tooltip{text-align:center;min-width:10em;max-width:30vw;margin:.5em 1em}.invalid-tooltip{text-align:center;min-width:10em;margin:.5em 1em}.exporter{flex-direction:column;display:flex}.exporter-processing{margin:.25em}.importer-uploading{margin:.25em;font-size:1.5em}.data-import-export-tab .importer-exporter{flex-direction:column;gap:.5em;display:inline-flex}.data-import-export-tab table td,.data-import-export-tab table th{line-height:1.5}.data-import-export-tab table th{padding:0 .5em}.data-import-export-tab table td{padding:.5em}.developer-tab .setting-list{margin-left:2em}.developer-tab .cache-buttons{-webkit-column-gap:.5em;grid-column:1/3;grid-template-columns:1fr 1fr;column-gap:.5em;margin:0 5em;display:grid}.-mobile .developer-tab .cache-buttons{grid-template-rows:1fr 1fr;grid-template-columns:1fr;row-gap:.5em;margin:0}.developer-tab dt{font-weight:900}.developer-tab dd{margin-top:.5em;margin-bottom:1em}.filtering-tab .muteFilterContainer{border:1px solid var(--border);border-radius:var(--roundness);height:33vh;overflow-y:auto}.filtering-tab .mute-filter{border:1px solid var(--border);border-radius:var(--roundness);grid-gap:.5em;align-items:baseline;margin:.5em;padding:.5em;display:grid}.filtering-tab .never{margin-left:.5em}.filtering-tab .filter-name{grid-area:1/1/auto/span 2}.filtering-tab .alert,.filtering-tab .button-default{padding:0 .5em;line-height:2;display:inline-block}.filtering-tab .filter-enabled{text-align:right;grid-area:1/3;line-height:2}.filtering-tab .filter-field{grid-template-columns:subgrid;grid-template-rows:subgrid;grid-column:1/span 3;align-items:baseline;display:grid}.filtering-tab .filter-field label{text-align:right;grid-column:1}.filtering-tab .filter-field>label.checkbox{grid-template-columns:subgrid;grid-template-rows:subgrid;text-align:right;grid-column:1/span 2;display:grid}.filtering-tab .filter-field .filter-field-value{grid-column:2/span 2;display:flex}.filtering-tab .filter-buttons{grid-gap:.5em;grid-column:1/span 3;grid-template-columns:repeat(4,1fr);justify-content:end;justify-self:end;max-width:100%;display:inline-grid}.filtering-tab .muteFiltersActions,.filtering-tab .muteFiltersActionsBottom{grid-gap:.5em;grid-template-columns:1fr;grid-auto-flow:column;align-items:baseline;margin:.5em 0;display:grid}.filtering-tab .muteFiltersActions .total,.filtering-tab .muteFiltersActionsBottom .total{flex:1}.settings-modal.-mobile .filtering-tab .filter-buttons{grid-template-columns:repeat(1,1fr)}.autosuggest{position:relative}.autosuggest-input{width:100%;display:block}.autosuggest-results{background-color:var(--bg);border-style:solid;border-width:1px;border-color:var(--border);border-radius:var(--roundness);max-height:400px;box-shadow:1px 1px 4px #0009;box-shadow:var(--shadow);z-index:1;border-top-left-radius:0;border-top-right-radius:0;position:absolute;top:100%;left:0;right:0;overflow-y:auto}.block-card-content-container{text-align:right;margin-top:.5em}.block-card-content-container button{width:10em}.domain-mute-card{flex:1 0;justify-content:space-between;align-items:center;padding:.6em 1em .6em 0;display:flex}.domain-mute-card-domain{text-overflow:ellipsis;margin-right:1em;overflow:hidden}.domain-mute-card button{width:10em}.autosuggest-results .domain-mute-card{padding-left:1em}.mute-card-content-container{text-align:right;margin-top:.5em}.mute-card-content-container button{width:10em}.mutes-and-blocks-tab{min-height:100%}.mutes-and-blocks-tab .usersearch-wrapper{padding:1em}.mutes-and-blocks-tab .bulk-actions{text-align:right;min-height:2em;padding:0 1em}.mutes-and-blocks-tab .bulk-action-button{width:10em}.mutes-and-blocks-tab .domain-mute-form{flex-direction:column;padding:1em;display:flex}.mutes-and-blocks-tab .domain-mute-button{align-self:flex-end;width:10em;margin-top:1em}.mutes-and-blocks-tab .blocks,.mutes-and-blocks-tab .mutes{flex-direction:column;min-height:100%;display:flex;position:absolute;inset:0}.ComponentPreview{grid-template-columns:1em 1fr 1fr 1em;grid-template-rows:2em 1fr 1fr 1fr 1em 2em -webkit-max-content;grid-gap:.5em;grid-template-rows:2em 1fr 1fr 1fr 1em 2em max-content;grid-template-areas:"header header header header""preview preview preview y-slide""preview preview preview y-slide""preview preview preview y-slide""x-slide x-slide x-slide.""x-num x-num y-num y-num""assists assists assists assists";display:grid}.ComponentPreview:not(.-shadow-controls){grid-template-areas:"header header header header""preview preview preview y-slide""preview preview preview y-slide""preview preview preview y-slide""assists assists assists assists";grid-template-rows:2em 1fr 1fr 1fr -webkit-max-content;grid-template-rows:2em 1fr 1fr 1fr max-content}.ComponentPreview .header{grid-area:header;place-self:baseline center;line-height:2}.ComponentPreview .invalid-container{background-color:#64000080;place-items:center;display:grid;position:absolute;inset:0}.ComponentPreview .invalid-container .alert{padding:.5em 1em}.ComponentPreview .assists{grid-gap:.5em;grid-area:assists;grid-auto-rows:2em;grid-auto-flow:row;display:grid}.ComponentPreview .input-light-grid{justify-self:center}.ComponentPreview .input-number{min-width:2em}.ComponentPreview .x-shift-number{grid-area:x-num;justify-self:right}.ComponentPreview .y-shift-number{grid-area:y-num;justify-self:left}.ComponentPreview .x-shift-number input,.ComponentPreview .y-shift-number input{max-width:4em}.ComponentPreview .x-shift-slider{grid-area:x-slide;align-self:start;min-width:10em;height:auto}.ComponentPreview .y-shift-slider{-webkit-writing-mode:vertical-lr;writing-mode:vertical-lr;grid-area:y-slide;justify-self:left;min-height:10em}.ComponentPreview .x-shift-slider,.ComponentPreview .y-shift-slider{padding:0}.ComponentPreview .preview-window{--__grid-color1:#666;--__grid-color2:#999;--__grid-color1-disabled:#6663;--__grid-color2-disabled:#9993}.ComponentPreview .preview-window.-light-grid{--__grid-color1:#cdcdcd;--__grid-color2:#fff;--__grid-color1-disabled:#cdcdcd33;--__grid-color2-disabled:#fff3}.ComponentPreview .preview-window{aspect-ratio:1;background-color:var(--__grid-color2);background-image:-webkit-linear-gradient(45deg, var(--__grid-color1) 25%, transparent 25%), -webkit-linear-gradient(135deg, var(--__grid-color1) 25%, transparent 25%), -webkit-linear-gradient(45deg, transparent 75%, var(--__grid-color1) 75%), -webkit-linear-gradient(135deg, transparent 75%, var(--__grid-color1) 75%);background-image:linear-gradient(45deg, var(--__grid-color1) 25%, transparent 25%), linear-gradient(-45deg, var(--__grid-color1) 25%, transparent 25%), linear-gradient(45deg, transparent 75%, var(--__grid-color1) 75%), linear-gradient(-45deg, transparent 75%, var(--__grid-color1) 75%);border-radius:var(--roundness);background-position:0 0,0 10px,10px -10px,-10px 0;background-size:20px 20px;grid-area:preview;justify-content:center;align-items:center;min-width:10em;min-height:10em;display:flex;position:relative}.ComponentPreview .preview-window.disabled{background-color:var(--__grid-color2-disabled);background-image:-webkit-linear-gradient(45deg, var(--__grid-color1-disabled) 25%, transparent 25%), -webkit-linear-gradient(135deg, var(--__grid-color1-disabled) 25%, transparent 25%), -webkit-linear-gradient(45deg, transparent 75%, var(--__grid-color1-disabled) 75%), -webkit-linear-gradient(135deg, transparent 75%, var(--__grid-color1-disabled) 75%);background-image:linear-gradient(45deg, var(--__grid-color1-disabled) 25%, transparent 25%), linear-gradient(-45deg, var(--__grid-color1-disabled) 25%, transparent 25%), linear-gradient(45deg, transparent 75%, var(--__grid-color1-disabled) 75%), linear-gradient(-45deg, transparent 75%, var(--__grid-color1-disabled) 75%)}.ComponentPreview .preview-window .preview-block{background:var(--background,var(--bg));border-style:solid;border-width:0;border-color:var(--border);border-radius:var(--roundness);min-width:33%;max-width:80%;min-height:33%;max-height:80%;box-shadow:var(--shadow);justify-content:center;align-items:center;display:flex}.SelectMotion{flex:none;grid-auto-columns:1fr;grid-auto-flow:column;margin-top:.25em;display:grid}.SelectMotion .button-default{margin:0;padding:0}.ShadowControl{grid-gap:.5em;-webkit-box-pack:stretch;grid-template:"selector preview tweak"1fr/10em 1fr 1fr;justify-content:stretch;display:grid}.ShadowControl .style-control{display:flex}.ShadowControl.-compact{grid-template:"selector preview""tweak tweak"/10em 1fr}.ShadowControl.-compact.-no-preview{grid-template:"selector"10em"tweak"1fr/1fr}.ShadowControl .shadow-switcher{flex-direction:column;flex:1 0 6em;order:1;grid-area:selector;min-width:6em;margin-right:.125em;display:flex}.ShadowControl .shadow-switcher .shadow-list{flex:1 0 auto}.ShadowControl .shadow-tweak{grid-gap:.25em;flex:2 0 10em;order:3;grid-area:tweak;grid-template-rows:auto 1fr;min-width:10em;margin-left:.125em;margin-right:.125em;display:grid}.ShadowControl .shadow-tweak .input-boolean{flex:1;display:flex}.ShadowControl .shadow-tweak .input-boolean .label{flex:1}.ShadowControl .shadow-tweak .input-string{flex:1 0 5em}.ShadowControl .shadow-tweak .shadow-expression{width:100%;height:100%}.ShadowControl .shadow-tweak .id-control{align-items:stretch}.ShadowControl .shadow-tweak .id-control .shadow-switcher,.ShadowControl .shadow-tweak .id-control .btn{min-width:1px;margin-right:5px}.ShadowControl .shadow-tweak .id-control .btn{margin:0 .1em;padding:0 .4em}.ShadowControl.-no-preview{grid-template:"selector tweak"1fr/10em 1fr}.ShadowControl.-no-preview .shadow-tweak{flex:2 0 8em;order:0;max-width:100%}.ShadowControl.-no-preview .input-range{min-width:5em}.ShadowControl .inset-alert{padding:.25em .5em}.ShadowControl.disabled .inset-alert{opacity:.2}.ShadowControl .shadow-preview{grid-area:preview;place-self:start center;margin-left:.125em}.inset-tooltip{max-width:30em}.old-theme-tab{min-width:var(--themeEditorMinWidth,-webkit-fit-content);min-width:var(--themeEditorMinWidth,fit-content);margin:1em}.old-theme-tab .deprecation-warning{margin:2em;padding:.5em}.old-theme-tab{padding-bottom:2em}.old-theme-tab .preset-switcher{margin-right:1em}.old-theme-tab .btn{margin-left:.25em;margin-right:.25em}.old-theme-tab .btn-group .btn{margin:0}.old-theme-tab .style-control{align-items:baseline;margin-bottom:5px;display:flex}.old-theme-tab .style-control .label{flex:1;margin-right:1em;line-height:2}.old-theme-tab .style-control .opt{margin:.5em}.old-theme-tab .style-control .color-input{flex:0 0 0}.old-theme-tab .style-control input,.old-theme-tab .style-control select{flex:0;min-width:3em;margin:0}.old-theme-tab .style-control input[type=number],.old-theme-tab .style-control select[type=number]{min-width:9em}.old-theme-tab .style-control input[type=number].-small,.old-theme-tab .style-control select[type=number].-small{min-width:5em}.old-theme-tab .style-control input[type=range],.old-theme-tab .style-control select[type=range]{flex:1;align-self:center;min-width:9em;margin:0 .5em}.old-theme-tab .style-control input[type=checkbox]+i,.old-theme-tab .style-control select[type=checkbox]+i{align-self:center;height:1.1em}.old-theme-tab .reset-container{flex-wrap:wrap}.old-theme-tab .fonts-container,.old-theme-tab .reset-container,.old-theme-tab .apply-container,.old-theme-tab .radius-container,.old-theme-tab .color-container{display:flex}.old-theme-tab .fonts-container,.old-theme-tab .radius-container{flex-direction:column}.old-theme-tab .color-container>h4{width:99%}.old-theme-tab .color-container{flex-wrap:wrap;justify-content:space-between}.old-theme-tab .fonts-container,.old-theme-tab .color-container,.old-theme-tab .shadow-container,.old-theme-tab .radius-container,.old-theme-tab .presets-container{margin:1em 1em 0}.old-theme-tab .tab-header{justify-content:space-between;align-items:baseline;width:100%;min-height:2.1em;margin-bottom:1em;display:flex}.old-theme-tab .tab-header p{flex:1;margin:0 .5em 0 0}.old-theme-tab .tab-header-buttons{flex-direction:column;display:flex}.old-theme-tab .tab-header-buttons .btn{flex:0 auto;min-width:1px;margin-bottom:.5em;padding:0 1em}.old-theme-tab .shadow-selector .override{flex:1;margin-left:.5em}.old-theme-tab .shadow-selector .select-container{margin-top:-4px;margin-bottom:-3px}.old-theme-tab .save-load,.old-theme-tab .save-load-options{flex-wrap:wrap;justify-content:center;align-items:baseline;display:flex}.old-theme-tab .save-load .presets,.old-theme-tab .save-load .import-export,.old-theme-tab .save-load-options .presets,.old-theme-tab .save-load-options .import-export{margin-bottom:.5em}.old-theme-tab .save-load .import-export,.old-theme-tab .save-load-options .import-export{display:flex}.old-theme-tab .save-load .override,.old-theme-tab .save-load-options .override{margin-left:.5em}.old-theme-tab .save-load-options{flex-wrap:wrap;justify-content:center;margin-top:.5em}.old-theme-tab .save-load-options .keep-option{min-width:25%;margin:0 .5em .5em}.old-theme-tab .radius-item{flex-basis:auto}.old-theme-tab .radius-item,.old-theme-tab .color-item{flex-direction:column;flex:1 1 0;min-width:20em;margin:5px 6px 0 0;display:flex}.old-theme-tab .radius-item.wide,.old-theme-tab .color-item.wide{min-width:60%}.old-theme-tab .radius-item:not(.wide):nth-child(odd),.old-theme-tab .color-item:not(.wide):nth-child(odd){margin-right:7px}.old-theme-tab .radius-item .color,.old-theme-tab .radius-item .opacity,.old-theme-tab .color-item .color,.old-theme-tab .color-item .opacity{align-items:baseline;display:flex}.old-theme-tab .theme-radius-rn,.old-theme-tab .theme-color-cl{box-shadow:none;color:var(--textFaint);background:0 0;border:0;align-self:stretch}.old-theme-tab .theme-color-cl,.old-theme-tab .theme-radius-in,.old-theme-tab .theme-color-in{margin-left:.3em}.old-theme-tab .theme-radius-in{flex:1;min-width:1em;max-width:7em}.old-theme-tab .theme-radius-lb{max-width:50em}.old-theme-tab .theme-warning{align-items:baseline;margin-bottom:.5em;display:flex}.old-theme-tab .theme-warning .buttons .btn{margin-bottom:.5em}.extra-content .apply-container{flex-direction:row;flex-grow:1;justify-content:space-around;display:flex}.extra-content .apply-container .btn{flex-grow:1;min-width:0;max-width:10em;min-height:2em;padding:0}.posts-tab .greentext{color:var(--funtextGreentext)}.profile-tab .setting-section.profile-edit{margin:0}.profile-tab .setting-section.profile-edit h2{margin-left:1rem}.profile-tab .setting-section.profile-edit .user-card{padding:1.2em;overflow:hidden}.profile-tab .setting-item .custom-boolean-setting{grid-template-columns:subgrid;display:grid}.profile-tab .setting-item .custom-boolean-setting .label{text-align:right;grid-area:label}.-mobile .profile-tab .setting-item .custom-boolean-setting .label{text-align:left}.profile-tab .setting-item .custom-boolean-setting .checkbox-indicator{grid-area:control}.mfa-backup-codes .warning{color:var(--cOrange)}.mfa-backup-codes .backup-codes{font-family:var(--monoFont)}.mfa-settings .mfa-heading,.mfa-settings .method-item{flex-wrap:wrap;justify-content:space-between;align-items:baseline;display:flex}.mfa-settings .warning{color:var(--cOrange)}.mfa-settings .setup-otp{flex-wrap:wrap;justify-content:center;display:flex}.mfa-settings .setup-otp .qr-code{flex:1;padding-right:.7em}.mfa-settings .setup-otp .verify{flex:1}.mfa-settings .setup-otp .error{margin:.3em 0 0}.mfa-settings .setup-otp .confirm-otp-actions button{width:15em;margin-top:5px}.StyleTab{min-width:var(--themeEditorMinWidth,-webkit-fit-content);min-width:var(--themeEditorMinWidth,fit-content);margin:1em}.StyleTab .style-control{align-items:baseline;margin-bottom:.5em}.StyleTab .style-control .label{flex:auto;min-height:2em;margin-right:.5em;line-height:2;display:inline-block}.StyleTab .style-control.suboption{margin-left:1em}.StyleTab .style-control input,.StyleTab .style-control select{flex:0;min-width:3em;margin:0}.StyleTab .style-control input[type=number],.StyleTab .style-control select[type=number]{min-width:9em}.StyleTab .style-control input[type=number].-small,.StyleTab .style-control select[type=number].-small{min-width:5em}.StyleTab .style-control input[type=range],.StyleTab .style-control select[type=range]{flex:1;align-self:center;min-width:9em;margin:0 .25em}.StyleTab .style-control input[type=checkbox]+i,.StyleTab .style-control select[type=checkbox]+i{align-self:center;height:1.1em}.StyleTab .setting-section.heading{padding:0}.StyleTab .meta-preview{grid-gap:1em;grid-template:"meta preview";grid-template-columns:-webkit-min-content 6fr;grid-template-columns:min-content 6fr;display:grid}.StyleTab .meta-preview .theme-preview-container{margin:0}.StyleTab .meta-preview ul.setting-list{grid-area:meta;margin:0;padding:0}.StyleTab .meta-preview ul.setting-list>li,.StyleTab .meta-preview ul.setting-list .meta-field{margin:0}.StyleTab .meta-preview ul.setting-list .meta-field .setting-label{margin-bottom:.5em;display:inline-block}.StyleTab .meta-preview #edited-style-preview{grid-area:preview}.StyleTab .setting-section{margin-top:1em;padding-bottom:0}.StyleTab .setting-section .btn{padding:0 .5em}.StyleTab .setting-section:not(:first-child){margin-top:.5em}.StyleTab .setting-section:not(:last-child){margin-bottom:.5em}.StyleTab .list-editor{grid-gap:.5em;grid-template:"label editor""selector editor"1fr"movement editor"/10em 1fr;display:grid}.StyleTab .list-editor .list-edit-area{grid-area:editor;align-items:baseline}.StyleTab .list-editor .list-select{grid-area:selector;margin:0}.StyleTab .list-editor .list-select-label{grid-area:label;align-self:baseline;margin:0;font-weight:700}.StyleTab .list-editor .list-select-movement{grid-area:movement;margin:0}.StyleTab .variables-editor .variable-selector{grid-template-columns:auto 1fr auto 10em;grid-template-rows:subgrid;grid-gap:0 .5em;align-items:baseline;display:grid}.StyleTab .variables-editor .list-edit-area{grid-template-rows:subgrid;display:grid}.StyleTab .variables-editor .shadow-control{grid-row:2/span 2}.StyleTab .component-editor{grid-gap:.5em;grid-template:"component component variant""state state state""preview settings settings"1fr/6fr 3fr 4fr;display:grid}.StyleTab .component-editor .component-selector{grid-area:component;align-self:center}.StyleTab .component-editor .component-selector,.StyleTab .component-editor .state-selector,.StyleTab .component-editor .variant-selector{grid-gap:.5em;grid-template-rows:auto;grid-template-columns:1fr minmax(10em,1fr);grid-auto-flow:column;align-items:baseline;display:grid}.StyleTab .component-editor .component-selector>label:not(.Select),.StyleTab .component-editor .state-selector>label:not(.Select),.StyleTab .component-editor .variant-selector>label:not(.Select){justify-self:right;font-weight:700}.StyleTab .component-editor .state-selector{grid-area:state;grid-template-columns:minmax(-webkit-min-content, 7em) 1fr;grid-template-columns:minmax(min-content,7em) 1fr}.StyleTab .component-editor .variant-selector{grid-area:variant}.StyleTab .component-editor .state-selector-list{grid-auto-flow:dense;grid-template-columns:repeat(5, minmax(-webkit-min-content, 1fr));grid-gap:.5em;grid-template-columns:repeat(5,minmax(min-content,1fr));grid-auto-rows:1fr;margin:0;padding:0;list-style:none;display:grid}.StyleTab .component-editor .preview-container{--border:none;--shadow:none;--roundness:none;grid-area:preview}.StyleTab .component-editor .component-settings{grid-area:settings}.StyleTab .component-editor .editor-tab{grid-column-gap:.5em;grid-template-columns:1fr 2em;grid-auto-rows:-webkit-min-content;grid-auto-rows:min-content;grid-auto-flow:dense;border-left:1px solid var(--border);border-right:1px solid var(--border);border-bottom:1px solid var(--border);align-items:center;padding:.5em;display:grid}.StyleTab .component-editor .shadow-tab{grid-template-columns:1fr;justify-items:center}.-mobile .StyleTab .meta-preview{grid-template:"meta""preview"}.-mobile .StyleTab .list-editor{grid-template:"label""selector"1fr"movement""editor""editor""editor""editor"/1fr;display:grid}.-mobile .StyleTab .component-editor{grid-template:"component""variant""state""preview""settings"/1fr}.-mobile .StyleTab .variable-selector{grid-gap:.5em;grid-template-rows:auto;grid-template-columns:1fr;grid-auto-flow:row}.extra-content .style-actions-container{justify-content:end;width:100%;display:flex}.extra-content .style-actions-container .style-actions{grid-gap:.25em;grid-template-columns:repeat(4,minmax(7em,1fr));display:grid} diff --git a/priv/static/static/css/shout_panel.BHBsqket.css b/priv/static/static/css/shout_panel.BHBsqket.css new file mode 100644 index 000000000..50d0eca34 --- /dev/null +++ b/priv/static/static/css/shout_panel.BHBsqket.css @@ -0,0 +1 @@ +.floating-shout{z-index:var(--ZI_popovers);max-width:25em;position:fixed;bottom:.5em}.floating-shout.-left{left:.5em}.floating-shout:not(.-left){right:.5em}.shout-panel .shout-heading{cursor:pointer}.shout-panel .shout-heading .icon{color:var(--text);margin-right:.5em}.shout-panel .shout-heading .title{justify-content:space-between;align-items:center;display:flex}.shout-panel .shout-window{max-height:20em;overflow:hidden auto}.shout-panel .shout-window-container{height:100%}.shout-panel .shout-message{padding:.2em .5em;display:flex}.shout-panel .shout-avatar img{border-radius:var(--roundness);width:.6em;height:.6em;margin-top:.25em;margin-right:.5em}.shout-panel .shout-input{display:flex}.shout-panel .shout-input textarea{resize:none;flex:1;min-height:3.5em;margin:.6em}.shout-panel .shout-panel .title{justify-content:space-between;display:flex} diff --git a/priv/static/static/css/side_drawer.2CnDtAJj.css b/priv/static/static/css/side_drawer.2CnDtAJj.css new file mode 100644 index 000000000..63d7240fa --- /dev/null +++ b/priv/static/static/css/side_drawer.2CnDtAJj.css @@ -0,0 +1 @@ +.side-drawer-container{z-index:var(--ZI_navbar);align-items:stretch;width:100%;height:100%;transition-property:transform;transition-duration:0s;display:flex;position:fixed;top:0;left:0}.side-drawer-container-open{transform:translate(0%)}.side-drawer-container-closed{transition-delay:.35s;transform:translate(-100%)}.side-drawer-darken{z-index:-1;transition:all .35s;background-color:#00000080;width:100vw;height:100vh;transition-property:background-color;position:fixed;top:0;left:0}.side-drawer-darken-closed{background-color:#0000}.side-drawer-click-outside{flex:100%}.side-drawer{transition:all .35s;width:80%;max-width:20em;box-shadow:var(--shadow);background-color:var(--background);flex:0 0 80%;margin:0 0 0 -100px;padding:0 0 1em 100px;transition-property:transform;transition-timing-function:cubic-bezier(0,1,.5,1);overflow-x:hidden}.side-drawer .badge{margin-left:10px}.side-drawer-logo-wrapper{align-items:center;padding:.85em;display:flex}.side-drawer-logo-wrapper img{flex:none;height:50px;margin-right:.85em}.side-drawer-logo-wrapper span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.side-drawer-click-outside-closed{flex:0 0 0}.side-drawer-closed{transform:translate(-100%)}.side-drawer-heading{background:0 0;flex-direction:column;align-items:stretch;margin:0;padding:0;display:flex}.side-drawer-heading .user-info{margin:1em}.side-drawer ul{border-bottom:1px solid;border-color:var(--border);margin:0;padding:0;list-style:none}.side-drawer ul:last-child{border:0}.side-drawer li{padding:0}.side-drawer li a,.side-drawer li button{box-sizing:border-box;height:3em;padding:0 .7em;line-height:3em;display:block} diff --git a/priv/static/static/css/status_history_modal.CZzNzXP5.css b/priv/static/static/css/status_history_modal.CZzNzXP5.css new file mode 100644 index 000000000..fe0df3392 --- /dev/null +++ b/priv/static/static/css/status_history_modal.CZzNzXP5.css @@ -0,0 +1 @@ +.modal-view.status-history-modal-view{align-items:flex-start}.status-history-modal-panel{flex-shrink:0;width:100%;max-width:700px;margin-top:25%;margin-bottom:2em}@media (orientation:landscape){.status-history-modal-panel{margin-top:8%}} diff --git a/priv/static/static/css/sticker_picker.BzJVB0Ta.css b/priv/static/static/css/sticker_picker.BzJVB0Ta.css deleted file mode 100644 index bae400571..000000000 --- a/priv/static/static/css/sticker_picker.BzJVB0Ta.css +++ /dev/null @@ -1 +0,0 @@ -.sticker-picker{width:100%}.sticker-picker .contents{min-height:250px}.sticker-picker .contents .sticker-picker-content{display:-webkit-box;display:flex;flex-wrap:wrap;padding:0 4px}.sticker-picker .contents .sticker-picker-content .sticker{display:-webkit-box;display:flex;-webkit-box-flex:1;flex:1 1 auto;margin:4px;width:56px;height:56px}.sticker-picker .contents .sticker-picker-content .sticker img{height:100%}.sticker-picker .contents .sticker-picker-content .sticker img:hover{-webkit-filter:drop-shadow(0 0 5px var(--accent));filter:drop-shadow(0 0 5px var(--accent))} diff --git a/priv/static/static/css/sticker_picker.C2lbaygI.css b/priv/static/static/css/sticker_picker.C2lbaygI.css new file mode 100644 index 000000000..1d642ee90 --- /dev/null +++ b/priv/static/static/css/sticker_picker.C2lbaygI.css @@ -0,0 +1 @@ +.sticker-picker{width:100%}.sticker-picker .contents{min-height:250px}.sticker-picker .contents .sticker-picker-content{flex-wrap:wrap;padding:0 4px;display:flex}.sticker-picker .contents .sticker-picker-content .sticker{flex:auto;width:56px;height:56px;margin:4px;display:flex}.sticker-picker .contents .sticker-picker-content .sticker img{height:100%}.sticker-picker .contents .sticker-picker-content .sticker img:hover{filter:drop-shadow(0 0 5px var(--accent))} diff --git a/priv/static/static/css/string_setting.CtukauKH.css b/priv/static/static/css/string_setting.CtukauKH.css deleted file mode 100644 index f85b58786..000000000 --- a/priv/static/static/css/string_setting.CtukauKH.css +++ /dev/null @@ -1 +0,0 @@ -.vertical-tab-switcher{display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-direction:row;container-type:inline-size}.vertical-tab-switcher>.tabs{-webkit-box-flex:0;flex:0 0 15em;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;overflow:hidden auto;white-space:nowrap;text-overflow:ellipsis;width:15em;min-width:15em;border-right:1px solid;border-right-color:var(--border);box-sizing:border-box}.vertical-tab-switcher>.tabs>.menu-item{padding:.5em 1em}.vertical-tab-switcher>.tabs>.menu-item .tab-icon{vertical-align:middle;margin-right:.75em}.vertical-tab-switcher>.contents .tab-content-label{box-sizing:border-box;margin:0;border-bottom:1px solid var(--border);display:none}.vertical-tab-switcher>.contents .tab-content-label button{box-sizing:border-box;padding:.5em}.vertical-tab-switcher>.contents .tab-slot-wrapper{-webkit-box-flex:1;flex:1 1 auto;height:100%;overflow-y:auto;display:grid;grid-template-columns:minmax(1em,1fr) minmax(-webkit-min-content,45em) minmax(1em,1fr);grid-template-columns:minmax(1em,1fr) minmax(min-content,45em) minmax(1em,1fr);grid-template-areas:". content .";-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column}.vertical-tab-switcher>.contents .tab-slot-wrapper .tab-content{grid-area:content}.vertical-tab-switcher>.contents .tab-slot-wrapper .tab-content.-full-width{grid-column:1/4}.vertical-tab-switcher>.contents .tab-slot-wrapper .tab-content.-full-height>*{height:100%}.vertical-tab-switcher>.contents .tab-content-wrapper{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;height:100%;-webkit-box-flex:1;flex:1 1 auto}.vertical-tab-switcher>.contents .tab-content-wrapper.-hidden{display:none}.vertical-tab-switcher.-mobile>.contents .tab-content-label{display:block}.vertical-tab-switcher.-mobile.-nav-contents>.contents{display:block;-webkit-box-flex:1;flex-grow:1;flex-shrink:1}.vertical-tab-switcher.-mobile.-nav-contents>.tabs{display:none;-webkit-box-flex:0;flex-grow:0;flex-shrink:1}.vertical-tab-switcher.-mobile.-nav-tabs>.tabs{display:block;-webkit-box-flex:1;flex-grow:1}.vertical-tab-switcher.-mobile.-nav-tabs>.contents{display:none;-webkit-box-flex:0;flex-grow:0;flex-shrink:1}.vertical-tab-switcher.-mobile .tab-slot-wrapper{grid-template-columns:0 minmax(-webkit-min-content,45em) 0;grid-template-columns:0 minmax(min-content,45em) 0}@supports (container-type: inline-size){.vertical-tab-switcher.-nav-contents>.contents,.vertical-tab-switcher.-nav-tabs>.contents,.vertical-tab-switcher.-mobile.-nav-contents>.contents,.vertical-tab-switcher.-mobile.-nav-tabs>.contents{display:block;-webkit-box-flex:1;flex-grow:1}.vertical-tab-switcher.-nav-contents>.tabs,.vertical-tab-switcher.-nav-tabs>.tabs,.vertical-tab-switcher.-mobile.-nav-contents>.tabs,.vertical-tab-switcher.-mobile.-nav-tabs>.tabs{display:block;-webkit-box-flex:0;flex-grow:0}@container (width < 50em){.vertical-tab-switcher>.contents .tab-content-label{display:block}.vertical-tab-switcher.-mobile>.contents .tab-content-label{display:block}.vertical-tab-switcher.-nav-contents>.contents,.vertical-tab-switcher.-mobile.-nav-contents>.contents{display:block;-webkit-box-flex:1;flex-grow:1}.vertical-tab-switcher.-nav-contents>.tabs,.vertical-tab-switcher.-mobile.-nav-contents>.tabs{display:none;-webkit-box-flex:0;flex-grow:0;flex-shrink:1}.vertical-tab-switcher.-nav-tabs>.tabs,.vertical-tab-switcher.-mobile.-nav-tabs>.tabs{display:block;-webkit-box-flex:1;flex-grow:1}.vertical-tab-switcher.-nav-tabs>.contents,.vertical-tab-switcher.-mobile.-nav-tabs>.contents{display:none;-webkit-box-flex:0;flex-grow:0;flex-shrink:1}}}.ModifiedIndicator{display:inline-block;position:relative}.modified-tooltip{margin:.5em 1em;min-width:10em;text-align:center}.DraftButtons{display:-webkit-inline-box;display:inline-flex;position:relative;gap:.5em;margin-top:.5em}.draft-tooltip{margin:.5em 1em;min-width:10em;text-align:center}.-mobile .ChoiceSetting.setting-item{display:block}.-mobile .ChoiceSetting.setting-item .setting-label{display:block;margin-bottom:.5em} diff --git a/priv/static/static/css/tab_switcher.CEadvXLu.css b/priv/static/static/css/tab_switcher.CEadvXLu.css new file mode 100644 index 000000000..2c11cc953 --- /dev/null +++ b/priv/static/static/css/tab_switcher.CEadvXLu.css @@ -0,0 +1 @@ +.tab-switcher{display:flex}.tab-switcher .tab-icon{margin:.2em auto;display:block}.tab-switcher.top-tabs{flex-direction:column}.tab-switcher.top-tabs>.tabs{flex-direction:row;flex:none;width:100%;padding-top:5px;overflow:auto hidden}.tab-switcher.top-tabs>.tabs:after,.tab-switcher.top-tabs>.tabs:before{content:"";border-bottom:1px solid;border-bottom-color:var(--border);flex:auto}.tab-switcher.top-tabs>.tabs .tab-wrapper{height:2em}.tab-switcher.top-tabs>.tabs .tab-wrapper:not(.active):after{border-bottom:1px solid;border-bottom-color:var(--border);bottom:0;left:0;right:0}.tab-switcher.top-tabs>.tabs .tab{border-bottom-right-radius:0;border-bottom-left-radius:0;width:100%;min-width:1px;margin-bottom:-93px;padding-bottom:99px}.tab-switcher.top-tabs .contents.scrollable-tabs{flex-basis:0;position:relative}.tab-switcher .contents{flex:1 0 auto;min-height:0;position:relative}.tab-switcher .contents .hidden{display:none}.tab-switcher .contents .-full-height:not(.hidden){flex-direction:column;height:100%;display:flex}.tab-switcher .contents .-full-height:not(.hidden)>:not(.mobile-label){flex:1}.tab-switcher .contents .-full-width:not(.hidden){flex-direction:column;display:flex}.tab-switcher .contents .-full-width:not(.hidden)>:not(.mobile-label){width:auto}.tab-switcher .contents.scrollable-tabs{overflow-y:auto}.tab-switcher .tab{-webkit-user-select:none;user-select:none;color:var(--text);cursor:pointer;box-shadow:var(--shadow);font-size:1em;font-family:var(--font);border-radius:var(--roundness);background-color:var(--background);white-space:nowrap;border:none;padding:6px 1em;position:relative}.tab-switcher .tab:not(.active){z-index:4;margin-top:.25em}.tab-switcher .tab:not(.active):hover{z-index:6}.tab-switcher .tab.active{z-index:5;background:0 0}.tab-switcher .tab img{vertical-align:top;max-height:1.9em;margin-top:-.3em}.tab-switcher .tabs{box-sizing:border-box;display:flex;position:relative}.tab-switcher .tabs:after,.tab-switcher .tabs:before{flex:auto;display:block}.tab-switcher .tab-wrapper{flex:none;display:flex;position:relative}.tab-switcher .tab-wrapper:not(.active):after{content:"";z-index:7;position:absolute}.tab-switcher .mobile-label{border-bottom:1px solid var(--border);margin-top:.5em;margin-bottom:.25em;margin-left:.2em;padding-bottom:.25em;padding-left:.3em}@media (width>=800px){.tab-switcher .mobile-label{display:none}} diff --git a/priv/static/static/css/terms_of_service_panel.ClR_b2MD.css b/priv/static/static/css/terms_of_service_panel.ClR_b2MD.css new file mode 100644 index 000000000..4db7f26ac --- /dev/null +++ b/priv/static/static/css/terms_of_service_panel.ClR_b2MD.css @@ -0,0 +1 @@ +.tos-content{margin:1em} diff --git a/priv/static/static/css/update_notification.BJ1CAGCw.css b/priv/static/static/css/update_notification.BJ1CAGCw.css deleted file mode 100644 index f261faefb..000000000 --- a/priv/static/static/css/update_notification.BJ1CAGCw.css +++ /dev/null @@ -1 +0,0 @@ -.UpdateNotification{overflow:hidden}.UpdateNotificationModal{--__top-fringe: 15em;--__bottom-fringe: 80em;--__right-fringe: 8em;font-size:15px;position:relative;-webkit-transition:-webkit-transform;transition:-webkit-transform;transition:transform;transition:transform,-webkit-transform;-webkit-transition-timing-function:ease-in-out;transition-timing-function:ease-in-out;-webkit-transition-duration:.5s;transition-duration:.5s}.UpdateNotificationModal .text{max-width:40em;padding-left:1em}@media all and (width <= 800px){.UpdateNotificationModal{width:100vw}}@media all and (height <= 600px){.UpdateNotificationModal{display:none}}.UpdateNotificationModal .content{overflow:hidden;margin-top:calc(-1 * var(--__top-fringe));margin-bottom:calc(-1 * var(--__bottom-fringe));margin-right:calc(-1 * var(--__right-fringe))}.UpdateNotificationModal .content.-noImage .text{padding-right:var(--__right-fringe)}.UpdateNotificationModal .panel-body{border-width:0 0 1px;border-style:solid;border-color:var(--border)}.UpdateNotificationModal .panel-footer{z-index:22;position:relative;border-width:0;grid-template-columns:auto}.UpdateNotificationModal .pleroma-tan{object-fit:cover;object-position:top;-webkit-transition:position,left,right,top,bottom,max-width,max-height;transition:position,left,right,top,bottom,max-width,max-height;-webkit-transition-timing-function:ease-in-out;transition-timing-function:ease-in-out;-webkit-transition-duration:.5s;transition-duration:.5s;width:25em;float:right;z-index:20;position:relative;shape-margin:.5em;-webkit-filter:drop-shadow(5px 5px 10px rgba(0,0,0,.5));filter:drop-shadow(5px 5px 10px rgba(0,0,0,.5));pointer-events:none}.UpdateNotificationModal .spacer-top{min-height:var(--__top-fringe)}.UpdateNotificationModal .spacer-bottom{min-height:var(--__bottom-fringe)}.UpdateNotificationModal .extra-info-group{-webkit-transition:max-height,padding,height;transition:max-height,padding,height;-webkit-transition-timing-function:ease-in;transition-timing-function:ease-in;-webkit-transition-duration:.7s;transition-duration:.7s;max-height:70vh;-webkit-mask:-webkit-linear-gradient(bottom,white,transparent) bottom/100% 2px no-repeat,-webkit-linear-gradient(bottom,white,white);mask:linear-gradient(to top,white,transparent) bottom/100% 2px no-repeat,linear-gradient(to top,white,white)}.UpdateNotificationModal .art-credit{text-align:right}.UpdateNotificationModal.-peek{-webkit-transform:translateY(calc((100vh - 100%)/2));transform:translateY(calc((100vh - 100%)/2))}.UpdateNotificationModal.-peek .pleroma-tan{float:right;z-index:10;shape-image-threshold:70%}.UpdateNotificationModal.-peek .extra-info-group{max-height:0} diff --git a/priv/static/static/css/update_notification.Co8luhDO.css b/priv/static/static/css/update_notification.Co8luhDO.css new file mode 100644 index 000000000..d49f70fc8 --- /dev/null +++ b/priv/static/static/css/update_notification.Co8luhDO.css @@ -0,0 +1 @@ +.UpdateNotification{overflow:hidden}.UpdateNotificationModal{--__top-fringe:15em;--__bottom-fringe:80em;--__right-fringe:8em;--__panel-background:rgb(0,255,255 / 80%);transition:transform;font-size:15px;transition-duration:.5s;transition-timing-function:ease-in-out;position:relative}.UpdateNotificationModal .text{max-width:40em;padding-left:1em}.UpdateNotificationModal .footer-buttons{--panel-heading-height-padding:calc(var(--panel-header-height) * .2);--__panel-heading-gap:calc(var(--panel-header-height) * .1565);--__panel-heading-height:var(--panel-header-height);--__panel-heading-height-inner:calc(var(--__panel-heading-height) - 2 * var(--panel-heading-height-padding,0));box-sizing:border-box;z-index:20;grid-column-gap:var(--__panel-heading-gap);padding:var(--panel-heading-height-padding);height:var(--__panel-heading-height);line-height:var(--__panel-heading-height-inner);font-size:calc(var(--panelHeaderSize) / 3.2);grid-template-columns:auto;grid-auto-columns:auto;grid-auto-flow:column;display:grid;position:absolute;bottom:0;left:0;right:0}@media (width<=800px){.UpdateNotificationModal{width:100vw}}@media (height<=600px){.UpdateNotificationModal{display:none}}.UpdateNotificationModal .content{margin-top:calc(-1 * var(--__top-fringe));margin-bottom:calc(-1 * var(--__bottom-fringe));margin-right:calc(-1 * var(--__right-fringe));overflow:hidden}.UpdateNotificationModal .content.-noImage .text{padding-right:var(--__right-fringe)}.UpdateNotificationModal .panel-body{border-style:solid;border-width:0 0 1px;border-color:var(--border)}.UpdateNotificationModal .panel-footer{z-index:10;border-width:0;position:relative}.UpdateNotificationModal .panel-footer .button-default{z-index:2}.UpdateNotificationModal .pleroma-tan{object-fit:cover;object-position:top;transition:position,left,right,top,bottom,max-width,max-height;float:right;z-index:20;shape-margin:.5em;filter:drop-shadow(5px 5px 10px #00000080);pointer-events:none;width:25em;transition-duration:.5s;transition-timing-function:ease-in-out;position:relative}.UpdateNotificationModal .spacer-top{min-height:var(--__top-fringe)}.UpdateNotificationModal .spacer-bottom{min-height:calc(var(--__bottom-fringe) + var(--panel-header-height))}.UpdateNotificationModal .extra-info-group{transition:max-height,padding,height;-webkit-mask:-webkit-linear-gradient(#fff,#0000) bottom/100% 2px no-repeat,-webkit-linear-gradient(#fff,#fff);max-height:70vh;transition-duration:.7s;transition-timing-function:ease-in;-webkit-mask:linear-gradient(#0000,#fff) bottom/100% 2px no-repeat,linear-gradient(#fff,#fff);mask:linear-gradient(#0000,#fff) bottom/100% 2px no-repeat,linear-gradient(#fff,#fff)}.UpdateNotificationModal .art-credit{text-align:right}.UpdateNotificationModal.-peek{transform:translateY(calc(50vh - 50%))}.UpdateNotificationModal.-peek .pleroma-tan{float:right;z-index:10;shape-image-threshold:70%}.UpdateNotificationModal.-peek .extra-info-group{max-height:0} diff --git a/priv/static/static/css/user_card.C2QiPTUj.css b/priv/static/static/css/user_card.C2QiPTUj.css new file mode 100644 index 000000000..aef67f754 --- /dev/null +++ b/priv/static/static/css/user_card.C2QiPTUj.css @@ -0,0 +1 @@ +.Avatar{--_avatarShadowBox:var(--shadow);--_avatarShadowFilter:var(--shadowFilter);--_avatarShadowInset:var(--shadowInset);--_still-image-label-visibility:hidden;width:3.5em;height:3.5em;display:inline-block;position:relative}.Avatar.-compact{border-radius:var(--roundness);width:2.2em;height:2.2em}.Avatar .avatar{width:100%;height:100%;box-shadow:var(--_avatarShadowBox);border-radius:var(--roundness)}.Avatar .avatar.-better-shadow{box-shadow:var(--_avatarShadowInset);filter:var(--_avatarShadowFilter)}.Avatar .avatar.-animated:before{display:none}.Avatar .avatar.-compact{border-radius:var(--roundness)}.Avatar .avatar.-placeholder{background-color:var(--background)}.Avatar img{width:100%;height:100%}.Avatar .actor-type-indicator{color:#fff;border-radius:var(--roundness);background:#7f7f7f80;margin:-.2em;padding:.2em;position:absolute;bottom:0;right:0}.color-input{flex-wrap:wrap;max-width:10em;display:inline-flex}.color-input.-compact{max-width:none}.color-input .label{flex:auto;grid-area:label}.color-input .opt{grid-area:checkbox;margin-right:.5em}.color-input-field.input{flex:10em;grid-area:input;align-items:stretch;max-width:10em;display:flex}.color-input-field.input input{color:var(--text);background:0 0;border:none;margin:0;padding:0}.color-input-field.input input.textColor{flex:1 0 3em;min-width:3em;padding:0}.color-input-field.input .nativeColor{cursor:pointer;flex:none;padding:0}.color-input-field.input .nativeColor input{appearance:none;min-width:0;max-width:0;max-height:0;opacity:0!important}.color-input-field.input .computedIndicator,.color-input-field.input .validIndicator,.color-input-field.input .invalidIndicator,.color-input-field.input .transparentIndicator{border-radius:var(--roundness);flex:0 0 2em;align-self:stretch;min-width:2em;min-height:1.1em;margin:.2em .5em}.color-input-field.input .invalidIndicator{box-sizing:border-box;border:2px solid var(--cRed);background:0 0}.color-input-field.input .transparentIndicator{background-color:#f0f;position:relative}.color-input-field.input .transparentIndicator:before,.color-input-field.input .transparentIndicator:after{content:"";background-color:#000;width:50%;height:50%;display:block;position:absolute}.color-input-field.input .transparentIndicator:after{border-top-left-radius:var(--roundness);top:0;left:0}.color-input-field.input .transparentIndicator:before{border-bottom-right-radius:var(--roundness);bottom:0;right:0}.color-input-field.input.disabled .nativeColor input,.color-input-field.input.disabled .computedIndicator,.color-input-field.input.disabled .validIndicator,.color-input-field.input.disabled .invalidIndicator,.color-input-field.input.disabled .transparentIndicator,.color-input-field.input:disabled .nativeColor input,.color-input-field.input:disabled .computedIndicator,.color-input-field.input:disabled .validIndicator,.color-input-field.input:disabled .invalidIndicator,.color-input-field.input:disabled .transparentIndicator{opacity:.25!important}.user-card{z-index:1;border-top-left-radius:calc(var(--roundness) - 1px);border-top-right-radius:calc(var(--roundness) - 1px);position:relative;overflow:hidden}.user-card h4{line-height:2;display:flex}.user-card h3{margin-bottom:0;padding-left:.6em}.user-card h3 .button-default{padding:0 .6em;font-size:1rem;line-height:2}.user-card summary{cursor:pointer;font-weight:700}.user-card details{margin:.5em 0}.user-card details[open] summary{margin-bottom:.5em}.user-card details ul{margin:0}.user-card .input.bio{height:auto}.user-card .follow-button{flex:1 0 auto}.user-card .user-card-inner{padding-bottom:0}.user-card-setting,.user-card-bio{color:var(--lightText);padding:0 .6em;line-height:1.3;display:block}.user-card-setting img,.user-card-bio img{object-fit:contain;vertical-align:middle;max-width:100%;max-height:400px}.user-card .user-card-setting{margin-left:.6em;margin-right:.6em}.user-card .user-card-bio{text-align:center;margin:.6em}.user-card .user-card-bio.input{margin:0 1em}.user-card .user-card-bio.input textarea{text-align:inherit}.user-card .user-card-bio,.user-card .user-card-bio *{line-height:1.5}.user-card .user-card-bio.-justify-left{text-align:start}.user-card:hover{--_still-image-img-visibility:visible;--_still-image-canvas-visibility:hidden;--_still-image-label-visibility:hidden}.user-card .admin-data,.user-card .personal-marks{margin:0 .6em;padding:0 .6em}.user-card .admin-data:not(:last-child),.user-card .personal-marks:not(:last-child){border-bottom:1px dotted var(--border)}.user-card .admin-data .highlighter,.user-card .personal-marks .highlighter{margin-bottom:.5em}.user-card .admin-data .highlighter h4,.user-card .personal-marks .highlighter h4{margin-top:.6em}.user-card .admin-data .highlighter .userHighlightSel,.user-card .personal-marks .highlighter .userHighlightSel{vertical-align:bottom;margin-left:-.1em}.user-card .admin-data .highlighter .userHighlightSel.-none select,.user-card .personal-marks .highlighter .userHighlightSel.-none select{color:var(--textFaint)}.user-card .admin-data .highlighter .userHighlightSel.-none select option,.user-card .personal-marks .highlighter .userHighlightSel.-none select option{color:var(--text);background:var(--background)}.user-card .admin-data .highlighter .highlighter-color,.user-card .personal-marks .highlighter .highlighter-color{vertical-align:bottom;margin-left:.6em}.user-card .header-overlay{-webkit-mask:-webkit-linear-gradient(#0000 0,#fff 5em) bottom no-repeat;position:absolute;inset:-1.4em -1.2em 0;-webkit-mask:linear-gradient(0deg,#0000 0,#fff 5em) bottom no-repeat;mask:linear-gradient(0deg,#0000 0,#fff 5em) bottom no-repeat}.user-card .banner-overlay,.user-card .banner-image{border-top-left-radius:calc(var(--roundness) - 1px);border-top-right-radius:calc(var(--roundness) - 1px);padding:0;position:absolute;inset:0}.user-card .banner-image{z-index:-2}.user-card .banner-image img{object-fit:cover;width:100%;height:100%}.user-card .banner-overlay{background-color:var(--profileTint);opacity:.5;pointer-events:none;z-index:-1}.user-card .bottom-buttons{gap:.5em;display:flex}.user-info{text-align:left;flex-direction:column;gap:1em;display:flex;position:relative}.user-info .user-identity{align-items:flex-end;min-height:6em;display:flex;position:relative;container:user-card/inline-size}.user-info .user-identity:not(.-compact){aspect-ratio:3}.user-info .user-identity>*{min-width:0}.user-info .user-identity>a{vertical-align:middle;display:flex}.user-info .user-identity .Avatar{--_avatarShadowBox:var(--avatarShadow);--_avatarShadowFilter:var(--avatarShadowFilter);--_avatarShadowInset:var(--avatarShadowInset);object-fit:cover;width:min(7em,20cqw);height:min(7em,20cqw)}.user-info .other-actions{grid-gap:.6em;z-index:2;grid-auto-flow:column;align-self:end;font-size:1.25rem;display:inline-grid;position:relative}.user-info .other-actions a,.user-info .other-actions button,.user-info .other-actions div{text-align:center;width:2em;height:2em;margin:-.6em;padding:.6em;line-height:2em}.user-info .other-actions a.edit-banner-button,.user-info .other-actions button.edit-banner-button,.user-info .other-actions div.edit-banner-button{width:auto}.user-info .other-actions a:hover .icon,.user-info .other-actions button:hover .icon,.user-info .other-actions div:hover .icon{color:var(--textFaint)}.user-info .other-actions a:not(:hover) .icon,.user-info .other-actions button:not(:hover) .icon,.user-info .other-actions div:not(:hover) .icon{color:var(--lightText)}.user-info-avatar{cursor:pointer;margin-right:.6em;position:relative}.user-info-avatar.-overlay{border-radius:var(--roundness);opacity:0;justify-content:center;align-items:center;transition:opacity .2s;display:flex;position:absolute;inset:-.6em -1.2em -.6em -.6em}.user-info-avatar.-overlay svg{color:#fff;margin:.5em}.user-info-avatar:hover .user-info-avatar.-overlay{opacity:1;background-color:#0000004d}.user-info .user-info-avatar.-editable .-overlay{opacity:1;justify-content:end;place-items:start end}.user-info .user-summary{text-align:left;text-overflow:ellipsis;white-space:nowrap;z-index:1;filter:drop-shadow(0 0 .5em var(--profileTint)) drop-shadow(0 0 .2em var(--profileTint));flex-direction:column;flex:1 1 0;align-self:stretch;margin-left:.6em;line-height:2em;display:flex}.user-info .user-summary .alert{text-shadow:none}.user-info .user-summary{--emoji-size:1.7em}.user-info .user-summary .RichContent{--link:var(--text)!important}.user-info .user-summary .name-wrapper{align-items:baseline;display:flex}.user-info .user-summary .name-wrapper .edit-button{text-align:center;width:3em}.user-info .user-summary .name-wrapper .edit-button:hover .icon{color:var(--textFaint)}.user-info .user-summary .name-wrapper .edit-button:not(:hover) .icon{color:var(--lightText)}.user-info .user-summary .name-wrapper .input,.user-info .user-summary .name-wrapper .user-name{text-overflow:ellipsis;flex:1;margin-right:1em;font-weight:600;line-height:2;overflow:hidden}.user-info .user-summary .name-wrapper .input{margin:0 -.5em;padding:0 .5em}.user-info .user-summary .top-line{flex-direction:column;flex:1;justify-content:space-between;margin-bottom:-.2em;font-size:max(110%,4cqw);line-height:2;display:flex}.user-info .user-summary .bottom-line{white-space:normal;flex-wrap:wrap;align-items:baseline;margin-top:.5em;font-size:max(90%,2.5cqw);font-weight:500;line-height:1.5;display:flex}.user-info .user-summary .lock-icon{vertical-align:baseline;margin:0 .35em 0 .5em;display:inline-block;overflow-x:hidden}.user-info .user-summary .user-screen-name{color:var(--text);text-overflow:ellipsis;display:inline-block;overflow:hidden}.user-info .user-summary .user-role{vertical-align:baseline;display:inline-block}.user-info .user-interactions{flex-wrap:wrap;gap:.6em;display:flex;position:relative}.user-info .user-interactions>*{flex:0 0 8em}.user-info .user-interactions .popover-trigger-button,.user-info .user-interactions .moderation-tools-button{width:100%}.user-info .user-interactions>.btn-group,.user-info .user-interactions>button{white-space:nowrap}.sidebar .edit-profile-button{display:none}.user-extras{padding:1.2em .6em;line-height:1.5}.user-extras .user-stats{text-align:center;word-wrap:break-word;width:100%;display:block}.user-extras .user-stats dl{white-space:nowrap;margin-right:1em;display:inline}.user-extras .user-stats dd{text-overflow:ellipsis;margin:0;font-weight:bolder;overflow:hidden}.user-extras .user-stats dd .svg-inline--fa{color:var(--text)}.user-extras .user-stats dd,.user-extras .user-stats dt{display:inline}.user-extras .user-stats a{text-decoration:none}.user-extras .birthday{text-align:center;white-space:nowrap;--icon:var(--text);width:100%}.user-profile-fields{--emoji-size:1.8em;flex-direction:column;margin:0 .5em;padding:0 .5em;display:flex}.user-profile-fields .user-profile-field-add,.user-profile-fields .user-profile-field{border:1px solid var(--border);border-radius:var(--roundness);align-items:center;margin:.25em;line-height:2em;display:flex}.user-profile-fields .user-profile-field-add .label,.user-profile-fields .user-profile-field .label{margin-left:.5em}.user-profile-fields .user-profile-field-add{justify-content:center;margin:.25em}.user-profile-fields .user-profile-field .input{text-align:inherit;flex:1}.user-profile-fields .user-profile-field .delete-field{text-align:center;width:2em;display:inline-block}.user-profile-fields .user-profile-field .user-profile-field-name,.user-profile-fields .user-profile-field .user-profile-field-value,.user-profile-fields .user-profile-field .user-profile-field-add{text-overflow:ellipsis;white-space:nowrap;box-sizing:border-box;display:inline-flex;overflow:hidden}.user-profile-fields .user-profile-field .user-profile-field-name.-edit,.user-profile-fields .user-profile-field .user-profile-field-value.-edit,.user-profile-fields .user-profile-field .user-profile-field-add.-edit{padding:0}.user-profile-fields .user-profile-field .user-profile-field-name.-edit input,.user-profile-fields .user-profile-field .user-profile-field-value.-edit input,.user-profile-fields .user-profile-field .user-profile-field-add.-edit input{font-weight:inherit}.user-profile-fields .user-profile-field .user-profile-field-name{text-align:right;color:var(--lightText);border-right:1px solid var(--border);flex:0 50%;justify-content:end;min-width:9em;padding-left:1.2em;padding-right:.6em;font-weight:600}.user-profile-fields .user-profile-field .user-profile-field-value{color:var(--text);flex:55%;margin:0;padding-left:.6em}.edit-image .panel-body{text-align:center;flex-direction:column;display:flex}.edit-image .current-avatar{object-fit:contain}.edit-image .image-container{aspect-ratio:1;flex:1 0 20em;align-self:center;gap:.5em;max-width:100%;max-height:30em;margin:0 0 1em;display:flex}.edit-image .image-container.-banner{aspect-ratio:3;max-width:100%}.edit-image .image-container .new-image{flex-direction:column;display:flex}.edit-image .image-container .cropper{flex:1;overflow-x:auto}.edit-image .image-container>*{aspect-ratio:1;flex:1 0 10em;max-height:100%}.edit-image .image-container .separator{min-width:1.1em;aspect-ratio:unset;flex:0 5em;align-self:center;font-size:500%}.edit-image.-banner .images-container{grid-template-rows:20em 5em 20em;grid-template-columns:1fr}.edit-image.-banner .images-container>*{aspect-ratio:3;flex:1 0 10em;width:100%}.edit-image.-banner .separator{min-height:1.1em;aspect-ratio:unset;flex:0 5em;justify-self:center;font-size:500%}#modal.-mobile .edit-image #pick-image{height:3em}#modal.-mobile .edit-image .image-container.-banner{max-height:10em} diff --git a/priv/static/static/css/user_note.A3UYQeEG.css b/priv/static/static/css/user_note.A3UYQeEG.css new file mode 100644 index 000000000..9b9dfe009 --- /dev/null +++ b/priv/static/static/css/user_note.A3UYQeEG.css @@ -0,0 +1 @@ +.user-note{position:relative}.user-note h4{justify-content:space-between;margin-bottom:.5em}.user-note .note-text{align-self:stretch;min-width:100%;margin:-.6em}.user-note .overlay{background-color:#0000004d;position:absolute;inset:0}.user-note .overlay .panel-loading{font-size:1em} diff --git a/priv/static/static/css/user_profile.CC-U32e-.css b/priv/static/static/css/user_profile.CC-U32e-.css new file mode 100644 index 000000000..f948f2ae7 --- /dev/null +++ b/priv/static/static/css/user_profile.CC-U32e-.css @@ -0,0 +1 @@ +.user-profile{flex:2}.user-profile .card-wrapper{border-top-left-radius:var(--roundness);border-top-right-radius:var(--roundness)}.user-profile .panel-footer{border-bottom-left-radius:var(--roundness);border-bottom-right-radius:var(--roundness)}.user-profile{--currentPanelStack:0}.user-profile .userlist-placeholder{justify-content:center;align-items:center;padding:2em;display:flex}.user-profile .user-info{margin:1.2em}.user-profile.-admin-view .filter{padding:1em}.user-profile.-admin-view .list-item{border-bottom:1px solid var(--border);padding:0}.user-profile.-admin-view .list-item .Status{width:100%}.user-profile.-admin-view .footer{background:var(--background)}.user-profile-placeholder .panel-body{justify-content:center;align-items:center;padding:7em;display:flex}.user-profile-placeholder .alert{border-width:2px;padding:.75em 5em}.user-profile-placeholder .alert .error-message{color:var(--text);font-weight:700} diff --git a/priv/static/static/css/user_reporting_modal.CVdwO3Sc.css b/priv/static/static/css/user_reporting_modal.CVdwO3Sc.css new file mode 100644 index 000000000..77dbac433 --- /dev/null +++ b/priv/static/static/css/user_reporting_modal.CVdwO3Sc.css @@ -0,0 +1 @@ +.user-reporting-panel{width:90vw;max-width:50rem;min-height:20vh;max-height:80vh}.user-reporting-panel .user-link{display:inline}.user-reporting-panel .panel-body{border-top:1px solid;border-color:var(--border);flex-direction:column-reverse;display:flex;overflow:hidden}.user-reporting-panel-left{line-height:var(--post-line-height);box-sizing:border-box;padding:1.1em .7em .7em}.user-reporting-panel-left>div{margin-bottom:1em}.user-reporting-panel-left>div:last-child{margin-bottom:0}.user-reporting-panel-left p{margin-top:0}.user-reporting-panel-left textarea.form-control{resize:none;width:100%;min-height:44px;line-height:1.1;transition:min-height .2s .1s;overflow:hidden}.user-reporting-panel-left .btn{min-width:10em;padding:0 2em}.user-reporting-panel-left .alert{margin:1em 0 0;line-height:1.3em}.user-reporting-panel-right{flex-direction:column;display:flex;overflow-y:auto}@media (width>=801px){.user-reporting-panel .panel-body{flex-direction:row}.user-reporting-panel-left{border-right:1px solid;border-color:var(--border);width:50%;max-width:320px;padding:1.1em}.user-reporting-panel-left>div{margin-bottom:2em}.user-reporting-panel-right{flex:auto;width:50%;margin-bottom:12px}} diff --git a/priv/static/static/css/user_timed_filter_modal.ClNyvF9i.css b/priv/static/static/css/user_timed_filter_modal.ClNyvF9i.css new file mode 100644 index 000000000..0d55c27f2 --- /dev/null +++ b/priv/static/static/css/user_timed_filter_modal.ClNyvF9i.css @@ -0,0 +1 @@ +.UserTimedFilterModal .input-dont-ask-again{margin-left:1em}.UserTimedFilterModal .input-expire-at{margin-left:.25em}.UserTimedFilterModal .footer-left-checkbox{width:max-content}.UserTimedFilterModal .expirationTime{white-space:nowrap;padding:.5em;display:inline-flex}.UserTimedFilterModal .dialog-modal-content .checkbox{vertical-align:middle;padding:.5em} diff --git a/priv/static/static/css/vertical_tab_switcher.DU3QPNwr.css b/priv/static/static/css/vertical_tab_switcher.DU3QPNwr.css new file mode 100644 index 000000000..5f01f9bee --- /dev/null +++ b/priv/static/static/css/vertical_tab_switcher.DU3QPNwr.css @@ -0,0 +1 @@ +.DraftButtons{gap:.5em;margin-top:.5em;display:inline-flex;position:relative}.draft-tooltip{text-align:center;min-width:10em;margin:.5em 1em}.LocalSettingIndicator{display:inline-block;position:relative}.profilesetting-tooltip{text-align:center;min-width:10em;margin:.5em 1em}.ModifiedIndicator{display:inline-block;position:relative}.modified-tooltip{text-align:center;min-width:10em;margin:.5em 1em}.-mobile .ChoiceSetting.setting-item{display:block}.-mobile .ChoiceSetting.setting-item .setting-label{margin-bottom:.5em;display:block}.vertical-tab-switcher{flex-direction:row;display:flex;container-type:inline-size}.vertical-tab-switcher>.tabs{white-space:nowrap;text-overflow:ellipsis;border-right:1px solid;border-right-color:var(--border);box-sizing:border-box;flex-direction:column;flex:0 0 15em;width:15em;min-width:15em;overflow:hidden auto}.vertical-tab-switcher>.tabs>.menu-item{padding:.5em 1em}.vertical-tab-switcher>.tabs>.menu-item .tab-icon{vertical-align:middle;margin-right:.75em}.vertical-tab-switcher>.contents .tab-content-label{box-sizing:border-box;border-bottom:1px solid var(--border);margin:0;display:none}.vertical-tab-switcher>.contents .tab-content-label button{box-sizing:border-box;padding:.5em}.vertical-tab-switcher>.contents .tab-slot-wrapper{grid-template-columns:minmax(1em, 1fr) minmax(-webkit-min-content, 45em) minmax(1em, 1fr);flex-direction:column;flex:auto;grid-template-columns:minmax(1em,1fr) minmax(min-content,45em) minmax(1em,1fr);grid-template-areas:".content.";height:100%;padding:0 1em;display:grid;position:relative;overflow-y:auto}.vertical-tab-switcher>.contents .tab-slot-wrapper.-full-width{padding-left:0;padding-right:0}.vertical-tab-switcher>.contents .tab-slot-wrapper.-full-height{padding-top:0;padding-bottom:0}.vertical-tab-switcher>.contents .tab-slot-wrapper .tab-content{grid-area:content}.vertical-tab-switcher>.contents .tab-slot-wrapper .tab-content.-full-width{grid-column:1/4}.vertical-tab-switcher>.contents .tab-slot-wrapper .tab-content.-full-height{flex-direction:column;height:100%;display:flex}.vertical-tab-switcher>.contents .tab-slot-wrapper .tab-content.-full-height>*{height:100%}.vertical-tab-switcher>.contents .tab-slot-wrapper .tab-content.-full-width.-full-height{position:absolute;inset:0}.vertical-tab-switcher>.contents .tab-content-wrapper{flex-direction:column;flex:auto;height:100%;display:flex}.vertical-tab-switcher>.contents .tab-content-wrapper.-hidden{display:none}.vertical-tab-switcher.-mobile>.contents .tab-content-label{display:block}.vertical-tab-switcher.-mobile.-nav-contents>.contents{flex-grow:1;flex-shrink:1;display:block}.vertical-tab-switcher.-mobile.-nav-contents>.tabs{flex-grow:0;flex-shrink:1;display:none}.vertical-tab-switcher.-mobile.-nav-tabs>.tabs{flex-grow:1;display:block}.vertical-tab-switcher.-mobile.-nav-tabs>.contents{flex-grow:0;flex-shrink:1;display:none}.vertical-tab-switcher.-mobile .tab-slot-wrapper{grid-template-columns:0 minmax(-webkit-min-content, 45em) 0;grid-template-columns:0 minmax(min-content,45em) 0}@supports (container-type:inline-size){.vertical-tab-switcher.-nav-contents>.contents,.vertical-tab-switcher.-nav-tabs>.contents,.vertical-tab-switcher.-mobile.-nav-contents>.contents,.vertical-tab-switcher.-mobile.-nav-tabs>.contents{flex-grow:1;display:block}.vertical-tab-switcher.-nav-contents>.tabs,.vertical-tab-switcher.-nav-tabs>.tabs,.vertical-tab-switcher.-mobile.-nav-contents>.tabs,.vertical-tab-switcher.-mobile.-nav-tabs>.tabs{flex-grow:0;display:block}@container (width<50em){.vertical-tab-switcher>.contents .tab-content-label,.vertical-tab-switcher.-mobile>.contents .tab-content-label{display:block}.vertical-tab-switcher.-nav-contents>.contents,.vertical-tab-switcher.-mobile.-nav-contents>.contents{flex-grow:1;display:block}.vertical-tab-switcher.-nav-contents>.tabs,.vertical-tab-switcher.-mobile.-nav-contents>.tabs{flex-grow:0;flex-shrink:1;display:none}.vertical-tab-switcher.-nav-tabs>.tabs,.vertical-tab-switcher.-mobile.-nav-tabs>.tabs{flex-grow:1;display:block}.vertical-tab-switcher.-nav-tabs>.contents,.vertical-tab-switcher.-mobile.-nav-tabs>.contents{flex-grow:0;flex-shrink:1;display:none}}} diff --git a/priv/static/static/css/who_to_follow_panel.BP23-WSI.css b/priv/static/static/css/who_to_follow_panel.BP23-WSI.css new file mode 100644 index 000000000..7025ee981 --- /dev/null +++ b/priv/static/static/css/who_to_follow_panel.BP23-WSI.css @@ -0,0 +1 @@ +.who-to-follow *{vertical-align:middle}.who-to-follow img{width:32px;height:32px}.who-to-follow{margin:0;padding:0 1em}.who-to-follow-items{white-space:nowrap;text-overflow:ellipsis;margin:1em 0;padding:0;overflow:hidden}.who-to-follow-more{text-align:center;margin:1em 0;padding:0} diff --git a/priv/static/static/js/_plugin-vue_export-helper.By3-ojQa.js b/priv/static/static/js/_plugin-vue_export-helper.By3-ojQa.js new file mode 100644 index 000000000..f60e09dc8 --- /dev/null +++ b/priv/static/static/js/_plugin-vue_export-helper.By3-ojQa.js @@ -0,0 +1,2 @@ +function e(e){let t=Object.create(null);for(let n of e.split(`,`))t[n]=1;return e=>e in t}var t={},n=()=>{},r=Object.assign,i=(e,t)=>{let n=e.indexOf(t);n>-1&&e.splice(n,1)},a=Object.prototype.hasOwnProperty,o=(e,t)=>a.call(e,t),s=Array.isArray,c=e=>h(e)===`[object Map]`,l=e=>h(e)===`[object Set]`,u=e=>typeof e==`function`,d=e=>typeof e==`string`,f=e=>typeof e==`symbol`,p=e=>typeof e==`object`&&!!e,m=Object.prototype.toString,h=e=>m.call(e),g=e=>h(e).slice(8,-1),_=e=>h(e)===`[object Object]`,v=e=>d(e)&&e!==`NaN`&&e[0]!==`-`&&``+parseInt(e,10)===e,y=(e,t)=>!Object.is(e,t),b=(e,t,n,r=!1)=>{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:r,value:n})},x,S=class{constructor(e=!1){this.detached=e,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this.parent=x,!e&&x&&(this.index=(x.scopes||=[]).push(this)-1)}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let e,t;if(this.scopes)for(e=0,t=this.scopes.length;e0&&--this._on===0&&(x=this.prevScope,this.prevScope=void 0)}stop(e){if(this._active){this._active=!1;let t,n;for(t=0,n=this.effects.length;t0)return;if(D){let e=D;for(D=void 0;e;){let t=e.next;e.next=void 0,e.flags&=-9,e=t}}let e;for(;re;){let t=re;for(re=void 0;t;){let n=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(t){e||=t}t=n}}if(e)throw e}function se(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function ce(e){let t,n=e.depsTail,r=n;for(;r;){let e=r.prevDep;r.version===-1?(r===n&&(n=e),de(r),fe(r)):t=r,r.dep.activeLink=r.prevActiveLink,r.prevActiveLink=void 0,r=e}e.deps=t,e.depsTail=n}function le(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(ue(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function ue(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===_e)||(e.globalVersion=_e,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!le(e))))return;e.flags|=2;let t=e.dep,n=E,r=O;E=e,O=!0;try{se(e);let n=e.fn(e._value);(t.version===0||y(n,e._value))&&(e.flags|=128,e._value=n,t.version++)}catch(e){throw t.version++,e}finally{E=n,O=r,ce(e),e.flags&=-3}}function de(e,t=!1){let{dep:n,prevSub:r,nextSub:i}=e;if(r&&(r.nextSub=i,e.prevSub=void 0),i&&(i.prevSub=r,e.nextSub=void 0),n.subs===e&&(n.subs=r,!r&&n.computed)){n.computed.flags&=-5;for(let e=n.computed.deps;e;e=e.nextDep)de(e,!0)}!t&&!--n.sc&&n.map&&n.map.delete(n.key)}function fe(e){let{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}var O=!0,pe=[];function me(){pe.push(O),O=!1}function he(){let e=pe.pop();O=e===void 0?!0:e}function ge(e){let{cleanup:t}=e;if(e.cleanup=void 0,t){let e=E;E=void 0;try{t()}finally{E=e}}}var _e=0,ve=class{constructor(e,t){this.sub=e,this.dep=t,this.version=t.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}},ye=class{constructor(e){this.computed=e,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(e){if(!E||!O||E===this.computed)return;let t=this.activeLink;if(t===void 0||t.sub!==E)t=this.activeLink=new ve(E,this),E.deps?(t.prevDep=E.depsTail,E.depsTail.nextDep=t,E.depsTail=t):E.deps=E.depsTail=t,be(t);else if(t.version===-1&&(t.version=this.version,t.nextDep)){let e=t.nextDep;e.prevDep=t.prevDep,t.prevDep&&(t.prevDep.nextDep=e),t.prevDep=E.depsTail,t.nextDep=void 0,E.depsTail.nextDep=t,E.depsTail=t,E.deps===t&&(E.deps=e)}return t}trigger(e){this.version++,_e++,this.notify(e)}notify(e){ae();try{for(let e=this.subs;e;e=e.prevSub)e.sub.notify()&&e.sub.dep.notify()}finally{oe()}}};function be(e){if(e.dep.sc++,e.sub.flags&4){let t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let e=t.deps;e;e=e.nextDep)be(e)}let n=e.dep.subs;n!==e&&(e.prevSub=n,n&&(n.nextSub=e)),e.dep.subs=e}}var xe=new WeakMap,Se=Symbol(``),Ce=Symbol(``),we=Symbol(``);function k(e,t,n){if(O&&E){let t=xe.get(e);t||xe.set(e,t=new Map);let r=t.get(n);r||(t.set(n,r=new ye),r.map=t,r.key=n),r.track()}}function Te(e,t,n,r,i,a){let o=xe.get(e);if(!o){_e++;return}let l=e=>{e&&e.trigger()};if(ae(),t===`clear`)o.forEach(l);else{let i=s(e),a=i&&v(n);if(i&&n===`length`){let e=Number(r);o.forEach((t,n)=>{(n===`length`||n===we||!f(n)&&n>=e)&&l(t)})}else switch((n!==void 0||o.has(void 0))&&l(o.get(n)),a&&l(o.get(we)),t){case`add`:i?a&&l(o.get(`length`)):(l(o.get(Se)),c(e)&&l(o.get(Ce)));break;case`delete`:i||(l(o.get(Se)),c(e)&&l(o.get(Ce)));break;case`set`:c(e)&&l(o.get(Se));break}}oe()}function Ee(e,t){let n=xe.get(e);return n&&n.get(t)}function De(e){let t=j(e);return t===e?t:(k(t,`iterate`,we),A(e)?t:t.map(M))}function Oe(e){return k(e=j(e),`iterate`,we),e}var ke={__proto__:null,[Symbol.iterator](){return Ae(this,Symbol.iterator,M)},concat(...e){return De(this).concat(...e.map(e=>s(e)?De(e):e))},entries(){return Ae(this,`entries`,e=>(e[1]=M(e[1]),e))},every(e,t){return Me(this,`every`,e,t,void 0,arguments)},filter(e,t){return Me(this,`filter`,e,t,e=>e.map(M),arguments)},find(e,t){return Me(this,`find`,e,t,M,arguments)},findIndex(e,t){return Me(this,`findIndex`,e,t,void 0,arguments)},findLast(e,t){return Me(this,`findLast`,e,t,M,arguments)},findLastIndex(e,t){return Me(this,`findLastIndex`,e,t,void 0,arguments)},forEach(e,t){return Me(this,`forEach`,e,t,void 0,arguments)},includes(...e){return Pe(this,`includes`,e)},indexOf(...e){return Pe(this,`indexOf`,e)},join(e){return De(this).join(e)},lastIndexOf(...e){return Pe(this,`lastIndexOf`,e)},map(e,t){return Me(this,`map`,e,t,void 0,arguments)},pop(){return Fe(this,`pop`)},push(...e){return Fe(this,`push`,e)},reduce(e,...t){return Ne(this,`reduce`,e,t)},reduceRight(e,...t){return Ne(this,`reduceRight`,e,t)},shift(){return Fe(this,`shift`)},some(e,t){return Me(this,`some`,e,t,void 0,arguments)},splice(...e){return Fe(this,`splice`,e)},toReversed(){return De(this).toReversed()},toSorted(e){return De(this).toSorted(e)},toSpliced(...e){return De(this).toSpliced(...e)},unshift(...e){return Fe(this,`unshift`,e)},values(){return Ae(this,`values`,M)}};function Ae(e,t,n){let r=Oe(e),i=r[t]();return r!==e&&!A(e)&&(i._next=i.next,i.next=()=>{let e=i._next();return e.done||(e.value=n(e.value)),e}),i}var je=Array.prototype;function Me(e,t,n,r,i,a){let o=Oe(e),s=o!==e&&!A(e),c=o[t];if(c!==je[t]){let t=c.apply(e,a);return s?M(t):t}let l=n;o!==e&&(s?l=function(t,r){return n.call(this,M(t),r,e)}:n.length>2&&(l=function(t,r){return n.call(this,t,r,e)}));let u=c.call(o,l,r);return s&&i?i(u):u}function Ne(e,t,n,r){let i=Oe(e),a=n;return i!==e&&(A(e)?n.length>3&&(a=function(t,r,i){return n.call(this,t,r,i,e)}):a=function(t,r,i){return n.call(this,t,M(r),i,e)}),i[t](a,...r)}function Pe(e,t,n){let r=j(e);k(r,`iterate`,we);let i=r[t](...n);return(i===-1||i===!1)&&ft(n[0])?(n[0]=j(n[0]),r[t](...n)):i}function Fe(e,t,n=[]){me(),ae();let r=j(e)[t].apply(e,n);return oe(),he(),r}var Ie=e(`__proto__,__v_isRef,__isVue`),Le=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!==`arguments`&&e!==`caller`).map(e=>Symbol[e]).filter(f));function Re(e){f(e)||(e=String(e));let t=j(this);return k(t,`has`,e),t.hasOwnProperty(e)}var ze=class{constructor(e=!1,t=!1){this._isReadonly=e,this._isShallow=t}get(e,t,n){if(t===`__v_skip`)return e.__v_skip;let r=this._isReadonly,i=this._isShallow;if(t===`__v_isReactive`)return!r;if(t===`__v_isReadonly`)return r;if(t===`__v_isShallow`)return i;if(t===`__v_raw`)return n===(r?i?rt:nt:i?tt:et).get(e)||Object.getPrototypeOf(e)===Object.getPrototypeOf(n)?e:void 0;let a=s(e);if(!r){let e;if(a&&(e=ke[t]))return e;if(t===`hasOwnProperty`)return Re}let o=Reflect.get(e,t,N(e)?e:n);if((f(t)?Le.has(t):Ie(t))||(r||k(e,`get`,t),i))return o;if(N(o)){let e=a&&v(t)?o:o.value;return r&&p(e)?ct(e):e}return p(o)?r?ct(o):ot(o):o}},Be=class extends ze{constructor(e=!1){super(!1,e)}set(e,t,n,r){let i=e[t];if(!this._isShallow){let t=dt(i);if(!A(n)&&!dt(n)&&(i=j(i),n=j(n)),!s(e)&&N(i)&&!N(n))return t||(i.value=n),!0}let a=s(e)&&v(t)?Number(t)e,Ke=e=>Reflect.getPrototypeOf(e);function qe(e,t,n){return function(...r){let i=this.__v_raw,a=j(i),o=c(a),s=e===`entries`||e===Symbol.iterator&&o,l=e===`keys`&&o,u=i[e](...r),d=n?Ge:t?mt:M;return!t&&k(a,`iterate`,l?Ce:Se),{next(){let{value:e,done:t}=u.next();return t?{value:e,done:t}:{value:s?[d(e[0]),d(e[1])]:d(e),done:t}},[Symbol.iterator](){return this}}}}function Je(e){return function(...t){return e===`delete`?!1:e===`clear`?void 0:this}}function Ye(e,t){let n={get(n){let r=this.__v_raw,i=j(r),a=j(n);e||(y(n,a)&&k(i,`get`,n),k(i,`get`,a));let{has:o}=Ke(i),s=t?Ge:e?mt:M;if(o.call(i,n))return s(r.get(n));if(o.call(i,a))return s(r.get(a));r!==i&&r.get(n)},get size(){let t=this.__v_raw;return!e&&k(j(t),`iterate`,Se),t.size},has(t){let n=this.__v_raw,r=j(n),i=j(t);return e||(y(t,i)&&k(r,`has`,t),k(r,`has`,i)),t===i?n.has(t):n.has(t)||n.has(i)},forEach(n,r){let i=this,a=i.__v_raw,o=j(a),s=t?Ge:e?mt:M;return!e&&k(o,`iterate`,Se),a.forEach((e,t)=>n.call(r,s(e),s(t),i))}};return r(n,e?{add:Je(`add`),set:Je(`set`),delete:Je(`delete`),clear:Je(`clear`)}:{add(e){!t&&!A(e)&&!dt(e)&&(e=j(e));let n=j(this);return Ke(n).has.call(n,e)||(n.add(e),Te(n,`add`,e,e)),this},set(e,n){!t&&!A(n)&&!dt(n)&&(n=j(n));let r=j(this),{has:i,get:a}=Ke(r),o=i.call(r,e);o||=(e=j(e),i.call(r,e));let s=a.call(r,e);return r.set(e,n),o?y(n,s)&&Te(r,`set`,e,n,s):Te(r,`add`,e,n),this},delete(e){let t=j(this),{has:n,get:r}=Ke(t),i=n.call(t,e);i||=(e=j(e),n.call(t,e));let a=r?r.call(t,e):void 0,o=t.delete(e);return i&&Te(t,`delete`,e,void 0,a),o},clear(){let e=j(this),t=e.size!==0,n=e.clear();return t&&Te(e,`clear`,void 0,void 0,void 0),n}}),[`keys`,`values`,`entries`,Symbol.iterator].forEach(r=>{n[r]=qe(r,e,t)}),n}function Xe(e,t){let n=Ye(e,t);return(t,r,i)=>r===`__v_isReactive`?!e:r===`__v_isReadonly`?e:r===`__v_raw`?t:Reflect.get(o(n,r)&&r in t?n:t,r,i)}var Ze={get:Xe(!1,!1)},Qe={get:Xe(!1,!0)},$e={get:Xe(!0,!1)},et=new WeakMap,tt=new WeakMap,nt=new WeakMap,rt=new WeakMap;function it(e){switch(e){case`Object`:case`Array`:return 1;case`Map`:case`Set`:case`WeakMap`:case`WeakSet`:return 2;default:return 0}}function at(e){return e.__v_skip||!Object.isExtensible(e)?0:it(g(e))}function ot(e){return dt(e)?e:lt(e,!1,He,Ze,et)}function st(e){return lt(e,!1,We,Qe,tt)}function ct(e){return lt(e,!0,Ue,$e,nt)}function lt(e,t,n,r,i){if(!p(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;let a=at(e);if(a===0)return e;let o=i.get(e);if(o)return o;let s=new Proxy(e,a===2?r:n);return i.set(e,s),s}function ut(e){return dt(e)?ut(e.__v_raw):!!(e&&e.__v_isReactive)}function dt(e){return!!(e&&e.__v_isReadonly)}function A(e){return!!(e&&e.__v_isShallow)}function ft(e){return e?!!e.__v_raw:!1}function j(e){let t=e&&e.__v_raw;return t?j(t):e}function pt(e){return!o(e,`__v_skip`)&&Object.isExtensible(e)&&b(e,`__v_skip`,!0),e}var M=e=>p(e)?ot(e):e,mt=e=>p(e)?ct(e):e;function N(e){return e?e.__v_isRef===!0:!1}function ht(e){return _t(e,!1)}function gt(e){return _t(e,!0)}function _t(e,t){return N(e)?e:new vt(e,t)}var vt=class{constructor(e,t){this.dep=new ye,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=t?e:j(e),this._value=t?e:M(e),this.__v_isShallow=t}get value(){return this.dep.track(),this._value}set value(e){let t=this._rawValue,n=this.__v_isShallow||A(e)||dt(e);e=n?e:j(e),y(e,t)&&(this._rawValue=e,this._value=n?e:M(e),this.dep.trigger())}};function yt(e){return N(e)?e.value:e}var bt={get:(e,t,n)=>t===`__v_raw`?e:yt(Reflect.get(e,t,n)),set:(e,t,n,r)=>{let i=e[t];return N(i)&&!N(n)?(i.value=n,!0):Reflect.set(e,t,n,r)}};function xt(e){return ut(e)?e:new Proxy(e,bt)}function St(e){let t=s(e)?Array(e.length):{};for(let n in e)t[n]=wt(e,n);return t}var Ct=class{constructor(e,t,n){this._object=e,this._key=t,this._defaultValue=n,this.__v_isRef=!0,this._value=void 0}get value(){let e=this._object[this._key];return this._value=e===void 0?this._defaultValue:e}set value(e){this._object[this._key]=e}get dep(){return Ee(j(this._object),this._key)}};function wt(e,t,n){let r=e[t];return N(r)?r:new Ct(e,t,n)}var Tt=class{constructor(e,t,n){this.fn=e,this.setter=t,this._value=void 0,this.dep=new ye(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=_e-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!t,this.isSSR=n}notify(){if(this.flags|=16,!(this.flags&8)&&E!==this)return ie(this,!0),!0}get value(){let e=this.dep.track();return ue(this),e&&(e.version=this.dep.version),this._value}set value(e){this.setter&&this.setter(e)}};function Et(e,t,n=!1){let r,i;return u(e)?r=e:(r=e.get,i=e.set),new Tt(r,i,n)}var Dt={},Ot=new WeakMap,kt=void 0;function At(e,t=!1,n=kt){if(n){let t=Ot.get(n);t||Ot.set(n,t=[]),t.push(e)}}function jt(e,r,a=t){let{immediate:o,deep:c,once:l,scheduler:d,augmentJob:f,call:p}=a,m=e=>c?e:A(e)||c===!1||c===0?Mt(e,1):Mt(e),h,g,_,v,b=!1,x=!1;if(N(e)?(g=()=>e.value,b=A(e)):ut(e)?(g=()=>m(e),b=!0):s(e)?(x=!0,b=e.some(e=>ut(e)||A(e)),g=()=>e.map(e=>{if(N(e))return e.value;if(ut(e))return m(e);if(u(e))return p?p(e,2):e()})):g=u(e)?r?p?()=>p(e,2):e:()=>{if(_){me();try{_()}finally{he()}}let t=kt;kt=h;try{return p?p(e,3,[v]):e(v)}finally{kt=t}}:n,r&&c){let e=g,t=c===!0?1/0:c;g=()=>Mt(e(),t)}let S=w(),C=()=>{h.stop(),S&&S.active&&i(S.effects,h)};if(l&&r){let e=r;r=(...t)=>{e(...t),C()}}let T=x?Array(e.length).fill(Dt):Dt,E=e=>{if(!(!(h.flags&1)||!h.dirty&&!e))if(r){let e=h.run();if(c||b||(x?e.some((e,t)=>y(e,T[t])):y(e,T))){_&&_();let t=kt;kt=h;try{let t=[e,T===Dt?void 0:x&&T[0]===Dt?[]:T,v];T=e,p?p(r,3,t):r(...t)}finally{kt=t}}}else h.run()};return f&&f(E),h=new te(g),h.scheduler=d?()=>d(E,!1):E,v=e=>At(e,!1,h),_=h.onStop=()=>{let e=Ot.get(h);if(e){if(p)p(e,4);else for(let t of e)t();Ot.delete(h)}},r?o?E(!0):T=h.run():d?d(E.bind(null,!0),!0):h.run(),C.pause=h.pause.bind(h),C.resume=h.resume.bind(h),C.stop=C,C}function Mt(e,t=1/0,n){if(t<=0||!p(e)||e.__v_skip||(n||=new Map,(n.get(e)||0)>=t))return e;if(n.set(e,t),t--,N(e))Mt(e.value,t,n);else if(s(e))for(let r=0;r{Mt(e,t,n)});else if(_(e)){for(let r in e)Mt(e[r],t,n);for(let r of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,r)&&Mt(e[r],t,n)}return e}function Nt(e){let t=Object.create(null);for(let n of e.split(`,`))t[n]=1;return e=>e in t}var P={},Pt=[],Ft=()=>{},It=()=>!1,Lt=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),Rt=e=>e.startsWith(`onUpdate:`),F=Object.assign,zt=(e,t)=>{let n=e.indexOf(t);n>-1&&e.splice(n,1)},Bt=Object.prototype.hasOwnProperty,I=(e,t)=>Bt.call(e,t),L=Array.isArray,Vt=e=>qt(e)===`[object Map]`,Ht=e=>qt(e)===`[object Set]`,Ut=e=>qt(e)===`[object RegExp]`,R=e=>typeof e==`function`,z=e=>typeof e==`string`,Wt=e=>typeof e==`symbol`,B=e=>typeof e==`object`&&!!e,Gt=e=>(B(e)||R(e))&&R(e.then)&&R(e.catch),Kt=Object.prototype.toString,qt=e=>Kt.call(e),Jt=e=>qt(e)===`[object Object]`,Yt=Nt(`,key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted`),Xt=e=>{let t=Object.create(null);return(n=>t[n]||(t[n]=e(n)))},Zt=/-\w/g,V=Xt(e=>e.replace(Zt,e=>e.slice(1).toUpperCase())),Qt=/\B([A-Z])/g,$t=Xt(e=>e.replace(Qt,`-$1`).toLowerCase()),en=Xt(e=>e.charAt(0).toUpperCase()+e.slice(1)),tn=Xt(e=>e?`on${en(e)}`:``),nn=(e,...t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:r,value:n})},an=e=>{let t=parseFloat(e);return isNaN(t)?e:t},on,sn=()=>on||=typeof globalThis<`u`?globalThis:typeof self<`u`?self:typeof window<`u`?window:typeof global<`u`?global:{};function cn(e){if(L(e)){let t={};for(let n=0;n{if(e){let n=e.split(un);n.length>1&&(t[n[0].trim()]=n[1].trim())}}),t}function pn(e){let t=``;if(z(e))t=e;else if(L(e))for(let n=0;n!!(e&&e.__v_isRef===!0),gn=e=>z(e)?e:e==null?``:L(e)||B(e)&&(e.toString===Kt||!R(e.toString))?hn(e)?gn(e.value):JSON.stringify(e,_n,2):String(e),_n=(e,t)=>hn(t)?_n(e,t.value):Vt(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((e,[t,n],r)=>(e[vn(t,r)+` =>`]=n,e),{})}:Ht(t)?{[`Set(${t.size})`]:[...t.values()].map(e=>vn(e))}:Wt(t)?vn(t):B(t)&&!L(t)&&!Jt(t)?String(t):t,vn=(e,t=``)=>Wt(e)?`Symbol(${e.description??t})`:e;function yn(e,t,n,r){try{return r?e(...r):e()}catch(e){bn(e,t,n)}}function H(e,t,n,r){if(R(e)){let i=yn(e,t,n,r);return i&&Gt(i)&&i.catch(e=>{bn(e,t,n)}),i}if(L(e)){let i=[];for(let a=0;a>>1,i=U[r],a=Fn(i);a=Fn(n)?U.push(e):U.splice(kn(t),0,e),e.flags|=1,jn()}}function jn(){Dn||=En.then(In)}function Mn(e){L(e)?Cn.push(...e):wn&&e.id===-1?wn.splice(Tn+1,0,e):e.flags&1||(Cn.push(e),e.flags|=1),jn()}function Nn(e,t,n=Sn+1){for(;nFn(e)-Fn(t));if(Cn.length=0,wn){wn.push(...e);return}for(wn=e,Tn=0;Tne.id==null?e.flags&2?-1:1/0:e.id;function In(e){try{for(Sn=0;SnHn;function Hn(e,t=W,n){if(!t||e._n)return e;let r=(...n)=>{r._d&&ja(-1);let i=Rn(t),a;try{a=e(...n)}finally{Rn(i),r._d&&ja(1)}return a};return r._n=!0,r._c=!0,r._d=!0,r}function Un(e,t){if(W===null)return e;let n=ho(W),r=e.dirs||=[];for(let e=0;ee.__isTeleport,qn=e=>e&&(e.disabled||e.disabled===``),Jn=e=>e&&(e.defer||e.defer===``),Yn=e=>typeof SVGElement<`u`&&e instanceof SVGElement,Xn=e=>typeof MathMLElement==`function`&&e instanceof MathMLElement,Zn=(e,t)=>{let n=e&&e.to;return z(n)?t?t(n):null:n},Qn={name:`Teleport`,__isTeleport:!0,process(e,t,n,r,i,a,o,s,c,l){let{mc:u,pc:d,pbc:f,o:{insert:p,querySelector:m,createText:h,createComment:g}}=l,_=qn(t.props),{shapeFlag:v,children:y,dynamicChildren:b}=t;if(e==null){let e=t.el=h(``),l=t.anchor=h(``);p(e,n,r),p(l,n,r);let d=(e,t)=>{v&16&&u(y,e,t,i,a,o,s,c)},f=()=>{let e=t.target=Zn(t.props,m),n=rr(e,t,h,p);e&&(o!==`svg`&&Yn(e)?o=`svg`:o!==`mathml`&&Xn(e)&&(o=`mathml`),i&&i.isCE&&(i.ce._teleportTargets||(i.ce._teleportTargets=new Set)).add(e),_||(d(e,n),nr(t,!1)))};_&&(d(n,l),nr(t,!0)),Jn(t.props)?(t.el.__isMounted=!1,q(()=>{f(),delete t.el.__isMounted},a)):f()}else{if(Jn(t.props)&&e.el.__isMounted===!1){q(()=>{Qn.process(e,t,n,r,i,a,o,s,c,l)},a);return}t.el=e.el,t.targetStart=e.targetStart;let u=t.anchor=e.anchor,p=t.target=e.target,h=t.targetAnchor=e.targetAnchor,g=qn(e.props),v=g?n:p,y=g?u:h;if(o===`svg`||Yn(p)?o=`svg`:(o===`mathml`||Xn(p))&&(o=`mathml`),b?(f(e.dynamicChildren,b,v,i,a,o,s),na(e,t,!0)):c||d(e,t,v,y,i,a,o,s,!1),_)g?t.props&&e.props&&t.props.to!==e.props.to&&(t.props.to=e.props.to):$n(t,n,u,l,1);else if((t.props&&t.props.to)!==(e.props&&e.props.to)){let e=t.target=Zn(t.props,m);e&&$n(t,e,null,l,0)}else g&&$n(t,p,h,l,1);nr(t,_)}},remove(e,t,n,{um:r,o:{remove:i}},a){let{shapeFlag:o,children:s,anchor:c,targetStart:l,targetAnchor:u,target:d,props:f}=e;if(d&&(i(l),i(u)),a&&i(c),o&16){let e=a||!qn(f);for(let i=0;i{e.isMounted=!0}),Ur(()=>{e.isUnmounting=!0}),e}var G=[Function,Array],sr={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:G,onEnter:G,onAfterEnter:G,onEnterCancelled:G,onBeforeLeave:G,onLeave:G,onAfterLeave:G,onLeaveCancelled:G,onBeforeAppear:G,onAppear:G,onAfterAppear:G,onAppearCancelled:G},cr=e=>{let t=e.subTree;return t.component?cr(t.component):t},lr={name:`BaseTransition`,props:sr,setup(e,{slots:t}){let n=$a(),r=or();return()=>{let i=t.default&&_r(t.default(),!0);if(!i||!i.length)return;let a=ur(i),o=j(e),{mode:s}=o;if(r.isLeaving)return mr(a);let c=hr(a);if(!c)return mr(a);let l=pr(c,o,r,n,e=>l=e);c.type!==Y&&gr(c,l);let u=n.subTree&&hr(n.subTree);if(u&&u.type!==Y&&!Ia(u,c)&&cr(n).type!==Y){let e=pr(u,o,r,n);if(gr(u,e),s===`out-in`&&c.type!==Y)return r.isLeaving=!0,e.afterLeave=()=>{r.isLeaving=!1,n.job.flags&8||n.update(),delete e.afterLeave,u=void 0},mr(a);s===`in-out`&&c.type!==Y?e.delayLeave=(e,t,n)=>{let i=fr(r,u);i[String(u.key)]=u,e[ir]=()=>{t(),e[ir]=void 0,delete l.delayedLeave,u=void 0},l.delayedLeave=()=>{n(),delete l.delayedLeave,u=void 0}}:u=void 0}else u&&=void 0;return a}}};function ur(e){let t=e[0];if(e.length>1){for(let n of e)if(n.type!==Y){t=n;break}}return t}var dr=lr;function fr(e,t){let{leavingVNodes:n}=e,r=n.get(t.type);return r||(r=Object.create(null),n.set(t.type,r)),r}function pr(e,t,n,r,i){let{appear:a,mode:o,persisted:s=!1,onBeforeEnter:c,onEnter:l,onAfterEnter:u,onEnterCancelled:d,onBeforeLeave:f,onLeave:p,onAfterLeave:m,onLeaveCancelled:h,onBeforeAppear:g,onAppear:_,onAfterAppear:v,onAppearCancelled:y}=t,b=String(e.key),x=fr(n,e),S=(e,t)=>{e&&H(e,r,9,t)},C=(e,t)=>{let n=t[1];S(e,t),L(e)?e.every(e=>e.length<=1)&&n():e.length<=1&&n()},w={mode:o,persisted:s,beforeEnter(t){let r=c;if(!n.isMounted)if(a)r=g||c;else return;t[ir]&&t[ir](!0);let i=x[b];i&&Ia(e,i)&&i.el[ir]&&i.el[ir](),S(r,[t])},enter(e){let t=l,r=u,i=d;if(!n.isMounted)if(a)t=_||l,r=v||u,i=y||d;else return;let o=!1,s=e[ar]=t=>{o||(o=!0,S(t?i:r,[e]),w.delayedLeave&&w.delayedLeave(),e[ar]=void 0)};t?C(t,[e,s]):s()},leave(t,r){let i=String(e.key);if(t[ar]&&t[ar](!0),n.isUnmounting)return r();S(f,[t]);let a=!1,o=t[ir]=n=>{a||(a=!0,r(),S(n?h:m,[t]),t[ir]=void 0,x[i]===e&&delete x[i])};x[i]=e,p?C(p,[t,o]):o()},clone(e){let a=pr(e,t,n,r,i);return i&&i(a),a}};return w}function mr(e){if(Or(e))return e=Ha(e),e.children=null,e}function hr(e){if(!Or(e))return Kn(e.type)&&e.children?ur(e.children):e;if(e.component)return e.component.subTree;let{shapeFlag:t,children:n}=e;if(n){if(t&16)return n[0];if(t&32&&R(n.default))return n.default()}}function gr(e,t){e.shapeFlag&6&&e.component?(e.transition=t,gr(e.component.subTree,t)):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function _r(e,t=!1,n){let r=[],i=0;for(let a=0;a1)for(let e=0;exr(e,t&&(L(t)?t[a]:t),n,r,i));return}if(Tr(r)&&!i){r.shapeFlag&512&&r.type.__asyncResolved&&r.component.subTree.component&&xr(e,t,n,r.component.subTree);return}let a=r.shapeFlag&4?ho(r.component):r.el,o=i?null:a,{i:s,r:c}=e,l=t&&t.r,u=s.refs===P?s.refs={}:s.refs,d=s.setupState,f=j(d),p=d===P?It:e=>I(f,e),m=e=>!0;if(l!=null&&l!==c){if(Sr(t),z(l))u[l]=null,p(l)&&(d[l]=null);else if(N(l)){m(l)&&(l.value=null);let e=t;e.k&&(u[e.k]=null)}}if(R(c))yn(c,s,12,[o,u]);else{let t=z(c),r=N(c);if(t||r){let s=()=>{if(e.f){let n=t?p(c)?d[c]:u[c]:m(c)||!e.k?c.value:u[e.k];if(i)L(n)&&zt(n,a);else if(L(n))n.includes(a)||n.push(a);else if(t)u[c]=[a],p(c)&&(d[c]=u[c]);else{let t=[a];m(c)&&(c.value=t),e.k&&(u[e.k]=t)}}else t?(u[c]=o,p(c)&&(d[c]=o)):r&&(m(c)&&(c.value=o),e.k&&(u[e.k]=o))};if(o){let t=()=>{s(),br.delete(e)};t.id=-1,br.set(e,t),q(t,n)}else Sr(e),s()}}}function Sr(e){let t=br.get(e);t&&(t.flags|=8,br.delete(e))}var Cr=e=>e.nodeType===8;sn().requestIdleCallback,sn().cancelIdleCallback;function wr(e,t){if(Cr(e)&&e.data===`[`){let n=1,r=e.nextSibling;for(;r;){if(r.nodeType===1){if(t(r)===!1)break}else if(Cr(r))if(r.data===`]`){if(--n===0)break}else r.data===`[`&&n++;r=r.nextSibling}}else t(e)}var Tr=e=>!!e.type.__asyncLoader;function Er(e){R(e)&&(e={loader:e});let{loader:t,loadingComponent:n,errorComponent:r,delay:i=200,hydrate:a,timeout:o,suspensible:s=!0,onError:c}=e,l=null,u,d=0,f=()=>(d++,l=null,p()),p=()=>{let e;return l||(e=l=t().catch(e=>{if(e=e instanceof Error?e:Error(String(e)),c)return new Promise((t,n)=>{c(e,()=>t(f()),()=>n(e),d+1)});throw e}).then(t=>e!==l&&l?l:(t&&(t.__esModule||t[Symbol.toStringTag]===`Module`)&&(t=t.default),u=t,t)))};return vr({name:`AsyncComponentWrapper`,__asyncLoader:p,__asyncHydrate(e,t,n){let r=!1;(t.bu||=[]).push(()=>r=!0);let i=()=>{r||n()},o=a?()=>{let n=a(i,t=>wr(e,t));n&&(t.bum||=[]).push(n)}:i;u?o():p().then(()=>!t.isUnmounted&&o())},get __asyncResolved(){return u},setup(){let e=$;if(yr(e),u)return()=>Dr(u,e);let t=t=>{l=null,bn(t,e,13,!r)};if(s&&e.suspense||ao)return p().then(t=>()=>Dr(t,e)).catch(e=>(t(e),()=>r?Z(r,{error:e}):null));let a=ht(!1),c=ht(),d=ht(!!i);return i&&setTimeout(()=>{d.value=!1},i),o!=null&&setTimeout(()=>{if(!a.value&&!c.value){let e=Error(`Async component timed out after ${o}ms.`);t(e),c.value=e}},o),p().then(()=>{a.value=!0,e.parent&&Or(e.parent.vnode)&&e.parent.update()}).catch(e=>{t(e),c.value=e}),()=>{if(a.value&&u)return Dr(u,e);if(c.value&&r)return Z(r,{error:c.value});if(n&&!d.value)return Z(n)}}})}function Dr(e,t){let{ref:n,props:r,children:i,ce:a}=t.vnode,o=Z(e,r,i);return o.ref=n,o.ce=a,delete t.vnode.ce,o}var Or=e=>e.type.__isKeepAlive,kr={name:`KeepAlive`,__isKeepAlive:!0,props:{include:[String,RegExp,Array],exclude:[String,RegExp,Array],max:[String,Number]},setup(e,{slots:t}){let n=$a(),r=n.ctx;if(!r.renderer)return()=>{let e=t.default&&t.default();return e&&e.length===1?e[0]:e};let i=new Map,a=new Set,o=null,s=n.suspense,{renderer:{p:c,m:l,um:u,o:{createElement:d}}}=r,f=d(`div`);r.activate=(e,t,n,r,i)=>{let a=e.component;l(e,t,n,0,s),c(a.vnode,e,t,n,a,s,r,e.slotScopeIds,i),q(()=>{a.isDeactivated=!1,a.a&&nn(a.a);let t=e.props&&e.props.onVnodeMounted;t&&Q(t,a.parent,e)},s)},r.deactivate=e=>{let t=e.component;aa(t.m),aa(t.a),l(e,f,null,1,s),q(()=>{t.da&&nn(t.da);let n=e.props&&e.props.onVnodeUnmounted;n&&Q(n,t.parent,e),t.isDeactivated=!0},s)};function p(e){Fr(e),u(e,n,s,!0)}function m(e){i.forEach((t,n)=>{let r=go(t.type);r&&!e(r)&&h(n)})}function h(e){let t=i.get(e);t&&(!o||!Ia(t,o))?p(t):o&&Fr(o),i.delete(e),a.delete(e)}ca(()=>[e.include,e.exclude],([e,t])=>{e&&m(t=>Ar(e,t)),t&&m(e=>!Ar(t,e))},{flush:`post`,deep:!0});let g=null,_=()=>{g!=null&&(Ca(n.subTree.type)?q(()=>{i.set(g,Ir(n.subTree))},n.subTree.suspense):i.set(g,Ir(n.subTree)))};return Br(_),Hr(_),Ur(()=>{i.forEach(e=>{let{subTree:t,suspense:r}=n,i=Ir(t);if(e.type===i.type&&e.key===i.key){Fr(i);let e=i.component.da;e&&q(e,r);return}p(e)})}),()=>{if(g=null,!t.default)return o=null;let n=t.default(),r=n[0];if(n.length>1)return o=null,n;if(!Fa(r)||!(r.shapeFlag&4)&&!(r.shapeFlag&128))return o=null,r;let s=Ir(r);if(s.type===Y)return o=null,s;let c=s.type,l=go(Tr(s)?s.type.__asyncResolved||{}:c),{include:u,exclude:d,max:f}=e;if(u&&(!l||!Ar(u,l))||d&&l&&Ar(d,l))return s.shapeFlag&=-257,o=s,r;let p=s.key==null?c:s.key,m=i.get(p);return s.el&&(s=Ha(s),r.shapeFlag&128&&(r.ssContent=s)),g=p,m?(s.el=m.el,s.component=m.component,s.transition&&gr(s,s.transition),s.shapeFlag|=512,a.delete(p),a.add(p)):(a.add(p),f&&a.size>parseInt(f,10)&&h(a.values().next().value)),s.shapeFlag|=256,o=s,Ca(r.type)?r:s}}};function Ar(e,t){return L(e)?e.some(e=>Ar(e,t)):z(e)?e.split(`,`).includes(t):Ut(e)?(e.lastIndex=0,e.test(t)):!1}function jr(e,t){Nr(e,`a`,t)}function Mr(e,t){Nr(e,`da`,t)}function Nr(e,t,n=$){let r=e.__wdc||=()=>{let t=n;for(;t;){if(t.isDeactivated)return;t=t.parent}return e()};if(Lr(t,r,n),n){let e=n.parent;for(;e&&e.parent;)Or(e.parent.vnode)&&Pr(r,t,n,e),e=e.parent}}function Pr(e,t,n,r){let i=Lr(t,e,r,!0);Wr(()=>{zt(r[t],i)},n)}function Fr(e){e.shapeFlag&=-257,e.shapeFlag&=-513}function Ir(e){return e.shapeFlag&128?e.ssContent:e}function Lr(e,t,n=$,r=!1){if(n){let i=n[e]||(n[e]=[]),a=t.__weh||=(...r)=>{me();let i=no(n),a=H(t,n,e,r);return i(),he(),a};return r?i.unshift(a):i.push(a),a}}var Rr=e=>(t,n=$)=>{(!ao||e===`sp`)&&Lr(e,(...e)=>t(...e),n)},zr=Rr(`bm`),Br=Rr(`m`),Vr=Rr(`bu`),Hr=Rr(`u`),Ur=Rr(`bum`),Wr=Rr(`um`),Gr=Rr(`sp`),Kr=Rr(`rtg`),qr=Rr(`rtc`);function Jr(e,t=$){Lr(`ec`,e,t)}var Yr=`components`,Xr=`directives`;function Zr(e,t){return ti(Yr,e,!0,t)||e}var Qr=Symbol.for(`v-ndc`);function $r(e){return z(e)?ti(Yr,e,!1)||e:e||Qr}function ei(e){return ti(Xr,e)}function ti(e,t,n=!0,r=!1){let i=W||$;if(i){let n=i.type;if(e===Yr){let e=go(n,!1);if(e&&(e===t||e===V(t)||e===en(V(t))))return n}let a=ni(i[e]||n[e],t)||ni(i.appContext[e],t);return!a&&r?n:a}}function ni(e,t){return e&&(e[t]||e[V(t)]||e[en(V(t))])}function ri(e,t,n,r){let i,a=n&&n[r],o=L(e);if(o||z(e)){let n=o&&ut(e),r=!1,s=!1;n&&(r=!A(e),s=dt(e),e=Oe(e)),i=Array(e.length);for(let n=0,o=e.length;nt(e,n,void 0,a&&a[n]));else{let n=Object.keys(e);i=Array(n.length);for(let r=0,o=n.length;r0;return t!=="default"&&(n.name=t),Oa(),Pa(J,null,[Z(`slot`,n,r&&r())],e?-2:64)}let a=e[t];a&&a._c&&(a._d=!1),Oa();let o=a&&ai(a(n)),s=n.key||o&&o.key,c=Pa(J,{key:(s&&!Wt(s)?s:`_${t}`)+(!o&&r?`_fb`:``)},o||(r?r():[]),o&&e._===1?64:-2);return!i&&c.scopeId&&(c.slotScopeIds=[c.scopeId+`-s`]),a&&a._c&&(a._d=!0),c}function ai(e){return e.some(e=>Fa(e)?!(e.type===Y||e.type===J&&!ai(e.children)):!0)?e:null}function oi(e,t){let n={};for(let r in e)n[t&&/[A-Z]/.test(r)?`on:${r}`:tn(r)]=e[r];return n}var si=e=>e?io(e)?ho(e):si(e.parent):null,ci=F(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>si(e.parent),$root:e=>si(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>_i(e),$forceUpdate:e=>e.f||=()=>{An(e.update)},$nextTick:e=>e.n||=On.bind(e.proxy),$watch:e=>ua.bind(e)}),li=(e,t)=>e!==P&&!e.__isScriptSetup&&I(e,t),ui={get({_:e},t){if(t===`__v_skip`)return!0;let{ctx:n,setupState:r,data:i,props:a,accessCache:o,type:s,appContext:c}=e,l;if(t[0]!==`$`){let s=o[t];if(s!==void 0)switch(s){case 1:return r[t];case 2:return i[t];case 4:return n[t];case 3:return a[t]}else if(li(r,t))return o[t]=1,r[t];else if(i!==P&&I(i,t))return o[t]=2,i[t];else if((l=e.propsOptions[0])&&I(l,t))return o[t]=3,a[t];else if(n!==P&&I(n,t))return o[t]=4,n[t];else fi&&(o[t]=0)}let u=ci[t],d,f;if(u)return t===`$attrs`&&k(e.attrs,`get`,``),u(e);if((d=s.__cssModules)&&(d=d[t]))return d;if(n!==P&&I(n,t))return o[t]=4,n[t];if(f=c.config.globalProperties,I(f,t))return f[t]},set({_:e},t,n){let{data:r,setupState:i,ctx:a}=e;return li(i,t)?(i[t]=n,!0):r!==P&&I(r,t)?(r[t]=n,!0):I(e.props,t)||t[0]===`$`&&t.slice(1)in e?!1:(a[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:r,appContext:i,propsOptions:a,type:o}},s){let c,l;return!!(n[s]||e!==P&&s[0]!==`$`&&I(e,s)||li(t,s)||(c=a[0])&&I(c,s)||I(r,s)||I(ci,s)||I(i.config.globalProperties,s)||(l=o.__cssModules)&&l[s])},defineProperty(e,t,n){return n.get==null?I(n,`value`)&&this.set(e,t,n.value,null):e._.accessCache[t]=0,Reflect.defineProperty(e,t,n)}};function di(e){return L(e)?e.reduce((e,t)=>(e[t]=null,e),{}):e}var fi=!0;function pi(e){let t=_i(e),n=e.proxy,r=e.ctx;fi=!1,t.beforeCreate&&hi(t.beforeCreate,e,`bc`);let{data:i,computed:a,methods:o,watch:s,provide:c,inject:l,created:u,beforeMount:d,mounted:f,beforeUpdate:p,updated:m,activated:h,deactivated:g,beforeDestroy:_,beforeUnmount:v,destroyed:y,unmounted:b,render:x,renderTracked:S,renderTriggered:C,errorCaptured:w,serverPrefetch:T,expose:E,inheritAttrs:ee,components:te,directives:ne,filters:re}=t;if(l&&mi(l,r,null),o)for(let e in o){let t=o[e];R(t)&&(r[e]=t.bind(n))}if(i){let t=i.call(n,n);B(t)&&(e.data=ot(t))}if(fi=!0,a)for(let e in a){let t=a[e],i=vo({get:R(t)?t.bind(n,n):R(t.get)?t.get.bind(n,n):Ft,set:!R(t)&&R(t.set)?t.set.bind(n):Ft});Object.defineProperty(r,e,{enumerable:!0,configurable:!0,get:()=>i.value,set:e=>i.value=e})}if(s)for(let e in s)gi(s[e],r,n,e);if(c){let e=R(c)?c.call(n):c;Reflect.ownKeys(e).forEach(t=>{Ai(t,e[t])})}u&&hi(u,e,`c`);function D(e,t){L(t)?t.forEach(t=>e(t.bind(n))):t&&e(t.bind(n))}if(D(zr,d),D(Br,f),D(Vr,p),D(Hr,m),D(jr,h),D(Mr,g),D(Jr,w),D(qr,S),D(Kr,C),D(Ur,v),D(Wr,b),D(Gr,T),L(E))if(E.length){let t=e.exposed||={};E.forEach(e=>{Object.defineProperty(t,e,{get:()=>n[e],set:t=>n[e]=t,enumerable:!0})})}else e.exposed||={};x&&e.render===Ft&&(e.render=x),ee!=null&&(e.inheritAttrs=ee),te&&(e.components=te),ne&&(e.directives=ne),T&&yr(e)}function mi(e,t,n=Ft){L(e)&&(e=Si(e));for(let n in e){let r=e[n],i;i=B(r)?`default`in r?ji(r.from||n,r.default,!0):ji(r.from||n):ji(r),N(i)?Object.defineProperty(t,n,{enumerable:!0,configurable:!0,get:()=>i.value,set:e=>i.value=e}):t[n]=i}}function hi(e,t,n){H(L(e)?e.map(e=>e.bind(t.proxy)):e.bind(t.proxy),t,n)}function gi(e,t,n,r){let i=r.includes(`.`)?da(n,r):()=>n[r];if(z(e)){let n=t[e];R(n)&&ca(i,n)}else if(R(e))ca(i,e.bind(n));else if(B(e))if(L(e))e.forEach(e=>gi(e,t,n,r));else{let r=R(e.handler)?e.handler.bind(n):t[e.handler];R(r)&&ca(i,r,e)}}function _i(e){let t=e.type,{mixins:n,extends:r}=t,{mixins:i,optionsCache:a,config:{optionMergeStrategies:o}}=e.appContext,s=a.get(t),c;return s?c=s:!i.length&&!n&&!r?c=t:(c={},i.length&&i.forEach(e=>vi(c,e,o,!0)),vi(c,t,o)),B(t)&&a.set(t,c),c}function vi(e,t,n,r=!1){let{mixins:i,extends:a}=t;a&&vi(e,a,n,!0),i&&i.forEach(t=>vi(e,t,n,!0));for(let i in t)if(!(r&&i===`expose`)){let r=yi[i]||n&&n[i];e[i]=r?r(e[i],t[i]):t[i]}return e}var yi={data:bi,props:wi,emits:wi,methods:Ci,computed:Ci,beforeCreate:K,created:K,beforeMount:K,mounted:K,beforeUpdate:K,updated:K,beforeDestroy:K,beforeUnmount:K,destroyed:K,unmounted:K,activated:K,deactivated:K,errorCaptured:K,serverPrefetch:K,components:Ci,directives:Ci,watch:Ti,provide:bi,inject:xi};function bi(e,t){return t?e?function(){return F(R(e)?e.call(this,this):e,R(t)?t.call(this,this):t)}:t:e}function xi(e,t){return Ci(Si(e),Si(t))}function Si(e){if(L(e)){let t={};for(let n=0;n1)return n&&R(t)?t.call(r&&r.proxy):t}}function Mi(){return!!($a()||ki)}var Ni={},Pi=()=>Object.create(Ni),Fi=e=>Object.getPrototypeOf(e)===Ni;function Ii(e,t,n,r=!1){let i={},a=Pi();e.propsDefaults=Object.create(null),Ri(e,t,i,a);for(let t in e.propsOptions[0])t in i||(i[t]=void 0);n?e.props=r?i:st(i):e.type.props?e.props=i:e.props=a,e.attrs=a}function Li(e,t,n,r){let{props:i,attrs:a,vnode:{patchFlag:o}}=e,s=j(i),[c]=e.propsOptions,l=!1;if((r||o>0)&&!(o&16)){if(o&8){let n=e.vnode.dynamicProps;for(let r=0;r{c=!0;let[n,r]=Vi(e,t,!0);F(o,n),r&&s.push(...r)};!n&&t.mixins.length&&t.mixins.forEach(r),e.extends&&r(e.extends),e.mixins&&e.mixins.forEach(r)}if(!a&&!c)return B(e)&&r.set(e,Pt),Pt;if(L(a))for(let e=0;ee===`_`||e===`_ctx`||e===`$stable`,Wi=e=>L(e)?e.map(Ka):[Ka(e)],Gi=(e,t,n)=>{if(t._n)return t;let r=Hn((...e)=>Wi(t(...e)),n);return r._c=!1,r},Ki=(e,t,n)=>{let r=e._ctx;for(let n in e){if(Ui(n))continue;let i=e[n];if(R(i))t[n]=Gi(n,i,r);else if(i!=null){let e=Wi(i);t[n]=()=>e}}},qi=(e,t)=>{let n=Wi(t);e.slots.default=()=>n},Ji=(e,t,n)=>{for(let r in t)(n||!Ui(r))&&(e[r]=t[r])},Yi=(e,t,n)=>{let r=e.slots=Pi();if(e.vnode.shapeFlag&32){let e=t._;e?(Ji(r,t,n),n&&rn(r,`_`,e,!0)):Ki(t,r)}else t&&qi(e,t)},Xi=(e,t,n)=>{let{vnode:r,slots:i}=e,a=!0,o=P;if(r.shapeFlag&32){let e=t._;e?n&&e===1?a=!1:Ji(i,t,n):(a=!t.$stable,Ki(t,i)),o=t}else t&&(qi(e,t),o={default:1});if(a)for(let e in i)!Ui(e)&&o[e]==null&&delete i[e]},q=wa;function Zi(e){return Qi(e)}function Qi(e,t){let n=sn();n.__VUE__=!0;let{insert:r,remove:i,patchProp:a,createElement:o,createText:s,createComment:c,setText:l,setElementText:u,parentNode:d,nextSibling:f,setScopeId:p=Ft,insertStaticContent:m}=e,h=(e,t,n,r=null,i=null,a=null,o=void 0,s=null,c=!!t.dynamicChildren)=>{if(e===t)return;e&&!Ia(e,t)&&(r=_e(e),de(e,i,a,!0),e=null),t.patchFlag===-2&&(c=!1,t.dynamicChildren=null);let{type:l,ref:u,shapeFlag:d}=t;switch(l){case Ta:g(e,t,n,r);break;case Y:_(e,t,n,r);break;case Ea:e??v(t,n,r,o);break;case J:ne(e,t,n,r,i,a,o,s,c);break;default:d&1?x(e,t,n,r,i,a,o,s,c):d&6?re(e,t,n,r,i,a,o,s,c):(d&64||d&128)&&l.process(e,t,n,r,i,a,o,s,c,be)}u!=null&&i?xr(u,e&&e.ref,a,t||e,!t):u==null&&e&&e.ref!=null&&xr(e.ref,null,a,e,!0)},g=(e,t,n,i)=>{if(e==null)r(t.el=s(t.children),n,i);else{let n=t.el=e.el;t.children!==e.children&&l(n,t.children)}},_=(e,t,n,i)=>{e==null?r(t.el=c(t.children||``),n,i):t.el=e.el},v=(e,t,n,r)=>{[e.el,e.anchor]=m(e.children,t,n,r,e.el,e.anchor)},y=({el:e,anchor:t},n,i)=>{let a;for(;e&&e!==t;)a=f(e),r(e,n,i),e=a;r(t,n,i)},b=({el:e,anchor:t})=>{let n;for(;e&&e!==t;)n=f(e),i(e),e=n;i(t)},x=(e,t,n,r,i,a,o,s,c)=>{t.type===`svg`?o=`svg`:t.type===`math`&&(o=`mathml`),e==null?S(t,n,r,i,a,o,s,c):T(e,t,i,a,o,s,c)},S=(e,t,n,i,s,c,l,d)=>{let f,p,{props:m,shapeFlag:h,transition:g,dirs:_}=e;if(f=e.el=o(e.type,c,m&&m.is,m),h&8?u(f,e.children):h&16&&w(e.children,f,null,i,s,$i(e,c),l,d),_&&Wn(e,null,i,`created`),C(f,e,e.scopeId,l,i),m){for(let e in m)e!==`value`&&!Yt(e)&&a(f,e,null,m[e],c,i);`value`in m&&a(f,`value`,null,m.value,c),(p=m.onVnodeBeforeMount)&&Q(p,i,e)}_&&Wn(e,null,i,`beforeMount`);let v=ta(s,g);v&&g.beforeEnter(f),r(f,t,n),((p=m&&m.onVnodeMounted)||v||_)&&q(()=>{p&&Q(p,i,e),v&&g.enter(f),_&&Wn(e,null,i,`mounted`)},s)},C=(e,t,n,r,i)=>{if(n&&p(e,n),r)for(let t=0;t{for(let l=c;l{let c=t.el=e.el,{patchFlag:l,dynamicChildren:d,dirs:f}=t;l|=e.patchFlag&16;let p=e.props||P,m=t.props||P,h;if(n&&ea(n,!1),(h=m.onVnodeBeforeUpdate)&&Q(h,n,t,e),f&&Wn(t,e,n,`beforeUpdate`),n&&ea(n,!0),(p.innerHTML&&m.innerHTML==null||p.textContent&&m.textContent==null)&&u(c,``),d?E(e.dynamicChildren,d,c,n,r,$i(t,i),o):s||se(e,t,c,null,n,r,$i(t,i),o,!1),l>0){if(l&16)ee(c,p,m,n,i);else if(l&2&&p.class!==m.class&&a(c,`class`,null,m.class,i),l&4&&a(c,`style`,p.style,m.style,i),l&8){let e=t.dynamicProps;for(let t=0;t{h&&Q(h,n,t,e),f&&Wn(t,e,n,`updated`)},r)},E=(e,t,n,r,i,a,o)=>{for(let s=0;s{if(t!==n){if(t!==P)for(let o in t)!Yt(o)&&!(o in n)&&a(e,o,t[o],null,i,r);for(let o in n){if(Yt(o))continue;let s=n[o],c=t[o];s!==c&&o!==`value`&&a(e,o,c,s,i,r)}`value`in n&&a(e,`value`,t.value,n.value,i)}},ne=(e,t,n,i,a,o,c,l,u)=>{let d=t.el=e?e.el:s(``),f=t.anchor=e?e.anchor:s(``),{patchFlag:p,dynamicChildren:m,slotScopeIds:h}=t;h&&(l=l?l.concat(h):h),e==null?(r(d,n,i),r(f,n,i),w(t.children||[],n,f,a,o,c,l,u)):p>0&&p&64&&m&&e.dynamicChildren?(E(e.dynamicChildren,m,n,a,o,c,l),(t.key!=null||a&&t===a.subTree)&&na(e,t,!0)):se(e,t,n,f,a,o,c,l,u)},re=(e,t,n,r,i,a,o,s,c)=>{t.slotScopeIds=s,e==null?t.shapeFlag&512?i.ctx.activate(t,n,r,o,c):D(t,n,r,i,a,o,c):ie(e,t,c)},D=(e,t,n,r,i,a,o)=>{let s=e.component=Qa(e,r,i);if(Or(e)&&(s.ctx.renderer=be),oo(s,!1,o),s.asyncDep){if(i&&i.registerDep(s,ae,o),!e.el){let r=s.subTree=Z(Y);_(null,r,t,n),e.placeholder=r.el}}else ae(s,e,t,n,i,a,o)},ie=(e,t,n)=>{let r=t.component=e.component;if(ba(e,t,n))if(r.asyncDep&&!r.asyncResolved){oe(r,t,n);return}else r.next=t,r.update();else t.el=e.el,r.vnode=t},ae=(e,t,n,r,i,a,o)=>{let s=()=>{if(e.isMounted){let{next:t,bu:n,u:r,parent:c,vnode:l}=e;{let n=ia(e);if(n){t&&(t.el=l.el,oe(e,t,o)),n.asyncDep.then(()=>{e.isUnmounted||s()});return}}let u=t,f;ea(e,!1),t?(t.el=l.el,oe(e,t,o)):t=l,n&&nn(n),(f=t.props&&t.props.onVnodeBeforeUpdate)&&Q(f,c,t,l),ea(e,!0);let p=_a(e),m=e.subTree;e.subTree=p,h(m,p,d(m.el),_e(m),e,i,a),t.el=p.el,u===null&&Sa(e,p.el),r&&q(r,i),(f=t.props&&t.props.onVnodeUpdated)&&q(()=>Q(f,c,t,l),i)}else{let o,{el:s,props:c}=t,{bm:l,m:u,parent:d,root:f,type:p}=e,m=Tr(t);if(ea(e,!1),l&&nn(l),!m&&(o=c&&c.onVnodeBeforeMount)&&Q(o,d,t),ea(e,!0),s&&Se){let t=()=>{e.subTree=_a(e),Se(s,e.subTree,e,i,null)};m&&p.__asyncHydrate?p.__asyncHydrate(s,e,t):t()}else{f.ce&&f.ce._def.shadowRoot!==!1&&f.ce._injectChildStyle(p);let o=e.subTree=_a(e);h(null,o,n,r,e,i,a),t.el=o.el}if(u&&q(u,i),!m&&(o=c&&c.onVnodeMounted)){let e=t;q(()=>Q(o,d,e),i)}(t.shapeFlag&256||d&&Tr(d.vnode)&&d.vnode.shapeFlag&256)&&e.a&&q(e.a,i),e.isMounted=!0,t=n=r=null}};e.scope.on();let c=e.effect=new te(s);e.scope.off();let l=e.update=c.run.bind(c),u=e.job=c.runIfDirty.bind(c);u.i=e,u.id=e.uid,c.scheduler=()=>An(u),ea(e,!0),l()},oe=(e,t,n)=>{t.component=e;let r=e.vnode.props;e.vnode=t,e.next=null,Li(e,t.props,r,n),Xi(e,t.children,n),me(),Nn(e),he()},se=(e,t,n,r,i,a,o,s,c=!1)=>{let l=e&&e.children,d=e?e.shapeFlag:0,f=t.children,{patchFlag:p,shapeFlag:m}=t;if(p>0){if(p&128){le(l,f,n,r,i,a,o,s,c);return}else if(p&256){ce(l,f,n,r,i,a,o,s,c);return}}m&8?(d&16&&ge(l,i,a),f!==l&&u(n,f)):d&16?m&16?le(l,f,n,r,i,a,o,s,c):ge(l,i,a,!0):(d&8&&u(n,``),m&16&&w(f,n,r,i,a,o,s,c))},ce=(e,t,n,r,i,a,o,s,c)=>{e||=Pt,t||=Pt;let l=e.length,u=t.length,d=Math.min(l,u),f;for(f=0;fu?ge(e,i,a,!0,!1,d):w(t,n,r,i,a,o,s,c,d)},le=(e,t,n,r,i,a,o,s,c)=>{let l=0,u=t.length,d=e.length-1,f=u-1;for(;l<=d&&l<=f;){let r=e[l],u=t[l]=c?qa(t[l]):Ka(t[l]);if(Ia(r,u))h(r,u,n,null,i,a,o,s,c);else break;l++}for(;l<=d&&l<=f;){let r=e[d],l=t[f]=c?qa(t[f]):Ka(t[f]);if(Ia(r,l))h(r,l,n,null,i,a,o,s,c);else break;d--,f--}if(l>d){if(l<=f){let e=f+1,d=ef)for(;l<=d;)de(e[l],i,a,!0),l++;else{let p=l,m=l,g=new Map;for(l=m;l<=f;l++){let e=t[l]=c?qa(t[l]):Ka(t[l]);e.key!=null&&g.set(e.key,l)}let _,v=0,y=f-m+1,b=!1,x=0,S=Array(y);for(l=0;l=y){de(r,i,a,!0);continue}let u;if(r.key!=null)u=g.get(r.key);else for(_=m;_<=f;_++)if(S[_-m]===0&&Ia(r,t[_])){u=_;break}u===void 0?de(r,i,a,!0):(S[u-m]=l+1,u>=x?x=u:b=!0,h(r,t[u],n,null,i,a,o,s,c),v++)}let C=b?ra(S):Pt;for(_=C.length-1,l=y-1;l>=0;l--){let e=m+l,d=t[e],f=t[e+1],p=e+1{let{el:s,type:c,transition:l,children:u,shapeFlag:d}=e;if(d&6){ue(e.component.subTree,t,n,a);return}if(d&128){e.suspense.move(t,n,a);return}if(d&64){c.move(e,t,n,be);return}if(c===J){r(s,t,n);for(let e=0;el.enter(s),o);else{let{leave:a,delayLeave:o,afterLeave:c}=l,u=()=>{e.ctx.isUnmounted?i(s):r(s,t,n)},d=()=>{s._isLeaving&&s[ir](!0),a(s,()=>{u(),c&&c()})};o?o(s,u,d):d()}else r(s,t,n)},de=(e,t,n,r=!1,i=!1)=>{let{type:a,props:o,ref:s,children:c,dynamicChildren:l,shapeFlag:u,patchFlag:d,dirs:f,cacheIndex:p}=e;if(d===-2&&(i=!1),s!=null&&(me(),xr(s,null,n,e,!0),he()),p!=null&&(t.renderCache[p]=void 0),u&256){t.ctx.deactivate(e);return}let m=u&1&&f,h=!Tr(e),g;if(h&&(g=o&&o.onVnodeBeforeUnmount)&&Q(g,t,e),u&6)pe(e.component,n,r);else{if(u&128){e.suspense.unmount(n,r);return}m&&Wn(e,null,t,`beforeUnmount`),u&64?e.type.remove(e,t,n,be,r):l&&!l.hasOnce&&(a!==J||d>0&&d&64)?ge(l,t,n,!1,!0):(a===J&&d&384||!i&&u&16)&&ge(c,t,n),r&&fe(e)}(h&&(g=o&&o.onVnodeUnmounted)||m)&&q(()=>{g&&Q(g,t,e),m&&Wn(e,null,t,`unmounted`)},n)},fe=e=>{let{type:t,el:n,anchor:r,transition:a}=e;if(t===J){O(n,r);return}if(t===Ea){b(e);return}let o=()=>{i(n),a&&!a.persisted&&a.afterLeave&&a.afterLeave()};if(e.shapeFlag&1&&a&&!a.persisted){let{leave:t,delayLeave:r}=a,i=()=>t(n,o);r?r(e.el,o,i):i()}else o()},O=(e,t)=>{let n;for(;e!==t;)n=f(e),i(e),e=n;i(t)},pe=(e,t,n)=>{let{bum:r,scope:i,job:a,subTree:o,um:s,m:c,a:l}=e;aa(c),aa(l),r&&nn(r),i.stop(),a&&(a.flags|=8,de(o,e,t,n)),s&&q(s,t),q(()=>{e.isUnmounted=!0},t)},ge=(e,t,n,r=!1,i=!1,a=0)=>{for(let o=a;o{if(e.shapeFlag&6)return _e(e.component.subTree);if(e.shapeFlag&128)return e.suspense.next();let t=f(e.anchor||e.el),n=t&&t[Gn];return n?f(n):t},ve=!1,ye=(e,t,n)=>{e==null?t._vnode&&de(t._vnode,null,null,!0):h(t._vnode||null,e,t,null,null,null,n),t._vnode=e,ve||=(ve=!0,Nn(),Pn(),!1)},be={p:h,um:de,m:ue,r:fe,mt:D,mc:w,pc:se,pbc:E,n:_e,o:e},xe,Se;return t&&([xe,Se]=t(be)),{render:ye,hydrate:xe,createApp:Oi(ye,xe)}}function $i({type:e,props:t},n){return n===`svg`&&e===`foreignObject`||n===`mathml`&&e===`annotation-xml`&&t&&t.encoding&&t.encoding.includes(`html`)?void 0:n}function ea({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function ta(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function na(e,t,n=!1){let r=e.children,i=t.children;if(L(r)&&L(i))for(let e=0;e>1,e[n[s]]0&&(t[r]=n[a-1]),n[a]=r)}}for(a=n.length,o=n[a-1];a-- >0;)n[a]=o,o=t[o];return n}function ia(e){let t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:ia(t)}function aa(e){if(e)for(let t=0;tji(oa);function ca(e,t,n){return la(e,t,n)}function la(e,t,n=P){let{immediate:r,deep:i,flush:a,once:o}=n,s=F({},n),c=t&&r||!t&&a!==`post`,l;if(ao){if(a===`sync`){let e=sa();l=e.__watcherHandles||=[]}else if(!c){let e=()=>{};return e.stop=Ft,e.resume=Ft,e.pause=Ft,e}}let u=$;s.call=(e,t,n)=>H(e,u,t,n);let d=!1;a===`post`?s.scheduler=e=>{q(e,u&&u.suspense)}:a!==`sync`&&(d=!0,s.scheduler=(e,t)=>{t?e():An(e)}),s.augmentJob=e=>{t&&(e.flags|=4),d&&(e.flags|=2,u&&(e.id=u.uid,e.i=u))};let f=jt(e,t,s);return ao&&(l?l.push(f):c&&f()),f}function ua(e,t,n){let r=this.proxy,i=z(e)?e.includes(`.`)?da(r,e):()=>r[e]:e.bind(r,r),a;R(t)?a=t:(a=t.handler,n=t);let o=no(this),s=la(i,a.bind(r),n);return o(),s}function da(e,t){let n=t.split(`.`);return()=>{let t=e;for(let e=0;et===`modelValue`||t===`model-value`?e.modelModifiers:e[`${t}Modifiers`]||e[`${V(t)}Modifiers`]||e[`${$t(t)}Modifiers`];function pa(e,t,...n){if(e.isUnmounted)return;let r=e.vnode.props||P,i=n,a=t.startsWith(`update:`),o=a&&fa(r,t.slice(7));o&&(o.trim&&(i=n.map(e=>z(e)?e.trim():e)),o.number&&(i=n.map(an)));let s,c=r[s=tn(t)]||r[s=tn(V(t))];!c&&a&&(c=r[s=tn($t(t))]),c&&H(c,e,6,i);let l=r[s+`Once`];if(l){if(!e.emitted)e.emitted={};else if(e.emitted[s])return;e.emitted[s]=!0,H(l,e,6,i)}}var ma=new WeakMap;function ha(e,t,n=!1){let r=n?ma:t.emitsCache,i=r.get(e);if(i!==void 0)return i;let a=e.emits,o={},s=!1;if(!R(e)){let r=e=>{let n=ha(e,t,!0);n&&(s=!0,F(o,n))};!n&&t.mixins.length&&t.mixins.forEach(r),e.extends&&r(e.extends),e.mixins&&e.mixins.forEach(r)}return!a&&!s?(B(e)&&r.set(e,null),null):(L(a)?a.forEach(e=>o[e]=null):F(o,a),B(e)&&r.set(e,o),o)}function ga(e,t){return!e||!Lt(t)?!1:(t=t.slice(2).replace(/Once$/,``),I(e,t[0].toLowerCase()+t.slice(1))||I(e,$t(t))||I(e,t))}function _a(e){let{type:t,vnode:n,proxy:r,withProxy:i,propsOptions:[a],slots:o,attrs:s,emit:c,render:l,renderCache:u,props:d,data:f,setupState:p,ctx:m,inheritAttrs:h}=e,g=Rn(e),_,v;try{if(n.shapeFlag&4){let e=i||r,t=e;_=Ka(l.call(t,e,u,d,p,f,m)),v=s}else{let e=t;_=Ka(e.length>1?e(d,{attrs:s,slots:o,emit:c}):e(d,null)),v=t.props?s:va(s)}}catch(t){Da.length=0,bn(t,e,1),_=Z(Y)}let y=_;if(v&&h!==!1){let e=Object.keys(v),{shapeFlag:t}=y;e.length&&t&7&&(a&&e.some(Rt)&&(v=ya(v,a)),y=Ha(y,v,!1,!0))}return n.dirs&&(y=Ha(y,null,!1,!0),y.dirs=y.dirs?y.dirs.concat(n.dirs):n.dirs),n.transition&&gr(y,n.transition),_=y,Rn(g),_}var va=e=>{let t;for(let n in e)(n===`class`||n===`style`||Lt(n))&&((t||={})[n]=e[n]);return t},ya=(e,t)=>{let n={};for(let r in e)(!Rt(r)||!(r.slice(9)in t))&&(n[r]=e[r]);return n};function ba(e,t,n){let{props:r,children:i,component:a}=e,{props:o,children:s,patchFlag:c}=t,l=a.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&c>=0){if(c&1024)return!0;if(c&16)return r?xa(r,o,l):!!o;if(c&8){let e=t.dynamicProps;for(let t=0;te.__isSuspense;function wa(e,t){t&&t.pendingBranch?L(e)?t.effects.push(...e):t.effects.push(e):Mn(e)}var J=Symbol.for(`v-fgt`),Ta=Symbol.for(`v-txt`),Y=Symbol.for(`v-cmt`),Ea=Symbol.for(`v-stc`),Da=[],X=null;function Oa(e=!1){Da.push(X=e?null:[])}function ka(){Da.pop(),X=Da[Da.length-1]||null}var Aa=1;function ja(e,t=!1){Aa+=e,e<0&&X&&t&&(X.hasOnce=!0)}function Ma(e){return e.dynamicChildren=Aa>0?X||Pt:null,ka(),Aa>0&&X&&X.push(e),e}function Na(e,t,n,r,i,a){return Ma(za(e,t,n,r,i,a,!0))}function Pa(e,t,n,r,i){return Ma(Z(e,t,n,r,i,!0))}function Fa(e){return e?e.__v_isVNode===!0:!1}function Ia(e,t){return e.type===t.type&&e.key===t.key}var La=({key:e})=>e??null,Ra=({ref:e,ref_key:t,ref_for:n})=>(typeof e==`number`&&(e=``+e),e==null?null:z(e)||N(e)||R(e)?{i:W,r:e,k:t,f:!!n}:e);function za(e,t=null,n=null,r=0,i=null,a=e===J?0:1,o=!1,s=!1){let c={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&La(t),ref:t&&Ra(t),scopeId:Ln,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:a,patchFlag:r,dynamicProps:i,dynamicChildren:null,appContext:null,ctx:W};return s?(Ja(c,n),a&128&&e.normalize(c)):n&&(c.shapeFlag|=z(n)?8:16),Aa>0&&!o&&X&&(c.patchFlag>0||a&6)&&c.patchFlag!==32&&X.push(c),c}var Z=Ba;function Ba(e,t=null,n=null,r=0,i=null,a=!1){if((!e||e===Qr)&&(e=Y),Fa(e)){let r=Ha(e,t,!0);return n&&Ja(r,n),Aa>0&&!a&&X&&(r.shapeFlag&6?X[X.indexOf(e)]=r:X.push(r)),r.patchFlag=-2,r}if(_o(e)&&(e=e.__vccOpts),t){t=Va(t);let{class:e,style:n}=t;e&&!z(e)&&(t.class=pn(e)),B(n)&&(ft(n)&&!L(n)&&(n=F({},n)),t.style=cn(n))}let o=z(e)?1:Ca(e)?128:Kn(e)?64:B(e)?4:R(e)?2:0;return za(e,t,n,r,i,o,a,!0)}function Va(e){return e?ft(e)||Fi(e)?F({},e):e:null}function Ha(e,t,n=!1,r=!1){let{props:i,ref:a,patchFlag:o,children:s,transition:c}=e,l=t?Ya(i||{},t):i,u={__v_isVNode:!0,__v_skip:!0,type:e.type,props:l,key:l&&La(l),ref:t&&t.ref?n&&a?L(a)?a.concat(Ra(t)):[a,Ra(t)]:Ra(t):a,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:s,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==J?o===-1?16:o|16:o,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:c,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&Ha(e.ssContent),ssFallback:e.ssFallback&&Ha(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return c&&r&&gr(u,c.clone(u)),u}function Ua(e=` `,t=0){return Z(Ta,null,e,t)}function Wa(e,t){let n=Z(Ea,null,e);return n.staticCount=t,n}function Ga(e=``,t=!1){return t?(Oa(),Pa(Y,null,e)):Z(Y,null,e)}function Ka(e){return e==null||typeof e==`boolean`?Z(Y):L(e)?Z(J,null,e.slice()):Fa(e)?qa(e):Z(Ta,null,String(e))}function qa(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:Ha(e)}function Ja(e,t){let n=0,{shapeFlag:r}=e;if(t==null)t=null;else if(L(t))n=16;else if(typeof t==`object`)if(r&65){let n=t.default;n&&(n._c&&(n._d=!1),Ja(e,n()),n._c&&(n._d=!0));return}else{n=32;let r=t._;!r&&!Fi(t)?t._ctx=W:r===3&&W&&(W.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else R(t)?(t={default:t,_ctx:W},n=32):(t=String(t),r&64?(n=16,t=[Ua(t)]):n=8);e.children=t,e.shapeFlag|=n}function Ya(...e){let t={};for(let n=0;n$||W,eo,to;{let e=sn(),t=(t,n)=>{let r;return(r=e[t])||(r=e[t]=[]),r.push(n),e=>{r.length>1?r.forEach(t=>t(e)):r[0](e)}};eo=t(`__VUE_INSTANCE_SETTERS__`,e=>$=e),to=t(`__VUE_SSR_SETTERS__`,e=>ao=e)}var no=e=>{let t=$;return eo(e),e.scope.on(),()=>{e.scope.off(),eo(t)}},ro=()=>{$&&$.scope.off(),eo(null)};function io(e){return e.vnode.shapeFlag&4}var ao=!1;function oo(e,t=!1,n=!1){t&&to(t);let{props:r,children:i}=e.vnode,a=io(e);Ii(e,r,a,t),Yi(e,i,n||t);let o=a?so(e,t):void 0;return t&&to(!1),o}function so(e,t){let n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,ui);let{setup:r}=n;if(r){me();let n=e.setupContext=r.length>1?mo(e):null,i=no(e),a=yn(r,e,0,[e.props,n]),o=Gt(a);if(he(),i(),(o||e.sp)&&!Tr(e)&&yr(e),o){if(a.then(ro,ro),t)return a.then(n=>{co(e,n,t)}).catch(t=>{bn(t,e,0)});e.asyncDep=a}else co(e,a,t)}else fo(e,t)}function co(e,t,n){R(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:B(t)&&(e.setupState=xt(t)),fo(e,n)}var lo,uo;function fo(e,t,n){let r=e.type;if(!e.render){if(!t&&lo&&!r.render){let t=r.template||_i(e).template;if(t){let{isCustomElement:n,compilerOptions:i}=e.appContext.config,{delimiters:a,compilerOptions:o}=r;r.render=lo(t,F(F({isCustomElement:n,delimiters:a},i),o))}}e.render=r.render||Ft,uo&&uo(e)}{let t=no(e);me();try{pi(e)}finally{he(),t()}}}var po={get(e,t){return k(e,`get`,``),e[t]}};function mo(e){return{attrs:new Proxy(e.attrs,po),slots:e.slots,emit:e.emit,expose:t=>{e.exposed=t||{}}}}function ho(e){return e.exposed?e.exposeProxy||=new Proxy(xt(pt(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in ci)return ci[n](e)},has(e,t){return t in e||t in ci}}):e.proxy}function go(e,t=!0){return R(e)?e.displayName||e.name:e.name||t&&e.__name}function _o(e){return R(e)&&`__vccOpts`in e}var vo=(e,t)=>Et(e,t,ao);function yo(e,t,n){try{ja(-1);let r=arguments.length;return r===2?B(t)&&!L(t)?Fa(t)?Z(e,null,[t]):Z(e,t):Z(e,null,t):(r>3?n=Array.prototype.slice.call(arguments,2):r===3&&Fa(n)&&(n=[n]),Z(e,t,n))}finally{ja(1)}}var bo=`3.5.22`,xo=(e,t)=>{let n=e.__vccOpts||e;for(let[e,r]of t)n[e]=r;return n};export{ut as $,Br as A,$r as B,Mi as C,On as D,Ya as E,zn as F,Vn as G,ca as H,ri as I,mn as J,V as K,ii as L,Oa as M,Bn as N,zr as O,Ai as P,w as Q,Zr as R,yo as S,Fa as T,Hn as U,oi as V,Un as W,gn as X,cn as Y,C as Z,Z as _,kr as a,ht as at,$a as b,H as c,j as ct,Pa as d,dt as et,Ga as f,Ua as g,Wa as h,J as i,ot as it,Wr as j,Ur as k,vo as l,St as lt,Zi as m,dr as n,pt as nt,tr as o,st as ot,Na as p,pn as q,sr as r,T as rt,Ta as s,gt as st,xo as t,N as tt,za as u,yt as ut,Er as v,ji as w,Va as x,vr as y,ei as z}; +//# sourceMappingURL=_plugin-vue_export-helper.By3-ojQa.js.map \ No newline at end of file diff --git a/priv/static/static/js/_plugin-vue_export-helper.By3-ojQa.js.map b/priv/static/static/js/_plugin-vue_export-helper.By3-ojQa.js.map new file mode 100644 index 000000000..164604606 --- /dev/null +++ b/priv/static/static/js/_plugin-vue_export-helper.By3-ojQa.js.map @@ -0,0 +1 @@ +{"version":3,"file":"_plugin-vue_export-helper.By3-ojQa.js","names":["makeMap","EMPTY_OBJ","NOOP","extend","remove","hasOwnProperty","hasOwn","isArray","isMap","toTypeString","isSet","isFunction","isString","isSymbol","isObject","objectToString","isPlainObject","cacheStringFunction","camelizeRE","camelize","hyphenateRE","hyphenate","capitalize","toHandlerKey","def","specialBooleanAttrs","isBooleanAttr","isArray","isSymbol","isMap","hasOwnProperty","isRef","isObject","hasOwn","computed","isFunction","watch","EMPTY_OBJ","NOOP","isSet","isPlainObject","isRef"],"sources":["../../../node_modules/@vue/reactivity/node_modules/@vue/shared/dist/shared.esm-bundler.js","../../../node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js","../../../node_modules/@vue/runtime-core/node_modules/@vue/shared/dist/shared.esm-bundler.js","../../../node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js"],"sourcesContent":["/**\n* @vue/shared v3.5.22\n* (c) 2018-present Yuxi (Evan) You and Vue contributors\n* @license MIT\n**/\n// @__NO_SIDE_EFFECTS__\nfunction makeMap(str) {\n const map = /* @__PURE__ */ Object.create(null);\n for (const key of str.split(\",\")) map[key] = 1;\n return (val) => val in map;\n}\n\nconst EMPTY_OBJ = !!(process.env.NODE_ENV !== \"production\") ? Object.freeze({}) : {};\nconst EMPTY_ARR = !!(process.env.NODE_ENV !== \"production\") ? Object.freeze([]) : [];\nconst NOOP = () => {\n};\nconst NO = () => false;\nconst isOn = (key) => key.charCodeAt(0) === 111 && key.charCodeAt(1) === 110 && // uppercase letter\n(key.charCodeAt(2) > 122 || key.charCodeAt(2) < 97);\nconst isModelListener = (key) => key.startsWith(\"onUpdate:\");\nconst extend = Object.assign;\nconst remove = (arr, el) => {\n const i = arr.indexOf(el);\n if (i > -1) {\n arr.splice(i, 1);\n }\n};\nconst hasOwnProperty = Object.prototype.hasOwnProperty;\nconst hasOwn = (val, key) => hasOwnProperty.call(val, key);\nconst isArray = Array.isArray;\nconst isMap = (val) => toTypeString(val) === \"[object Map]\";\nconst isSet = (val) => toTypeString(val) === \"[object Set]\";\nconst isDate = (val) => toTypeString(val) === \"[object Date]\";\nconst isRegExp = (val) => toTypeString(val) === \"[object RegExp]\";\nconst isFunction = (val) => typeof val === \"function\";\nconst isString = (val) => typeof val === \"string\";\nconst isSymbol = (val) => typeof val === \"symbol\";\nconst isObject = (val) => val !== null && typeof val === \"object\";\nconst isPromise = (val) => {\n return (isObject(val) || isFunction(val)) && isFunction(val.then) && isFunction(val.catch);\n};\nconst objectToString = Object.prototype.toString;\nconst toTypeString = (value) => objectToString.call(value);\nconst toRawType = (value) => {\n return toTypeString(value).slice(8, -1);\n};\nconst isPlainObject = (val) => toTypeString(val) === \"[object Object]\";\nconst isIntegerKey = (key) => isString(key) && key !== \"NaN\" && key[0] !== \"-\" && \"\" + parseInt(key, 10) === key;\nconst isReservedProp = /* @__PURE__ */ makeMap(\n // the leading comma is intentional so empty string \"\" is also included\n \",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted\"\n);\nconst isBuiltInDirective = /* @__PURE__ */ makeMap(\n \"bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo\"\n);\nconst cacheStringFunction = (fn) => {\n const cache = /* @__PURE__ */ Object.create(null);\n return ((str) => {\n const hit = cache[str];\n return hit || (cache[str] = fn(str));\n });\n};\nconst camelizeRE = /-\\w/g;\nconst camelize = cacheStringFunction(\n (str) => {\n return str.replace(camelizeRE, (c) => c.slice(1).toUpperCase());\n }\n);\nconst hyphenateRE = /\\B([A-Z])/g;\nconst hyphenate = cacheStringFunction(\n (str) => str.replace(hyphenateRE, \"-$1\").toLowerCase()\n);\nconst capitalize = cacheStringFunction((str) => {\n return str.charAt(0).toUpperCase() + str.slice(1);\n});\nconst toHandlerKey = cacheStringFunction(\n (str) => {\n const s = str ? `on${capitalize(str)}` : ``;\n return s;\n }\n);\nconst hasChanged = (value, oldValue) => !Object.is(value, oldValue);\nconst invokeArrayFns = (fns, ...arg) => {\n for (let i = 0; i < fns.length; i++) {\n fns[i](...arg);\n }\n};\nconst def = (obj, key, value, writable = false) => {\n Object.defineProperty(obj, key, {\n configurable: true,\n enumerable: false,\n writable,\n value\n });\n};\nconst looseToNumber = (val) => {\n const n = parseFloat(val);\n return isNaN(n) ? val : n;\n};\nconst toNumber = (val) => {\n const n = isString(val) ? Number(val) : NaN;\n return isNaN(n) ? val : n;\n};\nlet _globalThis;\nconst getGlobalThis = () => {\n return _globalThis || (_globalThis = typeof globalThis !== \"undefined\" ? globalThis : typeof self !== \"undefined\" ? self : typeof window !== \"undefined\" ? window : typeof global !== \"undefined\" ? global : {});\n};\nconst identRE = /^[_$a-zA-Z\\xA0-\\uFFFF][_$a-zA-Z0-9\\xA0-\\uFFFF]*$/;\nfunction genPropsAccessExp(name) {\n return identRE.test(name) ? `__props.${name}` : `__props[${JSON.stringify(name)}]`;\n}\nfunction genCacheKey(source, options) {\n return source + JSON.stringify(\n options,\n (_, val) => typeof val === \"function\" ? val.toString() : val\n );\n}\n\nconst PatchFlags = {\n \"TEXT\": 1,\n \"1\": \"TEXT\",\n \"CLASS\": 2,\n \"2\": \"CLASS\",\n \"STYLE\": 4,\n \"4\": \"STYLE\",\n \"PROPS\": 8,\n \"8\": \"PROPS\",\n \"FULL_PROPS\": 16,\n \"16\": \"FULL_PROPS\",\n \"NEED_HYDRATION\": 32,\n \"32\": \"NEED_HYDRATION\",\n \"STABLE_FRAGMENT\": 64,\n \"64\": \"STABLE_FRAGMENT\",\n \"KEYED_FRAGMENT\": 128,\n \"128\": \"KEYED_FRAGMENT\",\n \"UNKEYED_FRAGMENT\": 256,\n \"256\": \"UNKEYED_FRAGMENT\",\n \"NEED_PATCH\": 512,\n \"512\": \"NEED_PATCH\",\n \"DYNAMIC_SLOTS\": 1024,\n \"1024\": \"DYNAMIC_SLOTS\",\n \"DEV_ROOT_FRAGMENT\": 2048,\n \"2048\": \"DEV_ROOT_FRAGMENT\",\n \"CACHED\": -1,\n \"-1\": \"CACHED\",\n \"BAIL\": -2,\n \"-2\": \"BAIL\"\n};\nconst PatchFlagNames = {\n [1]: `TEXT`,\n [2]: `CLASS`,\n [4]: `STYLE`,\n [8]: `PROPS`,\n [16]: `FULL_PROPS`,\n [32]: `NEED_HYDRATION`,\n [64]: `STABLE_FRAGMENT`,\n [128]: `KEYED_FRAGMENT`,\n [256]: `UNKEYED_FRAGMENT`,\n [512]: `NEED_PATCH`,\n [1024]: `DYNAMIC_SLOTS`,\n [2048]: `DEV_ROOT_FRAGMENT`,\n [-1]: `CACHED`,\n [-2]: `BAIL`\n};\n\nconst ShapeFlags = {\n \"ELEMENT\": 1,\n \"1\": \"ELEMENT\",\n \"FUNCTIONAL_COMPONENT\": 2,\n \"2\": \"FUNCTIONAL_COMPONENT\",\n \"STATEFUL_COMPONENT\": 4,\n \"4\": \"STATEFUL_COMPONENT\",\n \"TEXT_CHILDREN\": 8,\n \"8\": \"TEXT_CHILDREN\",\n \"ARRAY_CHILDREN\": 16,\n \"16\": \"ARRAY_CHILDREN\",\n \"SLOTS_CHILDREN\": 32,\n \"32\": \"SLOTS_CHILDREN\",\n \"TELEPORT\": 64,\n \"64\": \"TELEPORT\",\n \"SUSPENSE\": 128,\n \"128\": \"SUSPENSE\",\n \"COMPONENT_SHOULD_KEEP_ALIVE\": 256,\n \"256\": \"COMPONENT_SHOULD_KEEP_ALIVE\",\n \"COMPONENT_KEPT_ALIVE\": 512,\n \"512\": \"COMPONENT_KEPT_ALIVE\",\n \"COMPONENT\": 6,\n \"6\": \"COMPONENT\"\n};\n\nconst SlotFlags = {\n \"STABLE\": 1,\n \"1\": \"STABLE\",\n \"DYNAMIC\": 2,\n \"2\": \"DYNAMIC\",\n \"FORWARDED\": 3,\n \"3\": \"FORWARDED\"\n};\nconst slotFlagsText = {\n [1]: \"STABLE\",\n [2]: \"DYNAMIC\",\n [3]: \"FORWARDED\"\n};\n\nconst GLOBALS_ALLOWED = \"Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,console,Error,Symbol\";\nconst isGloballyAllowed = /* @__PURE__ */ makeMap(GLOBALS_ALLOWED);\nconst isGloballyWhitelisted = isGloballyAllowed;\n\nconst range = 2;\nfunction generateCodeFrame(source, start = 0, end = source.length) {\n start = Math.max(0, Math.min(start, source.length));\n end = Math.max(0, Math.min(end, source.length));\n if (start > end) return \"\";\n let lines = source.split(/(\\r?\\n)/);\n const newlineSequences = lines.filter((_, idx) => idx % 2 === 1);\n lines = lines.filter((_, idx) => idx % 2 === 0);\n let count = 0;\n const res = [];\n for (let i = 0; i < lines.length; i++) {\n count += lines[i].length + (newlineSequences[i] && newlineSequences[i].length || 0);\n if (count >= start) {\n for (let j = i - range; j <= i + range || end > count; j++) {\n if (j < 0 || j >= lines.length) continue;\n const line = j + 1;\n res.push(\n `${line}${\" \".repeat(Math.max(3 - String(line).length, 0))}| ${lines[j]}`\n );\n const lineLength = lines[j].length;\n const newLineSeqLength = newlineSequences[j] && newlineSequences[j].length || 0;\n if (j === i) {\n const pad = start - (count - (lineLength + newLineSeqLength));\n const length = Math.max(\n 1,\n end > count ? lineLength - pad : end - start\n );\n res.push(` | ` + \" \".repeat(pad) + \"^\".repeat(length));\n } else if (j > i) {\n if (end > count) {\n const length = Math.max(Math.min(end - count, lineLength), 1);\n res.push(` | ` + \"^\".repeat(length));\n }\n count += lineLength + newLineSeqLength;\n }\n }\n break;\n }\n }\n return res.join(\"\\n\");\n}\n\nfunction normalizeStyle(value) {\n if (isArray(value)) {\n const res = {};\n for (let i = 0; i < value.length; i++) {\n const item = value[i];\n const normalized = isString(item) ? parseStringStyle(item) : normalizeStyle(item);\n if (normalized) {\n for (const key in normalized) {\n res[key] = normalized[key];\n }\n }\n }\n return res;\n } else if (isString(value) || isObject(value)) {\n return value;\n }\n}\nconst listDelimiterRE = /;(?![^(]*\\))/g;\nconst propertyDelimiterRE = /:([^]+)/;\nconst styleCommentRE = /\\/\\*[^]*?\\*\\//g;\nfunction parseStringStyle(cssText) {\n const ret = {};\n cssText.replace(styleCommentRE, \"\").split(listDelimiterRE).forEach((item) => {\n if (item) {\n const tmp = item.split(propertyDelimiterRE);\n tmp.length > 1 && (ret[tmp[0].trim()] = tmp[1].trim());\n }\n });\n return ret;\n}\nfunction stringifyStyle(styles) {\n if (!styles) return \"\";\n if (isString(styles)) return styles;\n let ret = \"\";\n for (const key in styles) {\n const value = styles[key];\n if (isString(value) || typeof value === \"number\") {\n const normalizedKey = key.startsWith(`--`) ? key : hyphenate(key);\n ret += `${normalizedKey}:${value};`;\n }\n }\n return ret;\n}\nfunction normalizeClass(value) {\n let res = \"\";\n if (isString(value)) {\n res = value;\n } else if (isArray(value)) {\n for (let i = 0; i < value.length; i++) {\n const normalized = normalizeClass(value[i]);\n if (normalized) {\n res += normalized + \" \";\n }\n }\n } else if (isObject(value)) {\n for (const name in value) {\n if (value[name]) {\n res += name + \" \";\n }\n }\n }\n return res.trim();\n}\nfunction normalizeProps(props) {\n if (!props) return null;\n let { class: klass, style } = props;\n if (klass && !isString(klass)) {\n props.class = normalizeClass(klass);\n }\n if (style) {\n props.style = normalizeStyle(style);\n }\n return props;\n}\n\nconst HTML_TAGS = \"html,body,base,head,link,meta,style,title,address,article,aside,footer,header,hgroup,h1,h2,h3,h4,h5,h6,nav,section,div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,ruby,s,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,output,progress,select,textarea,details,dialog,menu,summary,template,blockquote,iframe,tfoot\";\nconst SVG_TAGS = \"svg,animate,animateMotion,animateTransform,circle,clipPath,color-profile,defs,desc,discard,ellipse,feBlend,feColorMatrix,feComponentTransfer,feComposite,feConvolveMatrix,feDiffuseLighting,feDisplacementMap,feDistantLight,feDropShadow,feFlood,feFuncA,feFuncB,feFuncG,feFuncR,feGaussianBlur,feImage,feMerge,feMergeNode,feMorphology,feOffset,fePointLight,feSpecularLighting,feSpotLight,feTile,feTurbulence,filter,foreignObject,g,hatch,hatchpath,image,line,linearGradient,marker,mask,mesh,meshgradient,meshpatch,meshrow,metadata,mpath,path,pattern,polygon,polyline,radialGradient,rect,set,solidcolor,stop,switch,symbol,text,textPath,title,tspan,unknown,use,view\";\nconst MATH_TAGS = \"annotation,annotation-xml,maction,maligngroup,malignmark,math,menclose,merror,mfenced,mfrac,mfraction,mglyph,mi,mlabeledtr,mlongdiv,mmultiscripts,mn,mo,mover,mpadded,mphantom,mprescripts,mroot,mrow,ms,mscarries,mscarry,msgroup,msline,mspace,msqrt,msrow,mstack,mstyle,msub,msubsup,msup,mtable,mtd,mtext,mtr,munder,munderover,none,semantics\";\nconst VOID_TAGS = \"area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr\";\nconst isHTMLTag = /* @__PURE__ */ makeMap(HTML_TAGS);\nconst isSVGTag = /* @__PURE__ */ makeMap(SVG_TAGS);\nconst isMathMLTag = /* @__PURE__ */ makeMap(MATH_TAGS);\nconst isVoidTag = /* @__PURE__ */ makeMap(VOID_TAGS);\n\nconst specialBooleanAttrs = `itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly`;\nconst isSpecialBooleanAttr = /* @__PURE__ */ makeMap(specialBooleanAttrs);\nconst isBooleanAttr = /* @__PURE__ */ makeMap(\n specialBooleanAttrs + `,async,autofocus,autoplay,controls,default,defer,disabled,hidden,inert,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected`\n);\nfunction includeBooleanAttr(value) {\n return !!value || value === \"\";\n}\nconst unsafeAttrCharRE = /[>/=\"'\\u0009\\u000a\\u000c\\u0020]/;\nconst attrValidationCache = {};\nfunction isSSRSafeAttrName(name) {\n if (attrValidationCache.hasOwnProperty(name)) {\n return attrValidationCache[name];\n }\n const isUnsafe = unsafeAttrCharRE.test(name);\n if (isUnsafe) {\n console.error(`unsafe attribute name: ${name}`);\n }\n return attrValidationCache[name] = !isUnsafe;\n}\nconst propsToAttrMap = {\n acceptCharset: \"accept-charset\",\n className: \"class\",\n htmlFor: \"for\",\n httpEquiv: \"http-equiv\"\n};\nconst isKnownHtmlAttr = /* @__PURE__ */ makeMap(\n `accept,accept-charset,accesskey,action,align,allow,alt,async,autocapitalize,autocomplete,autofocus,autoplay,background,bgcolor,border,buffered,capture,challenge,charset,checked,cite,class,code,codebase,color,cols,colspan,content,contenteditable,contextmenu,controls,coords,crossorigin,csp,data,datetime,decoding,default,defer,dir,dirname,disabled,download,draggable,dropzone,enctype,enterkeyhint,for,form,formaction,formenctype,formmethod,formnovalidate,formtarget,headers,height,hidden,high,href,hreflang,http-equiv,icon,id,importance,inert,integrity,ismap,itemprop,keytype,kind,label,lang,language,loading,list,loop,low,manifest,max,maxlength,minlength,media,min,multiple,muted,name,novalidate,open,optimum,pattern,ping,placeholder,poster,preload,radiogroup,readonly,referrerpolicy,rel,required,reversed,rows,rowspan,sandbox,scope,scoped,selected,shape,size,sizes,slot,span,spellcheck,src,srcdoc,srclang,srcset,start,step,style,summary,tabindex,target,title,translate,type,usemap,value,width,wrap`\n);\nconst isKnownSvgAttr = /* @__PURE__ */ makeMap(\n `xmlns,accent-height,accumulate,additive,alignment-baseline,alphabetic,amplitude,arabic-form,ascent,attributeName,attributeType,azimuth,baseFrequency,baseline-shift,baseProfile,bbox,begin,bias,by,calcMode,cap-height,class,clip,clipPathUnits,clip-path,clip-rule,color,color-interpolation,color-interpolation-filters,color-profile,color-rendering,contentScriptType,contentStyleType,crossorigin,cursor,cx,cy,d,decelerate,descent,diffuseConstant,direction,display,divisor,dominant-baseline,dur,dx,dy,edgeMode,elevation,enable-background,end,exponent,fill,fill-opacity,fill-rule,filter,filterRes,filterUnits,flood-color,flood-opacity,font-family,font-size,font-size-adjust,font-stretch,font-style,font-variant,font-weight,format,from,fr,fx,fy,g1,g2,glyph-name,glyph-orientation-horizontal,glyph-orientation-vertical,glyphRef,gradientTransform,gradientUnits,hanging,height,href,hreflang,horiz-adv-x,horiz-origin-x,id,ideographic,image-rendering,in,in2,intercept,k,k1,k2,k3,k4,kernelMatrix,kernelUnitLength,kerning,keyPoints,keySplines,keyTimes,lang,lengthAdjust,letter-spacing,lighting-color,limitingConeAngle,local,marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mask,maskContentUnits,maskUnits,mathematical,max,media,method,min,mode,name,numOctaves,offset,opacity,operator,order,orient,orientation,origin,overflow,overline-position,overline-thickness,panose-1,paint-order,path,pathLength,patternContentUnits,patternTransform,patternUnits,ping,pointer-events,points,pointsAtX,pointsAtY,pointsAtZ,preserveAlpha,preserveAspectRatio,primitiveUnits,r,radius,referrerPolicy,refX,refY,rel,rendering-intent,repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,result,rotate,rx,ry,scale,seed,shape-rendering,slope,spacing,specularConstant,specularExponent,speed,spreadMethod,startOffset,stdDeviation,stemh,stemv,stitchTiles,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,string,stroke,stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,stroke-width,style,surfaceScale,systemLanguage,tabindex,tableValues,target,targetX,targetY,text-anchor,text-decoration,text-rendering,textLength,to,transform,transform-origin,type,u1,u2,underline-position,underline-thickness,unicode,unicode-bidi,unicode-range,units-per-em,v-alphabetic,v-hanging,v-ideographic,v-mathematical,values,vector-effect,version,vert-adv-y,vert-origin-x,vert-origin-y,viewBox,viewTarget,visibility,width,widths,word-spacing,writing-mode,x,x-height,x1,x2,xChannelSelector,xlink:actuate,xlink:arcrole,xlink:href,xlink:role,xlink:show,xlink:title,xlink:type,xmlns:xlink,xml:base,xml:lang,xml:space,y,y1,y2,yChannelSelector,z,zoomAndPan`\n);\nconst isKnownMathMLAttr = /* @__PURE__ */ makeMap(\n `accent,accentunder,actiontype,align,alignmentscope,altimg,altimg-height,altimg-valign,altimg-width,alttext,bevelled,close,columnsalign,columnlines,columnspan,denomalign,depth,dir,display,displaystyle,encoding,equalcolumns,equalrows,fence,fontstyle,fontweight,form,frame,framespacing,groupalign,height,href,id,indentalign,indentalignfirst,indentalignlast,indentshift,indentshiftfirst,indentshiftlast,indextype,justify,largetop,largeop,lquote,lspace,mathbackground,mathcolor,mathsize,mathvariant,maxsize,minlabelspacing,mode,other,overflow,position,rowalign,rowlines,rowspan,rquote,rspace,scriptlevel,scriptminsize,scriptsizemultiplier,selection,separator,separators,shift,side,src,stackalign,stretchy,subscriptshift,superscriptshift,symmetric,voffset,width,widths,xlink:href,xlink:show,xlink:type,xmlns`\n);\nfunction isRenderableAttrValue(value) {\n if (value == null) {\n return false;\n }\n const type = typeof value;\n return type === \"string\" || type === \"number\" || type === \"boolean\";\n}\n\nconst escapeRE = /[\"'&<>]/;\nfunction escapeHtml(string) {\n const str = \"\" + string;\n const match = escapeRE.exec(str);\n if (!match) {\n return str;\n }\n let html = \"\";\n let escaped;\n let index;\n let lastIndex = 0;\n for (index = match.index; index < str.length; index++) {\n switch (str.charCodeAt(index)) {\n case 34:\n escaped = \""\";\n break;\n case 38:\n escaped = \"&\";\n break;\n case 39:\n escaped = \"'\";\n break;\n case 60:\n escaped = \"<\";\n break;\n case 62:\n escaped = \">\";\n break;\n default:\n continue;\n }\n if (lastIndex !== index) {\n html += str.slice(lastIndex, index);\n }\n lastIndex = index + 1;\n html += escaped;\n }\n return lastIndex !== index ? html + str.slice(lastIndex, index) : html;\n}\nconst commentStripRE = /^-?>||--!>|?@[\\\\\\]^`{|}~]/g;\nfunction getEscapedCssVarName(key, doubleEscape) {\n return key.replace(\n cssVarNameEscapeSymbolsRE,\n (s) => doubleEscape ? s === '\"' ? '\\\\\\\\\\\\\"' : `\\\\\\\\${s}` : `\\\\${s}`\n );\n}\n\nfunction looseCompareArrays(a, b) {\n if (a.length !== b.length) return false;\n let equal = true;\n for (let i = 0; equal && i < a.length; i++) {\n equal = looseEqual(a[i], b[i]);\n }\n return equal;\n}\nfunction looseEqual(a, b) {\n if (a === b) return true;\n let aValidType = isDate(a);\n let bValidType = isDate(b);\n if (aValidType || bValidType) {\n return aValidType && bValidType ? a.getTime() === b.getTime() : false;\n }\n aValidType = isSymbol(a);\n bValidType = isSymbol(b);\n if (aValidType || bValidType) {\n return a === b;\n }\n aValidType = isArray(a);\n bValidType = isArray(b);\n if (aValidType || bValidType) {\n return aValidType && bValidType ? looseCompareArrays(a, b) : false;\n }\n aValidType = isObject(a);\n bValidType = isObject(b);\n if (aValidType || bValidType) {\n if (!aValidType || !bValidType) {\n return false;\n }\n const aKeysCount = Object.keys(a).length;\n const bKeysCount = Object.keys(b).length;\n if (aKeysCount !== bKeysCount) {\n return false;\n }\n for (const key in a) {\n const aHasKey = a.hasOwnProperty(key);\n const bHasKey = b.hasOwnProperty(key);\n if (aHasKey && !bHasKey || !aHasKey && bHasKey || !looseEqual(a[key], b[key])) {\n return false;\n }\n }\n }\n return String(a) === String(b);\n}\nfunction looseIndexOf(arr, val) {\n return arr.findIndex((item) => looseEqual(item, val));\n}\n\nconst isRef = (val) => {\n return !!(val && val[\"__v_isRef\"] === true);\n};\nconst toDisplayString = (val) => {\n return isString(val) ? val : val == null ? \"\" : isArray(val) || isObject(val) && (val.toString === objectToString || !isFunction(val.toString)) ? isRef(val) ? toDisplayString(val.value) : JSON.stringify(val, replacer, 2) : String(val);\n};\nconst replacer = (_key, val) => {\n if (isRef(val)) {\n return replacer(_key, val.value);\n } else if (isMap(val)) {\n return {\n [`Map(${val.size})`]: [...val.entries()].reduce(\n (entries, [key, val2], i) => {\n entries[stringifySymbol(key, i) + \" =>\"] = val2;\n return entries;\n },\n {}\n )\n };\n } else if (isSet(val)) {\n return {\n [`Set(${val.size})`]: [...val.values()].map((v) => stringifySymbol(v))\n };\n } else if (isSymbol(val)) {\n return stringifySymbol(val);\n } else if (isObject(val) && !isArray(val) && !isPlainObject(val)) {\n return String(val);\n }\n return val;\n};\nconst stringifySymbol = (v, i = \"\") => {\n var _a;\n return (\n // Symbol.description in es2019+ so we need to cast here to pass\n // the lib: es2016 check\n isSymbol(v) ? `Symbol(${(_a = v.description) != null ? _a : i})` : v\n );\n};\n\nfunction normalizeCssVarValue(value) {\n if (value == null) {\n return \"initial\";\n }\n if (typeof value === \"string\") {\n return value === \"\" ? \" \" : value;\n }\n if (typeof value !== \"number\" || !Number.isFinite(value)) {\n if (!!(process.env.NODE_ENV !== \"production\")) {\n console.warn(\n \"[Vue warn] Invalid value used for CSS binding. Expected a string or a finite number but received:\",\n value\n );\n }\n }\n return String(value);\n}\n\nexport { EMPTY_ARR, EMPTY_OBJ, NO, NOOP, PatchFlagNames, PatchFlags, ShapeFlags, SlotFlags, camelize, capitalize, cssVarNameEscapeSymbolsRE, def, escapeHtml, escapeHtmlComment, extend, genCacheKey, genPropsAccessExp, generateCodeFrame, getEscapedCssVarName, getGlobalThis, hasChanged, hasOwn, hyphenate, includeBooleanAttr, invokeArrayFns, isArray, isBooleanAttr, isBuiltInDirective, isDate, isFunction, isGloballyAllowed, isGloballyWhitelisted, isHTMLTag, isIntegerKey, isKnownHtmlAttr, isKnownMathMLAttr, isKnownSvgAttr, isMap, isMathMLTag, isModelListener, isObject, isOn, isPlainObject, isPromise, isRegExp, isRenderableAttrValue, isReservedProp, isSSRSafeAttrName, isSVGTag, isSet, isSpecialBooleanAttr, isString, isSymbol, isVoidTag, looseEqual, looseIndexOf, looseToNumber, makeMap, normalizeClass, normalizeCssVarValue, normalizeProps, normalizeStyle, objectToString, parseStringStyle, propsToAttrMap, remove, slotFlagsText, stringifyStyle, toDisplayString, toHandlerKey, toNumber, toRawType, toTypeString };\n","/**\n* @vue/reactivity v3.5.22\n* (c) 2018-present Yuxi (Evan) You and Vue contributors\n* @license MIT\n**/\nimport { extend, hasChanged, isArray, isIntegerKey, isSymbol, isMap, hasOwn, makeMap, isObject, capitalize, toRawType, def, isFunction, EMPTY_OBJ, isSet, isPlainObject, remove, NOOP } from '@vue/shared';\n\nfunction warn(msg, ...args) {\n console.warn(`[Vue warn] ${msg}`, ...args);\n}\n\nlet activeEffectScope;\nclass EffectScope {\n constructor(detached = false) {\n this.detached = detached;\n /**\n * @internal\n */\n this._active = true;\n /**\n * @internal track `on` calls, allow `on` call multiple times\n */\n this._on = 0;\n /**\n * @internal\n */\n this.effects = [];\n /**\n * @internal\n */\n this.cleanups = [];\n this._isPaused = false;\n this.parent = activeEffectScope;\n if (!detached && activeEffectScope) {\n this.index = (activeEffectScope.scopes || (activeEffectScope.scopes = [])).push(\n this\n ) - 1;\n }\n }\n get active() {\n return this._active;\n }\n pause() {\n if (this._active) {\n this._isPaused = true;\n let i, l;\n if (this.scopes) {\n for (i = 0, l = this.scopes.length; i < l; i++) {\n this.scopes[i].pause();\n }\n }\n for (i = 0, l = this.effects.length; i < l; i++) {\n this.effects[i].pause();\n }\n }\n }\n /**\n * Resumes the effect scope, including all child scopes and effects.\n */\n resume() {\n if (this._active) {\n if (this._isPaused) {\n this._isPaused = false;\n let i, l;\n if (this.scopes) {\n for (i = 0, l = this.scopes.length; i < l; i++) {\n this.scopes[i].resume();\n }\n }\n for (i = 0, l = this.effects.length; i < l; i++) {\n this.effects[i].resume();\n }\n }\n }\n }\n run(fn) {\n if (this._active) {\n const currentEffectScope = activeEffectScope;\n try {\n activeEffectScope = this;\n return fn();\n } finally {\n activeEffectScope = currentEffectScope;\n }\n } else if (!!(process.env.NODE_ENV !== \"production\")) {\n warn(`cannot run an inactive effect scope.`);\n }\n }\n /**\n * This should only be called on non-detached scopes\n * @internal\n */\n on() {\n if (++this._on === 1) {\n this.prevScope = activeEffectScope;\n activeEffectScope = this;\n }\n }\n /**\n * This should only be called on non-detached scopes\n * @internal\n */\n off() {\n if (this._on > 0 && --this._on === 0) {\n activeEffectScope = this.prevScope;\n this.prevScope = void 0;\n }\n }\n stop(fromParent) {\n if (this._active) {\n this._active = false;\n let i, l;\n for (i = 0, l = this.effects.length; i < l; i++) {\n this.effects[i].stop();\n }\n this.effects.length = 0;\n for (i = 0, l = this.cleanups.length; i < l; i++) {\n this.cleanups[i]();\n }\n this.cleanups.length = 0;\n if (this.scopes) {\n for (i = 0, l = this.scopes.length; i < l; i++) {\n this.scopes[i].stop(true);\n }\n this.scopes.length = 0;\n }\n if (!this.detached && this.parent && !fromParent) {\n const last = this.parent.scopes.pop();\n if (last && last !== this) {\n this.parent.scopes[this.index] = last;\n last.index = this.index;\n }\n }\n this.parent = void 0;\n }\n }\n}\nfunction effectScope(detached) {\n return new EffectScope(detached);\n}\nfunction getCurrentScope() {\n return activeEffectScope;\n}\nfunction onScopeDispose(fn, failSilently = false) {\n if (activeEffectScope) {\n activeEffectScope.cleanups.push(fn);\n } else if (!!(process.env.NODE_ENV !== \"production\") && !failSilently) {\n warn(\n `onScopeDispose() is called when there is no active effect scope to be associated with.`\n );\n }\n}\n\nlet activeSub;\nconst EffectFlags = {\n \"ACTIVE\": 1,\n \"1\": \"ACTIVE\",\n \"RUNNING\": 2,\n \"2\": \"RUNNING\",\n \"TRACKING\": 4,\n \"4\": \"TRACKING\",\n \"NOTIFIED\": 8,\n \"8\": \"NOTIFIED\",\n \"DIRTY\": 16,\n \"16\": \"DIRTY\",\n \"ALLOW_RECURSE\": 32,\n \"32\": \"ALLOW_RECURSE\",\n \"PAUSED\": 64,\n \"64\": \"PAUSED\",\n \"EVALUATED\": 128,\n \"128\": \"EVALUATED\"\n};\nconst pausedQueueEffects = /* @__PURE__ */ new WeakSet();\nclass ReactiveEffect {\n constructor(fn) {\n this.fn = fn;\n /**\n * @internal\n */\n this.deps = void 0;\n /**\n * @internal\n */\n this.depsTail = void 0;\n /**\n * @internal\n */\n this.flags = 1 | 4;\n /**\n * @internal\n */\n this.next = void 0;\n /**\n * @internal\n */\n this.cleanup = void 0;\n this.scheduler = void 0;\n if (activeEffectScope && activeEffectScope.active) {\n activeEffectScope.effects.push(this);\n }\n }\n pause() {\n this.flags |= 64;\n }\n resume() {\n if (this.flags & 64) {\n this.flags &= -65;\n if (pausedQueueEffects.has(this)) {\n pausedQueueEffects.delete(this);\n this.trigger();\n }\n }\n }\n /**\n * @internal\n */\n notify() {\n if (this.flags & 2 && !(this.flags & 32)) {\n return;\n }\n if (!(this.flags & 8)) {\n batch(this);\n }\n }\n run() {\n if (!(this.flags & 1)) {\n return this.fn();\n }\n this.flags |= 2;\n cleanupEffect(this);\n prepareDeps(this);\n const prevEffect = activeSub;\n const prevShouldTrack = shouldTrack;\n activeSub = this;\n shouldTrack = true;\n try {\n return this.fn();\n } finally {\n if (!!(process.env.NODE_ENV !== \"production\") && activeSub !== this) {\n warn(\n \"Active effect was not restored correctly - this is likely a Vue internal bug.\"\n );\n }\n cleanupDeps(this);\n activeSub = prevEffect;\n shouldTrack = prevShouldTrack;\n this.flags &= -3;\n }\n }\n stop() {\n if (this.flags & 1) {\n for (let link = this.deps; link; link = link.nextDep) {\n removeSub(link);\n }\n this.deps = this.depsTail = void 0;\n cleanupEffect(this);\n this.onStop && this.onStop();\n this.flags &= -2;\n }\n }\n trigger() {\n if (this.flags & 64) {\n pausedQueueEffects.add(this);\n } else if (this.scheduler) {\n this.scheduler();\n } else {\n this.runIfDirty();\n }\n }\n /**\n * @internal\n */\n runIfDirty() {\n if (isDirty(this)) {\n this.run();\n }\n }\n get dirty() {\n return isDirty(this);\n }\n}\nlet batchDepth = 0;\nlet batchedSub;\nlet batchedComputed;\nfunction batch(sub, isComputed = false) {\n sub.flags |= 8;\n if (isComputed) {\n sub.next = batchedComputed;\n batchedComputed = sub;\n return;\n }\n sub.next = batchedSub;\n batchedSub = sub;\n}\nfunction startBatch() {\n batchDepth++;\n}\nfunction endBatch() {\n if (--batchDepth > 0) {\n return;\n }\n if (batchedComputed) {\n let e = batchedComputed;\n batchedComputed = void 0;\n while (e) {\n const next = e.next;\n e.next = void 0;\n e.flags &= -9;\n e = next;\n }\n }\n let error;\n while (batchedSub) {\n let e = batchedSub;\n batchedSub = void 0;\n while (e) {\n const next = e.next;\n e.next = void 0;\n e.flags &= -9;\n if (e.flags & 1) {\n try {\n ;\n e.trigger();\n } catch (err) {\n if (!error) error = err;\n }\n }\n e = next;\n }\n }\n if (error) throw error;\n}\nfunction prepareDeps(sub) {\n for (let link = sub.deps; link; link = link.nextDep) {\n link.version = -1;\n link.prevActiveLink = link.dep.activeLink;\n link.dep.activeLink = link;\n }\n}\nfunction cleanupDeps(sub) {\n let head;\n let tail = sub.depsTail;\n let link = tail;\n while (link) {\n const prev = link.prevDep;\n if (link.version === -1) {\n if (link === tail) tail = prev;\n removeSub(link);\n removeDep(link);\n } else {\n head = link;\n }\n link.dep.activeLink = link.prevActiveLink;\n link.prevActiveLink = void 0;\n link = prev;\n }\n sub.deps = head;\n sub.depsTail = tail;\n}\nfunction isDirty(sub) {\n for (let link = sub.deps; link; link = link.nextDep) {\n if (link.dep.version !== link.version || link.dep.computed && (refreshComputed(link.dep.computed) || link.dep.version !== link.version)) {\n return true;\n }\n }\n if (sub._dirty) {\n return true;\n }\n return false;\n}\nfunction refreshComputed(computed) {\n if (computed.flags & 4 && !(computed.flags & 16)) {\n return;\n }\n computed.flags &= -17;\n if (computed.globalVersion === globalVersion) {\n return;\n }\n computed.globalVersion = globalVersion;\n if (!computed.isSSR && computed.flags & 128 && (!computed.deps && !computed._dirty || !isDirty(computed))) {\n return;\n }\n computed.flags |= 2;\n const dep = computed.dep;\n const prevSub = activeSub;\n const prevShouldTrack = shouldTrack;\n activeSub = computed;\n shouldTrack = true;\n try {\n prepareDeps(computed);\n const value = computed.fn(computed._value);\n if (dep.version === 0 || hasChanged(value, computed._value)) {\n computed.flags |= 128;\n computed._value = value;\n dep.version++;\n }\n } catch (err) {\n dep.version++;\n throw err;\n } finally {\n activeSub = prevSub;\n shouldTrack = prevShouldTrack;\n cleanupDeps(computed);\n computed.flags &= -3;\n }\n}\nfunction removeSub(link, soft = false) {\n const { dep, prevSub, nextSub } = link;\n if (prevSub) {\n prevSub.nextSub = nextSub;\n link.prevSub = void 0;\n }\n if (nextSub) {\n nextSub.prevSub = prevSub;\n link.nextSub = void 0;\n }\n if (!!(process.env.NODE_ENV !== \"production\") && dep.subsHead === link) {\n dep.subsHead = nextSub;\n }\n if (dep.subs === link) {\n dep.subs = prevSub;\n if (!prevSub && dep.computed) {\n dep.computed.flags &= -5;\n for (let l = dep.computed.deps; l; l = l.nextDep) {\n removeSub(l, true);\n }\n }\n }\n if (!soft && !--dep.sc && dep.map) {\n dep.map.delete(dep.key);\n }\n}\nfunction removeDep(link) {\n const { prevDep, nextDep } = link;\n if (prevDep) {\n prevDep.nextDep = nextDep;\n link.prevDep = void 0;\n }\n if (nextDep) {\n nextDep.prevDep = prevDep;\n link.nextDep = void 0;\n }\n}\nfunction effect(fn, options) {\n if (fn.effect instanceof ReactiveEffect) {\n fn = fn.effect.fn;\n }\n const e = new ReactiveEffect(fn);\n if (options) {\n extend(e, options);\n }\n try {\n e.run();\n } catch (err) {\n e.stop();\n throw err;\n }\n const runner = e.run.bind(e);\n runner.effect = e;\n return runner;\n}\nfunction stop(runner) {\n runner.effect.stop();\n}\nlet shouldTrack = true;\nconst trackStack = [];\nfunction pauseTracking() {\n trackStack.push(shouldTrack);\n shouldTrack = false;\n}\nfunction enableTracking() {\n trackStack.push(shouldTrack);\n shouldTrack = true;\n}\nfunction resetTracking() {\n const last = trackStack.pop();\n shouldTrack = last === void 0 ? true : last;\n}\nfunction onEffectCleanup(fn, failSilently = false) {\n if (activeSub instanceof ReactiveEffect) {\n activeSub.cleanup = fn;\n } else if (!!(process.env.NODE_ENV !== \"production\") && !failSilently) {\n warn(\n `onEffectCleanup() was called when there was no active effect to associate with.`\n );\n }\n}\nfunction cleanupEffect(e) {\n const { cleanup } = e;\n e.cleanup = void 0;\n if (cleanup) {\n const prevSub = activeSub;\n activeSub = void 0;\n try {\n cleanup();\n } finally {\n activeSub = prevSub;\n }\n }\n}\n\nlet globalVersion = 0;\nclass Link {\n constructor(sub, dep) {\n this.sub = sub;\n this.dep = dep;\n this.version = dep.version;\n this.nextDep = this.prevDep = this.nextSub = this.prevSub = this.prevActiveLink = void 0;\n }\n}\nclass Dep {\n // TODO isolatedDeclarations \"__v_skip\"\n constructor(computed) {\n this.computed = computed;\n this.version = 0;\n /**\n * Link between this dep and the current active effect\n */\n this.activeLink = void 0;\n /**\n * Doubly linked list representing the subscribing effects (tail)\n */\n this.subs = void 0;\n /**\n * For object property deps cleanup\n */\n this.map = void 0;\n this.key = void 0;\n /**\n * Subscriber counter\n */\n this.sc = 0;\n /**\n * @internal\n */\n this.__v_skip = true;\n if (!!(process.env.NODE_ENV !== \"production\")) {\n this.subsHead = void 0;\n }\n }\n track(debugInfo) {\n if (!activeSub || !shouldTrack || activeSub === this.computed) {\n return;\n }\n let link = this.activeLink;\n if (link === void 0 || link.sub !== activeSub) {\n link = this.activeLink = new Link(activeSub, this);\n if (!activeSub.deps) {\n activeSub.deps = activeSub.depsTail = link;\n } else {\n link.prevDep = activeSub.depsTail;\n activeSub.depsTail.nextDep = link;\n activeSub.depsTail = link;\n }\n addSub(link);\n } else if (link.version === -1) {\n link.version = this.version;\n if (link.nextDep) {\n const next = link.nextDep;\n next.prevDep = link.prevDep;\n if (link.prevDep) {\n link.prevDep.nextDep = next;\n }\n link.prevDep = activeSub.depsTail;\n link.nextDep = void 0;\n activeSub.depsTail.nextDep = link;\n activeSub.depsTail = link;\n if (activeSub.deps === link) {\n activeSub.deps = next;\n }\n }\n }\n if (!!(process.env.NODE_ENV !== \"production\") && activeSub.onTrack) {\n activeSub.onTrack(\n extend(\n {\n effect: activeSub\n },\n debugInfo\n )\n );\n }\n return link;\n }\n trigger(debugInfo) {\n this.version++;\n globalVersion++;\n this.notify(debugInfo);\n }\n notify(debugInfo) {\n startBatch();\n try {\n if (!!(process.env.NODE_ENV !== \"production\")) {\n for (let head = this.subsHead; head; head = head.nextSub) {\n if (head.sub.onTrigger && !(head.sub.flags & 8)) {\n head.sub.onTrigger(\n extend(\n {\n effect: head.sub\n },\n debugInfo\n )\n );\n }\n }\n }\n for (let link = this.subs; link; link = link.prevSub) {\n if (link.sub.notify()) {\n ;\n link.sub.dep.notify();\n }\n }\n } finally {\n endBatch();\n }\n }\n}\nfunction addSub(link) {\n link.dep.sc++;\n if (link.sub.flags & 4) {\n const computed = link.dep.computed;\n if (computed && !link.dep.subs) {\n computed.flags |= 4 | 16;\n for (let l = computed.deps; l; l = l.nextDep) {\n addSub(l);\n }\n }\n const currentTail = link.dep.subs;\n if (currentTail !== link) {\n link.prevSub = currentTail;\n if (currentTail) currentTail.nextSub = link;\n }\n if (!!(process.env.NODE_ENV !== \"production\") && link.dep.subsHead === void 0) {\n link.dep.subsHead = link;\n }\n link.dep.subs = link;\n }\n}\nconst targetMap = /* @__PURE__ */ new WeakMap();\nconst ITERATE_KEY = Symbol(\n !!(process.env.NODE_ENV !== \"production\") ? \"Object iterate\" : \"\"\n);\nconst MAP_KEY_ITERATE_KEY = Symbol(\n !!(process.env.NODE_ENV !== \"production\") ? \"Map keys iterate\" : \"\"\n);\nconst ARRAY_ITERATE_KEY = Symbol(\n !!(process.env.NODE_ENV !== \"production\") ? \"Array iterate\" : \"\"\n);\nfunction track(target, type, key) {\n if (shouldTrack && activeSub) {\n let depsMap = targetMap.get(target);\n if (!depsMap) {\n targetMap.set(target, depsMap = /* @__PURE__ */ new Map());\n }\n let dep = depsMap.get(key);\n if (!dep) {\n depsMap.set(key, dep = new Dep());\n dep.map = depsMap;\n dep.key = key;\n }\n if (!!(process.env.NODE_ENV !== \"production\")) {\n dep.track({\n target,\n type,\n key\n });\n } else {\n dep.track();\n }\n }\n}\nfunction trigger(target, type, key, newValue, oldValue, oldTarget) {\n const depsMap = targetMap.get(target);\n if (!depsMap) {\n globalVersion++;\n return;\n }\n const run = (dep) => {\n if (dep) {\n if (!!(process.env.NODE_ENV !== \"production\")) {\n dep.trigger({\n target,\n type,\n key,\n newValue,\n oldValue,\n oldTarget\n });\n } else {\n dep.trigger();\n }\n }\n };\n startBatch();\n if (type === \"clear\") {\n depsMap.forEach(run);\n } else {\n const targetIsArray = isArray(target);\n const isArrayIndex = targetIsArray && isIntegerKey(key);\n if (targetIsArray && key === \"length\") {\n const newLength = Number(newValue);\n depsMap.forEach((dep, key2) => {\n if (key2 === \"length\" || key2 === ARRAY_ITERATE_KEY || !isSymbol(key2) && key2 >= newLength) {\n run(dep);\n }\n });\n } else {\n if (key !== void 0 || depsMap.has(void 0)) {\n run(depsMap.get(key));\n }\n if (isArrayIndex) {\n run(depsMap.get(ARRAY_ITERATE_KEY));\n }\n switch (type) {\n case \"add\":\n if (!targetIsArray) {\n run(depsMap.get(ITERATE_KEY));\n if (isMap(target)) {\n run(depsMap.get(MAP_KEY_ITERATE_KEY));\n }\n } else if (isArrayIndex) {\n run(depsMap.get(\"length\"));\n }\n break;\n case \"delete\":\n if (!targetIsArray) {\n run(depsMap.get(ITERATE_KEY));\n if (isMap(target)) {\n run(depsMap.get(MAP_KEY_ITERATE_KEY));\n }\n }\n break;\n case \"set\":\n if (isMap(target)) {\n run(depsMap.get(ITERATE_KEY));\n }\n break;\n }\n }\n }\n endBatch();\n}\nfunction getDepFromReactive(object, key) {\n const depMap = targetMap.get(object);\n return depMap && depMap.get(key);\n}\n\nfunction reactiveReadArray(array) {\n const raw = toRaw(array);\n if (raw === array) return raw;\n track(raw, \"iterate\", ARRAY_ITERATE_KEY);\n return isShallow(array) ? raw : raw.map(toReactive);\n}\nfunction shallowReadArray(arr) {\n track(arr = toRaw(arr), \"iterate\", ARRAY_ITERATE_KEY);\n return arr;\n}\nconst arrayInstrumentations = {\n __proto__: null,\n [Symbol.iterator]() {\n return iterator(this, Symbol.iterator, toReactive);\n },\n concat(...args) {\n return reactiveReadArray(this).concat(\n ...args.map((x) => isArray(x) ? reactiveReadArray(x) : x)\n );\n },\n entries() {\n return iterator(this, \"entries\", (value) => {\n value[1] = toReactive(value[1]);\n return value;\n });\n },\n every(fn, thisArg) {\n return apply(this, \"every\", fn, thisArg, void 0, arguments);\n },\n filter(fn, thisArg) {\n return apply(this, \"filter\", fn, thisArg, (v) => v.map(toReactive), arguments);\n },\n find(fn, thisArg) {\n return apply(this, \"find\", fn, thisArg, toReactive, arguments);\n },\n findIndex(fn, thisArg) {\n return apply(this, \"findIndex\", fn, thisArg, void 0, arguments);\n },\n findLast(fn, thisArg) {\n return apply(this, \"findLast\", fn, thisArg, toReactive, arguments);\n },\n findLastIndex(fn, thisArg) {\n return apply(this, \"findLastIndex\", fn, thisArg, void 0, arguments);\n },\n // flat, flatMap could benefit from ARRAY_ITERATE but are not straight-forward to implement\n forEach(fn, thisArg) {\n return apply(this, \"forEach\", fn, thisArg, void 0, arguments);\n },\n includes(...args) {\n return searchProxy(this, \"includes\", args);\n },\n indexOf(...args) {\n return searchProxy(this, \"indexOf\", args);\n },\n join(separator) {\n return reactiveReadArray(this).join(separator);\n },\n // keys() iterator only reads `length`, no optimization required\n lastIndexOf(...args) {\n return searchProxy(this, \"lastIndexOf\", args);\n },\n map(fn, thisArg) {\n return apply(this, \"map\", fn, thisArg, void 0, arguments);\n },\n pop() {\n return noTracking(this, \"pop\");\n },\n push(...args) {\n return noTracking(this, \"push\", args);\n },\n reduce(fn, ...args) {\n return reduce(this, \"reduce\", fn, args);\n },\n reduceRight(fn, ...args) {\n return reduce(this, \"reduceRight\", fn, args);\n },\n shift() {\n return noTracking(this, \"shift\");\n },\n // slice could use ARRAY_ITERATE but also seems to beg for range tracking\n some(fn, thisArg) {\n return apply(this, \"some\", fn, thisArg, void 0, arguments);\n },\n splice(...args) {\n return noTracking(this, \"splice\", args);\n },\n toReversed() {\n return reactiveReadArray(this).toReversed();\n },\n toSorted(comparer) {\n return reactiveReadArray(this).toSorted(comparer);\n },\n toSpliced(...args) {\n return reactiveReadArray(this).toSpliced(...args);\n },\n unshift(...args) {\n return noTracking(this, \"unshift\", args);\n },\n values() {\n return iterator(this, \"values\", toReactive);\n }\n};\nfunction iterator(self, method, wrapValue) {\n const arr = shallowReadArray(self);\n const iter = arr[method]();\n if (arr !== self && !isShallow(self)) {\n iter._next = iter.next;\n iter.next = () => {\n const result = iter._next();\n if (!result.done) {\n result.value = wrapValue(result.value);\n }\n return result;\n };\n }\n return iter;\n}\nconst arrayProto = Array.prototype;\nfunction apply(self, method, fn, thisArg, wrappedRetFn, args) {\n const arr = shallowReadArray(self);\n const needsWrap = arr !== self && !isShallow(self);\n const methodFn = arr[method];\n if (methodFn !== arrayProto[method]) {\n const result2 = methodFn.apply(self, args);\n return needsWrap ? toReactive(result2) : result2;\n }\n let wrappedFn = fn;\n if (arr !== self) {\n if (needsWrap) {\n wrappedFn = function(item, index) {\n return fn.call(this, toReactive(item), index, self);\n };\n } else if (fn.length > 2) {\n wrappedFn = function(item, index) {\n return fn.call(this, item, index, self);\n };\n }\n }\n const result = methodFn.call(arr, wrappedFn, thisArg);\n return needsWrap && wrappedRetFn ? wrappedRetFn(result) : result;\n}\nfunction reduce(self, method, fn, args) {\n const arr = shallowReadArray(self);\n let wrappedFn = fn;\n if (arr !== self) {\n if (!isShallow(self)) {\n wrappedFn = function(acc, item, index) {\n return fn.call(this, acc, toReactive(item), index, self);\n };\n } else if (fn.length > 3) {\n wrappedFn = function(acc, item, index) {\n return fn.call(this, acc, item, index, self);\n };\n }\n }\n return arr[method](wrappedFn, ...args);\n}\nfunction searchProxy(self, method, args) {\n const arr = toRaw(self);\n track(arr, \"iterate\", ARRAY_ITERATE_KEY);\n const res = arr[method](...args);\n if ((res === -1 || res === false) && isProxy(args[0])) {\n args[0] = toRaw(args[0]);\n return arr[method](...args);\n }\n return res;\n}\nfunction noTracking(self, method, args = []) {\n pauseTracking();\n startBatch();\n const res = toRaw(self)[method].apply(self, args);\n endBatch();\n resetTracking();\n return res;\n}\n\nconst isNonTrackableKeys = /* @__PURE__ */ makeMap(`__proto__,__v_isRef,__isVue`);\nconst builtInSymbols = new Set(\n /* @__PURE__ */ Object.getOwnPropertyNames(Symbol).filter((key) => key !== \"arguments\" && key !== \"caller\").map((key) => Symbol[key]).filter(isSymbol)\n);\nfunction hasOwnProperty(key) {\n if (!isSymbol(key)) key = String(key);\n const obj = toRaw(this);\n track(obj, \"has\", key);\n return obj.hasOwnProperty(key);\n}\nclass BaseReactiveHandler {\n constructor(_isReadonly = false, _isShallow = false) {\n this._isReadonly = _isReadonly;\n this._isShallow = _isShallow;\n }\n get(target, key, receiver) {\n if (key === \"__v_skip\") return target[\"__v_skip\"];\n const isReadonly2 = this._isReadonly, isShallow2 = this._isShallow;\n if (key === \"__v_isReactive\") {\n return !isReadonly2;\n } else if (key === \"__v_isReadonly\") {\n return isReadonly2;\n } else if (key === \"__v_isShallow\") {\n return isShallow2;\n } else if (key === \"__v_raw\") {\n if (receiver === (isReadonly2 ? isShallow2 ? shallowReadonlyMap : readonlyMap : isShallow2 ? shallowReactiveMap : reactiveMap).get(target) || // receiver is not the reactive proxy, but has the same prototype\n // this means the receiver is a user proxy of the reactive proxy\n Object.getPrototypeOf(target) === Object.getPrototypeOf(receiver)) {\n return target;\n }\n return;\n }\n const targetIsArray = isArray(target);\n if (!isReadonly2) {\n let fn;\n if (targetIsArray && (fn = arrayInstrumentations[key])) {\n return fn;\n }\n if (key === \"hasOwnProperty\") {\n return hasOwnProperty;\n }\n }\n const res = Reflect.get(\n target,\n key,\n // if this is a proxy wrapping a ref, return methods using the raw ref\n // as receiver so that we don't have to call `toRaw` on the ref in all\n // its class methods\n isRef(target) ? target : receiver\n );\n if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {\n return res;\n }\n if (!isReadonly2) {\n track(target, \"get\", key);\n }\n if (isShallow2) {\n return res;\n }\n if (isRef(res)) {\n const value = targetIsArray && isIntegerKey(key) ? res : res.value;\n return isReadonly2 && isObject(value) ? readonly(value) : value;\n }\n if (isObject(res)) {\n return isReadonly2 ? readonly(res) : reactive(res);\n }\n return res;\n }\n}\nclass MutableReactiveHandler extends BaseReactiveHandler {\n constructor(isShallow2 = false) {\n super(false, isShallow2);\n }\n set(target, key, value, receiver) {\n let oldValue = target[key];\n if (!this._isShallow) {\n const isOldValueReadonly = isReadonly(oldValue);\n if (!isShallow(value) && !isReadonly(value)) {\n oldValue = toRaw(oldValue);\n value = toRaw(value);\n }\n if (!isArray(target) && isRef(oldValue) && !isRef(value)) {\n if (isOldValueReadonly) {\n if (!!(process.env.NODE_ENV !== \"production\")) {\n warn(\n `Set operation on key \"${String(key)}\" failed: target is readonly.`,\n target[key]\n );\n }\n return true;\n } else {\n oldValue.value = value;\n return true;\n }\n }\n }\n const hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key);\n const result = Reflect.set(\n target,\n key,\n value,\n isRef(target) ? target : receiver\n );\n if (target === toRaw(receiver)) {\n if (!hadKey) {\n trigger(target, \"add\", key, value);\n } else if (hasChanged(value, oldValue)) {\n trigger(target, \"set\", key, value, oldValue);\n }\n }\n return result;\n }\n deleteProperty(target, key) {\n const hadKey = hasOwn(target, key);\n const oldValue = target[key];\n const result = Reflect.deleteProperty(target, key);\n if (result && hadKey) {\n trigger(target, \"delete\", key, void 0, oldValue);\n }\n return result;\n }\n has(target, key) {\n const result = Reflect.has(target, key);\n if (!isSymbol(key) || !builtInSymbols.has(key)) {\n track(target, \"has\", key);\n }\n return result;\n }\n ownKeys(target) {\n track(\n target,\n \"iterate\",\n isArray(target) ? \"length\" : ITERATE_KEY\n );\n return Reflect.ownKeys(target);\n }\n}\nclass ReadonlyReactiveHandler extends BaseReactiveHandler {\n constructor(isShallow2 = false) {\n super(true, isShallow2);\n }\n set(target, key) {\n if (!!(process.env.NODE_ENV !== \"production\")) {\n warn(\n `Set operation on key \"${String(key)}\" failed: target is readonly.`,\n target\n );\n }\n return true;\n }\n deleteProperty(target, key) {\n if (!!(process.env.NODE_ENV !== \"production\")) {\n warn(\n `Delete operation on key \"${String(key)}\" failed: target is readonly.`,\n target\n );\n }\n return true;\n }\n}\nconst mutableHandlers = /* @__PURE__ */ new MutableReactiveHandler();\nconst readonlyHandlers = /* @__PURE__ */ new ReadonlyReactiveHandler();\nconst shallowReactiveHandlers = /* @__PURE__ */ new MutableReactiveHandler(true);\nconst shallowReadonlyHandlers = /* @__PURE__ */ new ReadonlyReactiveHandler(true);\n\nconst toShallow = (value) => value;\nconst getProto = (v) => Reflect.getPrototypeOf(v);\nfunction createIterableMethod(method, isReadonly2, isShallow2) {\n return function(...args) {\n const target = this[\"__v_raw\"];\n const rawTarget = toRaw(target);\n const targetIsMap = isMap(rawTarget);\n const isPair = method === \"entries\" || method === Symbol.iterator && targetIsMap;\n const isKeyOnly = method === \"keys\" && targetIsMap;\n const innerIterator = target[method](...args);\n const wrap = isShallow2 ? toShallow : isReadonly2 ? toReadonly : toReactive;\n !isReadonly2 && track(\n rawTarget,\n \"iterate\",\n isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY\n );\n return {\n // iterator protocol\n next() {\n const { value, done } = innerIterator.next();\n return done ? { value, done } : {\n value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),\n done\n };\n },\n // iterable protocol\n [Symbol.iterator]() {\n return this;\n }\n };\n };\n}\nfunction createReadonlyMethod(type) {\n return function(...args) {\n if (!!(process.env.NODE_ENV !== \"production\")) {\n const key = args[0] ? `on key \"${args[0]}\" ` : ``;\n warn(\n `${capitalize(type)} operation ${key}failed: target is readonly.`,\n toRaw(this)\n );\n }\n return type === \"delete\" ? false : type === \"clear\" ? void 0 : this;\n };\n}\nfunction createInstrumentations(readonly, shallow) {\n const instrumentations = {\n get(key) {\n const target = this[\"__v_raw\"];\n const rawTarget = toRaw(target);\n const rawKey = toRaw(key);\n if (!readonly) {\n if (hasChanged(key, rawKey)) {\n track(rawTarget, \"get\", key);\n }\n track(rawTarget, \"get\", rawKey);\n }\n const { has } = getProto(rawTarget);\n const wrap = shallow ? toShallow : readonly ? toReadonly : toReactive;\n if (has.call(rawTarget, key)) {\n return wrap(target.get(key));\n } else if (has.call(rawTarget, rawKey)) {\n return wrap(target.get(rawKey));\n } else if (target !== rawTarget) {\n target.get(key);\n }\n },\n get size() {\n const target = this[\"__v_raw\"];\n !readonly && track(toRaw(target), \"iterate\", ITERATE_KEY);\n return target.size;\n },\n has(key) {\n const target = this[\"__v_raw\"];\n const rawTarget = toRaw(target);\n const rawKey = toRaw(key);\n if (!readonly) {\n if (hasChanged(key, rawKey)) {\n track(rawTarget, \"has\", key);\n }\n track(rawTarget, \"has\", rawKey);\n }\n return key === rawKey ? target.has(key) : target.has(key) || target.has(rawKey);\n },\n forEach(callback, thisArg) {\n const observed = this;\n const target = observed[\"__v_raw\"];\n const rawTarget = toRaw(target);\n const wrap = shallow ? toShallow : readonly ? toReadonly : toReactive;\n !readonly && track(rawTarget, \"iterate\", ITERATE_KEY);\n return target.forEach((value, key) => {\n return callback.call(thisArg, wrap(value), wrap(key), observed);\n });\n }\n };\n extend(\n instrumentations,\n readonly ? {\n add: createReadonlyMethod(\"add\"),\n set: createReadonlyMethod(\"set\"),\n delete: createReadonlyMethod(\"delete\"),\n clear: createReadonlyMethod(\"clear\")\n } : {\n add(value) {\n if (!shallow && !isShallow(value) && !isReadonly(value)) {\n value = toRaw(value);\n }\n const target = toRaw(this);\n const proto = getProto(target);\n const hadKey = proto.has.call(target, value);\n if (!hadKey) {\n target.add(value);\n trigger(target, \"add\", value, value);\n }\n return this;\n },\n set(key, value) {\n if (!shallow && !isShallow(value) && !isReadonly(value)) {\n value = toRaw(value);\n }\n const target = toRaw(this);\n const { has, get } = getProto(target);\n let hadKey = has.call(target, key);\n if (!hadKey) {\n key = toRaw(key);\n hadKey = has.call(target, key);\n } else if (!!(process.env.NODE_ENV !== \"production\")) {\n checkIdentityKeys(target, has, key);\n }\n const oldValue = get.call(target, key);\n target.set(key, value);\n if (!hadKey) {\n trigger(target, \"add\", key, value);\n } else if (hasChanged(value, oldValue)) {\n trigger(target, \"set\", key, value, oldValue);\n }\n return this;\n },\n delete(key) {\n const target = toRaw(this);\n const { has, get } = getProto(target);\n let hadKey = has.call(target, key);\n if (!hadKey) {\n key = toRaw(key);\n hadKey = has.call(target, key);\n } else if (!!(process.env.NODE_ENV !== \"production\")) {\n checkIdentityKeys(target, has, key);\n }\n const oldValue = get ? get.call(target, key) : void 0;\n const result = target.delete(key);\n if (hadKey) {\n trigger(target, \"delete\", key, void 0, oldValue);\n }\n return result;\n },\n clear() {\n const target = toRaw(this);\n const hadItems = target.size !== 0;\n const oldTarget = !!(process.env.NODE_ENV !== \"production\") ? isMap(target) ? new Map(target) : new Set(target) : void 0;\n const result = target.clear();\n if (hadItems) {\n trigger(\n target,\n \"clear\",\n void 0,\n void 0,\n oldTarget\n );\n }\n return result;\n }\n }\n );\n const iteratorMethods = [\n \"keys\",\n \"values\",\n \"entries\",\n Symbol.iterator\n ];\n iteratorMethods.forEach((method) => {\n instrumentations[method] = createIterableMethod(method, readonly, shallow);\n });\n return instrumentations;\n}\nfunction createInstrumentationGetter(isReadonly2, shallow) {\n const instrumentations = createInstrumentations(isReadonly2, shallow);\n return (target, key, receiver) => {\n if (key === \"__v_isReactive\") {\n return !isReadonly2;\n } else if (key === \"__v_isReadonly\") {\n return isReadonly2;\n } else if (key === \"__v_raw\") {\n return target;\n }\n return Reflect.get(\n hasOwn(instrumentations, key) && key in target ? instrumentations : target,\n key,\n receiver\n );\n };\n}\nconst mutableCollectionHandlers = {\n get: /* @__PURE__ */ createInstrumentationGetter(false, false)\n};\nconst shallowCollectionHandlers = {\n get: /* @__PURE__ */ createInstrumentationGetter(false, true)\n};\nconst readonlyCollectionHandlers = {\n get: /* @__PURE__ */ createInstrumentationGetter(true, false)\n};\nconst shallowReadonlyCollectionHandlers = {\n get: /* @__PURE__ */ createInstrumentationGetter(true, true)\n};\nfunction checkIdentityKeys(target, has, key) {\n const rawKey = toRaw(key);\n if (rawKey !== key && has.call(target, rawKey)) {\n const type = toRawType(target);\n warn(\n `Reactive ${type} contains both the raw and reactive versions of the same object${type === `Map` ? ` as keys` : ``}, which can lead to inconsistencies. Avoid differentiating between the raw and reactive versions of an object and only use the reactive version if possible.`\n );\n }\n}\n\nconst reactiveMap = /* @__PURE__ */ new WeakMap();\nconst shallowReactiveMap = /* @__PURE__ */ new WeakMap();\nconst readonlyMap = /* @__PURE__ */ new WeakMap();\nconst shallowReadonlyMap = /* @__PURE__ */ new WeakMap();\nfunction targetTypeMap(rawType) {\n switch (rawType) {\n case \"Object\":\n case \"Array\":\n return 1 /* COMMON */;\n case \"Map\":\n case \"Set\":\n case \"WeakMap\":\n case \"WeakSet\":\n return 2 /* COLLECTION */;\n default:\n return 0 /* INVALID */;\n }\n}\nfunction getTargetType(value) {\n return value[\"__v_skip\"] || !Object.isExtensible(value) ? 0 /* INVALID */ : targetTypeMap(toRawType(value));\n}\nfunction reactive(target) {\n if (isReadonly(target)) {\n return target;\n }\n return createReactiveObject(\n target,\n false,\n mutableHandlers,\n mutableCollectionHandlers,\n reactiveMap\n );\n}\nfunction shallowReactive(target) {\n return createReactiveObject(\n target,\n false,\n shallowReactiveHandlers,\n shallowCollectionHandlers,\n shallowReactiveMap\n );\n}\nfunction readonly(target) {\n return createReactiveObject(\n target,\n true,\n readonlyHandlers,\n readonlyCollectionHandlers,\n readonlyMap\n );\n}\nfunction shallowReadonly(target) {\n return createReactiveObject(\n target,\n true,\n shallowReadonlyHandlers,\n shallowReadonlyCollectionHandlers,\n shallowReadonlyMap\n );\n}\nfunction createReactiveObject(target, isReadonly2, baseHandlers, collectionHandlers, proxyMap) {\n if (!isObject(target)) {\n if (!!(process.env.NODE_ENV !== \"production\")) {\n warn(\n `value cannot be made ${isReadonly2 ? \"readonly\" : \"reactive\"}: ${String(\n target\n )}`\n );\n }\n return target;\n }\n if (target[\"__v_raw\"] && !(isReadonly2 && target[\"__v_isReactive\"])) {\n return target;\n }\n const targetType = getTargetType(target);\n if (targetType === 0 /* INVALID */) {\n return target;\n }\n const existingProxy = proxyMap.get(target);\n if (existingProxy) {\n return existingProxy;\n }\n const proxy = new Proxy(\n target,\n targetType === 2 /* COLLECTION */ ? collectionHandlers : baseHandlers\n );\n proxyMap.set(target, proxy);\n return proxy;\n}\nfunction isReactive(value) {\n if (isReadonly(value)) {\n return isReactive(value[\"__v_raw\"]);\n }\n return !!(value && value[\"__v_isReactive\"]);\n}\nfunction isReadonly(value) {\n return !!(value && value[\"__v_isReadonly\"]);\n}\nfunction isShallow(value) {\n return !!(value && value[\"__v_isShallow\"]);\n}\nfunction isProxy(value) {\n return value ? !!value[\"__v_raw\"] : false;\n}\nfunction toRaw(observed) {\n const raw = observed && observed[\"__v_raw\"];\n return raw ? toRaw(raw) : observed;\n}\nfunction markRaw(value) {\n if (!hasOwn(value, \"__v_skip\") && Object.isExtensible(value)) {\n def(value, \"__v_skip\", true);\n }\n return value;\n}\nconst toReactive = (value) => isObject(value) ? reactive(value) : value;\nconst toReadonly = (value) => isObject(value) ? readonly(value) : value;\n\nfunction isRef(r) {\n return r ? r[\"__v_isRef\"] === true : false;\n}\nfunction ref(value) {\n return createRef(value, false);\n}\nfunction shallowRef(value) {\n return createRef(value, true);\n}\nfunction createRef(rawValue, shallow) {\n if (isRef(rawValue)) {\n return rawValue;\n }\n return new RefImpl(rawValue, shallow);\n}\nclass RefImpl {\n constructor(value, isShallow2) {\n this.dep = new Dep();\n this[\"__v_isRef\"] = true;\n this[\"__v_isShallow\"] = false;\n this._rawValue = isShallow2 ? value : toRaw(value);\n this._value = isShallow2 ? value : toReactive(value);\n this[\"__v_isShallow\"] = isShallow2;\n }\n get value() {\n if (!!(process.env.NODE_ENV !== \"production\")) {\n this.dep.track({\n target: this,\n type: \"get\",\n key: \"value\"\n });\n } else {\n this.dep.track();\n }\n return this._value;\n }\n set value(newValue) {\n const oldValue = this._rawValue;\n const useDirectValue = this[\"__v_isShallow\"] || isShallow(newValue) || isReadonly(newValue);\n newValue = useDirectValue ? newValue : toRaw(newValue);\n if (hasChanged(newValue, oldValue)) {\n this._rawValue = newValue;\n this._value = useDirectValue ? newValue : toReactive(newValue);\n if (!!(process.env.NODE_ENV !== \"production\")) {\n this.dep.trigger({\n target: this,\n type: \"set\",\n key: \"value\",\n newValue,\n oldValue\n });\n } else {\n this.dep.trigger();\n }\n }\n }\n}\nfunction triggerRef(ref2) {\n if (ref2.dep) {\n if (!!(process.env.NODE_ENV !== \"production\")) {\n ref2.dep.trigger({\n target: ref2,\n type: \"set\",\n key: \"value\",\n newValue: ref2._value\n });\n } else {\n ref2.dep.trigger();\n }\n }\n}\nfunction unref(ref2) {\n return isRef(ref2) ? ref2.value : ref2;\n}\nfunction toValue(source) {\n return isFunction(source) ? source() : unref(source);\n}\nconst shallowUnwrapHandlers = {\n get: (target, key, receiver) => key === \"__v_raw\" ? target : unref(Reflect.get(target, key, receiver)),\n set: (target, key, value, receiver) => {\n const oldValue = target[key];\n if (isRef(oldValue) && !isRef(value)) {\n oldValue.value = value;\n return true;\n } else {\n return Reflect.set(target, key, value, receiver);\n }\n }\n};\nfunction proxyRefs(objectWithRefs) {\n return isReactive(objectWithRefs) ? objectWithRefs : new Proxy(objectWithRefs, shallowUnwrapHandlers);\n}\nclass CustomRefImpl {\n constructor(factory) {\n this[\"__v_isRef\"] = true;\n this._value = void 0;\n const dep = this.dep = new Dep();\n const { get, set } = factory(dep.track.bind(dep), dep.trigger.bind(dep));\n this._get = get;\n this._set = set;\n }\n get value() {\n return this._value = this._get();\n }\n set value(newVal) {\n this._set(newVal);\n }\n}\nfunction customRef(factory) {\n return new CustomRefImpl(factory);\n}\nfunction toRefs(object) {\n if (!!(process.env.NODE_ENV !== \"production\") && !isProxy(object)) {\n warn(`toRefs() expects a reactive object but received a plain one.`);\n }\n const ret = isArray(object) ? new Array(object.length) : {};\n for (const key in object) {\n ret[key] = propertyToRef(object, key);\n }\n return ret;\n}\nclass ObjectRefImpl {\n constructor(_object, _key, _defaultValue) {\n this._object = _object;\n this._key = _key;\n this._defaultValue = _defaultValue;\n this[\"__v_isRef\"] = true;\n this._value = void 0;\n }\n get value() {\n const val = this._object[this._key];\n return this._value = val === void 0 ? this._defaultValue : val;\n }\n set value(newVal) {\n this._object[this._key] = newVal;\n }\n get dep() {\n return getDepFromReactive(toRaw(this._object), this._key);\n }\n}\nclass GetterRefImpl {\n constructor(_getter) {\n this._getter = _getter;\n this[\"__v_isRef\"] = true;\n this[\"__v_isReadonly\"] = true;\n this._value = void 0;\n }\n get value() {\n return this._value = this._getter();\n }\n}\nfunction toRef(source, key, defaultValue) {\n if (isRef(source)) {\n return source;\n } else if (isFunction(source)) {\n return new GetterRefImpl(source);\n } else if (isObject(source) && arguments.length > 1) {\n return propertyToRef(source, key, defaultValue);\n } else {\n return ref(source);\n }\n}\nfunction propertyToRef(source, key, defaultValue) {\n const val = source[key];\n return isRef(val) ? val : new ObjectRefImpl(source, key, defaultValue);\n}\n\nclass ComputedRefImpl {\n constructor(fn, setter, isSSR) {\n this.fn = fn;\n this.setter = setter;\n /**\n * @internal\n */\n this._value = void 0;\n /**\n * @internal\n */\n this.dep = new Dep(this);\n /**\n * @internal\n */\n this.__v_isRef = true;\n // TODO isolatedDeclarations \"__v_isReadonly\"\n // A computed is also a subscriber that tracks other deps\n /**\n * @internal\n */\n this.deps = void 0;\n /**\n * @internal\n */\n this.depsTail = void 0;\n /**\n * @internal\n */\n this.flags = 16;\n /**\n * @internal\n */\n this.globalVersion = globalVersion - 1;\n /**\n * @internal\n */\n this.next = void 0;\n // for backwards compat\n this.effect = this;\n this[\"__v_isReadonly\"] = !setter;\n this.isSSR = isSSR;\n }\n /**\n * @internal\n */\n notify() {\n this.flags |= 16;\n if (!(this.flags & 8) && // avoid infinite self recursion\n activeSub !== this) {\n batch(this, true);\n return true;\n } else if (!!(process.env.NODE_ENV !== \"production\")) ;\n }\n get value() {\n const link = !!(process.env.NODE_ENV !== \"production\") ? this.dep.track({\n target: this,\n type: \"get\",\n key: \"value\"\n }) : this.dep.track();\n refreshComputed(this);\n if (link) {\n link.version = this.dep.version;\n }\n return this._value;\n }\n set value(newValue) {\n if (this.setter) {\n this.setter(newValue);\n } else if (!!(process.env.NODE_ENV !== \"production\")) {\n warn(\"Write operation failed: computed value is readonly\");\n }\n }\n}\nfunction computed(getterOrOptions, debugOptions, isSSR = false) {\n let getter;\n let setter;\n if (isFunction(getterOrOptions)) {\n getter = getterOrOptions;\n } else {\n getter = getterOrOptions.get;\n setter = getterOrOptions.set;\n }\n const cRef = new ComputedRefImpl(getter, setter, isSSR);\n if (!!(process.env.NODE_ENV !== \"production\") && debugOptions && !isSSR) {\n cRef.onTrack = debugOptions.onTrack;\n cRef.onTrigger = debugOptions.onTrigger;\n }\n return cRef;\n}\n\nconst TrackOpTypes = {\n \"GET\": \"get\",\n \"HAS\": \"has\",\n \"ITERATE\": \"iterate\"\n};\nconst TriggerOpTypes = {\n \"SET\": \"set\",\n \"ADD\": \"add\",\n \"DELETE\": \"delete\",\n \"CLEAR\": \"clear\"\n};\nconst ReactiveFlags = {\n \"SKIP\": \"__v_skip\",\n \"IS_REACTIVE\": \"__v_isReactive\",\n \"IS_READONLY\": \"__v_isReadonly\",\n \"IS_SHALLOW\": \"__v_isShallow\",\n \"RAW\": \"__v_raw\",\n \"IS_REF\": \"__v_isRef\"\n};\n\nconst WatchErrorCodes = {\n \"WATCH_GETTER\": 2,\n \"2\": \"WATCH_GETTER\",\n \"WATCH_CALLBACK\": 3,\n \"3\": \"WATCH_CALLBACK\",\n \"WATCH_CLEANUP\": 4,\n \"4\": \"WATCH_CLEANUP\"\n};\nconst INITIAL_WATCHER_VALUE = {};\nconst cleanupMap = /* @__PURE__ */ new WeakMap();\nlet activeWatcher = void 0;\nfunction getCurrentWatcher() {\n return activeWatcher;\n}\nfunction onWatcherCleanup(cleanupFn, failSilently = false, owner = activeWatcher) {\n if (owner) {\n let cleanups = cleanupMap.get(owner);\n if (!cleanups) cleanupMap.set(owner, cleanups = []);\n cleanups.push(cleanupFn);\n } else if (!!(process.env.NODE_ENV !== \"production\") && !failSilently) {\n warn(\n `onWatcherCleanup() was called when there was no active watcher to associate with.`\n );\n }\n}\nfunction watch(source, cb, options = EMPTY_OBJ) {\n const { immediate, deep, once, scheduler, augmentJob, call } = options;\n const warnInvalidSource = (s) => {\n (options.onWarn || warn)(\n `Invalid watch source: `,\n s,\n `A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types.`\n );\n };\n const reactiveGetter = (source2) => {\n if (deep) return source2;\n if (isShallow(source2) || deep === false || deep === 0)\n return traverse(source2, 1);\n return traverse(source2);\n };\n let effect;\n let getter;\n let cleanup;\n let boundCleanup;\n let forceTrigger = false;\n let isMultiSource = false;\n if (isRef(source)) {\n getter = () => source.value;\n forceTrigger = isShallow(source);\n } else if (isReactive(source)) {\n getter = () => reactiveGetter(source);\n forceTrigger = true;\n } else if (isArray(source)) {\n isMultiSource = true;\n forceTrigger = source.some((s) => isReactive(s) || isShallow(s));\n getter = () => source.map((s) => {\n if (isRef(s)) {\n return s.value;\n } else if (isReactive(s)) {\n return reactiveGetter(s);\n } else if (isFunction(s)) {\n return call ? call(s, 2) : s();\n } else {\n !!(process.env.NODE_ENV !== \"production\") && warnInvalidSource(s);\n }\n });\n } else if (isFunction(source)) {\n if (cb) {\n getter = call ? () => call(source, 2) : source;\n } else {\n getter = () => {\n if (cleanup) {\n pauseTracking();\n try {\n cleanup();\n } finally {\n resetTracking();\n }\n }\n const currentEffect = activeWatcher;\n activeWatcher = effect;\n try {\n return call ? call(source, 3, [boundCleanup]) : source(boundCleanup);\n } finally {\n activeWatcher = currentEffect;\n }\n };\n }\n } else {\n getter = NOOP;\n !!(process.env.NODE_ENV !== \"production\") && warnInvalidSource(source);\n }\n if (cb && deep) {\n const baseGetter = getter;\n const depth = deep === true ? Infinity : deep;\n getter = () => traverse(baseGetter(), depth);\n }\n const scope = getCurrentScope();\n const watchHandle = () => {\n effect.stop();\n if (scope && scope.active) {\n remove(scope.effects, effect);\n }\n };\n if (once && cb) {\n const _cb = cb;\n cb = (...args) => {\n _cb(...args);\n watchHandle();\n };\n }\n let oldValue = isMultiSource ? new Array(source.length).fill(INITIAL_WATCHER_VALUE) : INITIAL_WATCHER_VALUE;\n const job = (immediateFirstRun) => {\n if (!(effect.flags & 1) || !effect.dirty && !immediateFirstRun) {\n return;\n }\n if (cb) {\n const newValue = effect.run();\n if (deep || forceTrigger || (isMultiSource ? newValue.some((v, i) => hasChanged(v, oldValue[i])) : hasChanged(newValue, oldValue))) {\n if (cleanup) {\n cleanup();\n }\n const currentWatcher = activeWatcher;\n activeWatcher = effect;\n try {\n const args = [\n newValue,\n // pass undefined as the old value when it's changed for the first time\n oldValue === INITIAL_WATCHER_VALUE ? void 0 : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE ? [] : oldValue,\n boundCleanup\n ];\n oldValue = newValue;\n call ? call(cb, 3, args) : (\n // @ts-expect-error\n cb(...args)\n );\n } finally {\n activeWatcher = currentWatcher;\n }\n }\n } else {\n effect.run();\n }\n };\n if (augmentJob) {\n augmentJob(job);\n }\n effect = new ReactiveEffect(getter);\n effect.scheduler = scheduler ? () => scheduler(job, false) : job;\n boundCleanup = (fn) => onWatcherCleanup(fn, false, effect);\n cleanup = effect.onStop = () => {\n const cleanups = cleanupMap.get(effect);\n if (cleanups) {\n if (call) {\n call(cleanups, 4);\n } else {\n for (const cleanup2 of cleanups) cleanup2();\n }\n cleanupMap.delete(effect);\n }\n };\n if (!!(process.env.NODE_ENV !== \"production\")) {\n effect.onTrack = options.onTrack;\n effect.onTrigger = options.onTrigger;\n }\n if (cb) {\n if (immediate) {\n job(true);\n } else {\n oldValue = effect.run();\n }\n } else if (scheduler) {\n scheduler(job.bind(null, true), true);\n } else {\n effect.run();\n }\n watchHandle.pause = effect.pause.bind(effect);\n watchHandle.resume = effect.resume.bind(effect);\n watchHandle.stop = watchHandle;\n return watchHandle;\n}\nfunction traverse(value, depth = Infinity, seen) {\n if (depth <= 0 || !isObject(value) || value[\"__v_skip\"]) {\n return value;\n }\n seen = seen || /* @__PURE__ */ new Map();\n if ((seen.get(value) || 0) >= depth) {\n return value;\n }\n seen.set(value, depth);\n depth--;\n if (isRef(value)) {\n traverse(value.value, depth, seen);\n } else if (isArray(value)) {\n for (let i = 0; i < value.length; i++) {\n traverse(value[i], depth, seen);\n }\n } else if (isSet(value) || isMap(value)) {\n value.forEach((v) => {\n traverse(v, depth, seen);\n });\n } else if (isPlainObject(value)) {\n for (const key in value) {\n traverse(value[key], depth, seen);\n }\n for (const key of Object.getOwnPropertySymbols(value)) {\n if (Object.prototype.propertyIsEnumerable.call(value, key)) {\n traverse(value[key], depth, seen);\n }\n }\n }\n return value;\n}\n\nexport { ARRAY_ITERATE_KEY, EffectFlags, EffectScope, ITERATE_KEY, MAP_KEY_ITERATE_KEY, ReactiveEffect, ReactiveFlags, TrackOpTypes, TriggerOpTypes, WatchErrorCodes, computed, customRef, effect, effectScope, enableTracking, getCurrentScope, getCurrentWatcher, isProxy, isReactive, isReadonly, isRef, isShallow, markRaw, onEffectCleanup, onScopeDispose, onWatcherCleanup, pauseTracking, proxyRefs, reactive, reactiveReadArray, readonly, ref, resetTracking, shallowReactive, shallowReadArray, shallowReadonly, shallowRef, stop, toRaw, toReactive, toReadonly, toRef, toRefs, toValue, track, traverse, trigger, triggerRef, unref, watch };\n","/**\n* @vue/shared v3.5.22\n* (c) 2018-present Yuxi (Evan) You and Vue contributors\n* @license MIT\n**/\n// @__NO_SIDE_EFFECTS__\nfunction makeMap(str) {\n const map = /* @__PURE__ */ Object.create(null);\n for (const key of str.split(\",\")) map[key] = 1;\n return (val) => val in map;\n}\n\nconst EMPTY_OBJ = !!(process.env.NODE_ENV !== \"production\") ? Object.freeze({}) : {};\nconst EMPTY_ARR = !!(process.env.NODE_ENV !== \"production\") ? Object.freeze([]) : [];\nconst NOOP = () => {\n};\nconst NO = () => false;\nconst isOn = (key) => key.charCodeAt(0) === 111 && key.charCodeAt(1) === 110 && // uppercase letter\n(key.charCodeAt(2) > 122 || key.charCodeAt(2) < 97);\nconst isModelListener = (key) => key.startsWith(\"onUpdate:\");\nconst extend = Object.assign;\nconst remove = (arr, el) => {\n const i = arr.indexOf(el);\n if (i > -1) {\n arr.splice(i, 1);\n }\n};\nconst hasOwnProperty = Object.prototype.hasOwnProperty;\nconst hasOwn = (val, key) => hasOwnProperty.call(val, key);\nconst isArray = Array.isArray;\nconst isMap = (val) => toTypeString(val) === \"[object Map]\";\nconst isSet = (val) => toTypeString(val) === \"[object Set]\";\nconst isDate = (val) => toTypeString(val) === \"[object Date]\";\nconst isRegExp = (val) => toTypeString(val) === \"[object RegExp]\";\nconst isFunction = (val) => typeof val === \"function\";\nconst isString = (val) => typeof val === \"string\";\nconst isSymbol = (val) => typeof val === \"symbol\";\nconst isObject = (val) => val !== null && typeof val === \"object\";\nconst isPromise = (val) => {\n return (isObject(val) || isFunction(val)) && isFunction(val.then) && isFunction(val.catch);\n};\nconst objectToString = Object.prototype.toString;\nconst toTypeString = (value) => objectToString.call(value);\nconst toRawType = (value) => {\n return toTypeString(value).slice(8, -1);\n};\nconst isPlainObject = (val) => toTypeString(val) === \"[object Object]\";\nconst isIntegerKey = (key) => isString(key) && key !== \"NaN\" && key[0] !== \"-\" && \"\" + parseInt(key, 10) === key;\nconst isReservedProp = /* @__PURE__ */ makeMap(\n // the leading comma is intentional so empty string \"\" is also included\n \",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted\"\n);\nconst isBuiltInDirective = /* @__PURE__ */ makeMap(\n \"bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo\"\n);\nconst cacheStringFunction = (fn) => {\n const cache = /* @__PURE__ */ Object.create(null);\n return ((str) => {\n const hit = cache[str];\n return hit || (cache[str] = fn(str));\n });\n};\nconst camelizeRE = /-\\w/g;\nconst camelize = cacheStringFunction(\n (str) => {\n return str.replace(camelizeRE, (c) => c.slice(1).toUpperCase());\n }\n);\nconst hyphenateRE = /\\B([A-Z])/g;\nconst hyphenate = cacheStringFunction(\n (str) => str.replace(hyphenateRE, \"-$1\").toLowerCase()\n);\nconst capitalize = cacheStringFunction((str) => {\n return str.charAt(0).toUpperCase() + str.slice(1);\n});\nconst toHandlerKey = cacheStringFunction(\n (str) => {\n const s = str ? `on${capitalize(str)}` : ``;\n return s;\n }\n);\nconst hasChanged = (value, oldValue) => !Object.is(value, oldValue);\nconst invokeArrayFns = (fns, ...arg) => {\n for (let i = 0; i < fns.length; i++) {\n fns[i](...arg);\n }\n};\nconst def = (obj, key, value, writable = false) => {\n Object.defineProperty(obj, key, {\n configurable: true,\n enumerable: false,\n writable,\n value\n });\n};\nconst looseToNumber = (val) => {\n const n = parseFloat(val);\n return isNaN(n) ? val : n;\n};\nconst toNumber = (val) => {\n const n = isString(val) ? Number(val) : NaN;\n return isNaN(n) ? val : n;\n};\nlet _globalThis;\nconst getGlobalThis = () => {\n return _globalThis || (_globalThis = typeof globalThis !== \"undefined\" ? globalThis : typeof self !== \"undefined\" ? self : typeof window !== \"undefined\" ? window : typeof global !== \"undefined\" ? global : {});\n};\nconst identRE = /^[_$a-zA-Z\\xA0-\\uFFFF][_$a-zA-Z0-9\\xA0-\\uFFFF]*$/;\nfunction genPropsAccessExp(name) {\n return identRE.test(name) ? `__props.${name}` : `__props[${JSON.stringify(name)}]`;\n}\nfunction genCacheKey(source, options) {\n return source + JSON.stringify(\n options,\n (_, val) => typeof val === \"function\" ? val.toString() : val\n );\n}\n\nconst PatchFlags = {\n \"TEXT\": 1,\n \"1\": \"TEXT\",\n \"CLASS\": 2,\n \"2\": \"CLASS\",\n \"STYLE\": 4,\n \"4\": \"STYLE\",\n \"PROPS\": 8,\n \"8\": \"PROPS\",\n \"FULL_PROPS\": 16,\n \"16\": \"FULL_PROPS\",\n \"NEED_HYDRATION\": 32,\n \"32\": \"NEED_HYDRATION\",\n \"STABLE_FRAGMENT\": 64,\n \"64\": \"STABLE_FRAGMENT\",\n \"KEYED_FRAGMENT\": 128,\n \"128\": \"KEYED_FRAGMENT\",\n \"UNKEYED_FRAGMENT\": 256,\n \"256\": \"UNKEYED_FRAGMENT\",\n \"NEED_PATCH\": 512,\n \"512\": \"NEED_PATCH\",\n \"DYNAMIC_SLOTS\": 1024,\n \"1024\": \"DYNAMIC_SLOTS\",\n \"DEV_ROOT_FRAGMENT\": 2048,\n \"2048\": \"DEV_ROOT_FRAGMENT\",\n \"CACHED\": -1,\n \"-1\": \"CACHED\",\n \"BAIL\": -2,\n \"-2\": \"BAIL\"\n};\nconst PatchFlagNames = {\n [1]: `TEXT`,\n [2]: `CLASS`,\n [4]: `STYLE`,\n [8]: `PROPS`,\n [16]: `FULL_PROPS`,\n [32]: `NEED_HYDRATION`,\n [64]: `STABLE_FRAGMENT`,\n [128]: `KEYED_FRAGMENT`,\n [256]: `UNKEYED_FRAGMENT`,\n [512]: `NEED_PATCH`,\n [1024]: `DYNAMIC_SLOTS`,\n [2048]: `DEV_ROOT_FRAGMENT`,\n [-1]: `CACHED`,\n [-2]: `BAIL`\n};\n\nconst ShapeFlags = {\n \"ELEMENT\": 1,\n \"1\": \"ELEMENT\",\n \"FUNCTIONAL_COMPONENT\": 2,\n \"2\": \"FUNCTIONAL_COMPONENT\",\n \"STATEFUL_COMPONENT\": 4,\n \"4\": \"STATEFUL_COMPONENT\",\n \"TEXT_CHILDREN\": 8,\n \"8\": \"TEXT_CHILDREN\",\n \"ARRAY_CHILDREN\": 16,\n \"16\": \"ARRAY_CHILDREN\",\n \"SLOTS_CHILDREN\": 32,\n \"32\": \"SLOTS_CHILDREN\",\n \"TELEPORT\": 64,\n \"64\": \"TELEPORT\",\n \"SUSPENSE\": 128,\n \"128\": \"SUSPENSE\",\n \"COMPONENT_SHOULD_KEEP_ALIVE\": 256,\n \"256\": \"COMPONENT_SHOULD_KEEP_ALIVE\",\n \"COMPONENT_KEPT_ALIVE\": 512,\n \"512\": \"COMPONENT_KEPT_ALIVE\",\n \"COMPONENT\": 6,\n \"6\": \"COMPONENT\"\n};\n\nconst SlotFlags = {\n \"STABLE\": 1,\n \"1\": \"STABLE\",\n \"DYNAMIC\": 2,\n \"2\": \"DYNAMIC\",\n \"FORWARDED\": 3,\n \"3\": \"FORWARDED\"\n};\nconst slotFlagsText = {\n [1]: \"STABLE\",\n [2]: \"DYNAMIC\",\n [3]: \"FORWARDED\"\n};\n\nconst GLOBALS_ALLOWED = \"Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,console,Error,Symbol\";\nconst isGloballyAllowed = /* @__PURE__ */ makeMap(GLOBALS_ALLOWED);\nconst isGloballyWhitelisted = isGloballyAllowed;\n\nconst range = 2;\nfunction generateCodeFrame(source, start = 0, end = source.length) {\n start = Math.max(0, Math.min(start, source.length));\n end = Math.max(0, Math.min(end, source.length));\n if (start > end) return \"\";\n let lines = source.split(/(\\r?\\n)/);\n const newlineSequences = lines.filter((_, idx) => idx % 2 === 1);\n lines = lines.filter((_, idx) => idx % 2 === 0);\n let count = 0;\n const res = [];\n for (let i = 0; i < lines.length; i++) {\n count += lines[i].length + (newlineSequences[i] && newlineSequences[i].length || 0);\n if (count >= start) {\n for (let j = i - range; j <= i + range || end > count; j++) {\n if (j < 0 || j >= lines.length) continue;\n const line = j + 1;\n res.push(\n `${line}${\" \".repeat(Math.max(3 - String(line).length, 0))}| ${lines[j]}`\n );\n const lineLength = lines[j].length;\n const newLineSeqLength = newlineSequences[j] && newlineSequences[j].length || 0;\n if (j === i) {\n const pad = start - (count - (lineLength + newLineSeqLength));\n const length = Math.max(\n 1,\n end > count ? lineLength - pad : end - start\n );\n res.push(` | ` + \" \".repeat(pad) + \"^\".repeat(length));\n } else if (j > i) {\n if (end > count) {\n const length = Math.max(Math.min(end - count, lineLength), 1);\n res.push(` | ` + \"^\".repeat(length));\n }\n count += lineLength + newLineSeqLength;\n }\n }\n break;\n }\n }\n return res.join(\"\\n\");\n}\n\nfunction normalizeStyle(value) {\n if (isArray(value)) {\n const res = {};\n for (let i = 0; i < value.length; i++) {\n const item = value[i];\n const normalized = isString(item) ? parseStringStyle(item) : normalizeStyle(item);\n if (normalized) {\n for (const key in normalized) {\n res[key] = normalized[key];\n }\n }\n }\n return res;\n } else if (isString(value) || isObject(value)) {\n return value;\n }\n}\nconst listDelimiterRE = /;(?![^(]*\\))/g;\nconst propertyDelimiterRE = /:([^]+)/;\nconst styleCommentRE = /\\/\\*[^]*?\\*\\//g;\nfunction parseStringStyle(cssText) {\n const ret = {};\n cssText.replace(styleCommentRE, \"\").split(listDelimiterRE).forEach((item) => {\n if (item) {\n const tmp = item.split(propertyDelimiterRE);\n tmp.length > 1 && (ret[tmp[0].trim()] = tmp[1].trim());\n }\n });\n return ret;\n}\nfunction stringifyStyle(styles) {\n if (!styles) return \"\";\n if (isString(styles)) return styles;\n let ret = \"\";\n for (const key in styles) {\n const value = styles[key];\n if (isString(value) || typeof value === \"number\") {\n const normalizedKey = key.startsWith(`--`) ? key : hyphenate(key);\n ret += `${normalizedKey}:${value};`;\n }\n }\n return ret;\n}\nfunction normalizeClass(value) {\n let res = \"\";\n if (isString(value)) {\n res = value;\n } else if (isArray(value)) {\n for (let i = 0; i < value.length; i++) {\n const normalized = normalizeClass(value[i]);\n if (normalized) {\n res += normalized + \" \";\n }\n }\n } else if (isObject(value)) {\n for (const name in value) {\n if (value[name]) {\n res += name + \" \";\n }\n }\n }\n return res.trim();\n}\nfunction normalizeProps(props) {\n if (!props) return null;\n let { class: klass, style } = props;\n if (klass && !isString(klass)) {\n props.class = normalizeClass(klass);\n }\n if (style) {\n props.style = normalizeStyle(style);\n }\n return props;\n}\n\nconst HTML_TAGS = \"html,body,base,head,link,meta,style,title,address,article,aside,footer,header,hgroup,h1,h2,h3,h4,h5,h6,nav,section,div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,ruby,s,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,output,progress,select,textarea,details,dialog,menu,summary,template,blockquote,iframe,tfoot\";\nconst SVG_TAGS = \"svg,animate,animateMotion,animateTransform,circle,clipPath,color-profile,defs,desc,discard,ellipse,feBlend,feColorMatrix,feComponentTransfer,feComposite,feConvolveMatrix,feDiffuseLighting,feDisplacementMap,feDistantLight,feDropShadow,feFlood,feFuncA,feFuncB,feFuncG,feFuncR,feGaussianBlur,feImage,feMerge,feMergeNode,feMorphology,feOffset,fePointLight,feSpecularLighting,feSpotLight,feTile,feTurbulence,filter,foreignObject,g,hatch,hatchpath,image,line,linearGradient,marker,mask,mesh,meshgradient,meshpatch,meshrow,metadata,mpath,path,pattern,polygon,polyline,radialGradient,rect,set,solidcolor,stop,switch,symbol,text,textPath,title,tspan,unknown,use,view\";\nconst MATH_TAGS = \"annotation,annotation-xml,maction,maligngroup,malignmark,math,menclose,merror,mfenced,mfrac,mfraction,mglyph,mi,mlabeledtr,mlongdiv,mmultiscripts,mn,mo,mover,mpadded,mphantom,mprescripts,mroot,mrow,ms,mscarries,mscarry,msgroup,msline,mspace,msqrt,msrow,mstack,mstyle,msub,msubsup,msup,mtable,mtd,mtext,mtr,munder,munderover,none,semantics\";\nconst VOID_TAGS = \"area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr\";\nconst isHTMLTag = /* @__PURE__ */ makeMap(HTML_TAGS);\nconst isSVGTag = /* @__PURE__ */ makeMap(SVG_TAGS);\nconst isMathMLTag = /* @__PURE__ */ makeMap(MATH_TAGS);\nconst isVoidTag = /* @__PURE__ */ makeMap(VOID_TAGS);\n\nconst specialBooleanAttrs = `itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly`;\nconst isSpecialBooleanAttr = /* @__PURE__ */ makeMap(specialBooleanAttrs);\nconst isBooleanAttr = /* @__PURE__ */ makeMap(\n specialBooleanAttrs + `,async,autofocus,autoplay,controls,default,defer,disabled,hidden,inert,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected`\n);\nfunction includeBooleanAttr(value) {\n return !!value || value === \"\";\n}\nconst unsafeAttrCharRE = /[>/=\"'\\u0009\\u000a\\u000c\\u0020]/;\nconst attrValidationCache = {};\nfunction isSSRSafeAttrName(name) {\n if (attrValidationCache.hasOwnProperty(name)) {\n return attrValidationCache[name];\n }\n const isUnsafe = unsafeAttrCharRE.test(name);\n if (isUnsafe) {\n console.error(`unsafe attribute name: ${name}`);\n }\n return attrValidationCache[name] = !isUnsafe;\n}\nconst propsToAttrMap = {\n acceptCharset: \"accept-charset\",\n className: \"class\",\n htmlFor: \"for\",\n httpEquiv: \"http-equiv\"\n};\nconst isKnownHtmlAttr = /* @__PURE__ */ makeMap(\n `accept,accept-charset,accesskey,action,align,allow,alt,async,autocapitalize,autocomplete,autofocus,autoplay,background,bgcolor,border,buffered,capture,challenge,charset,checked,cite,class,code,codebase,color,cols,colspan,content,contenteditable,contextmenu,controls,coords,crossorigin,csp,data,datetime,decoding,default,defer,dir,dirname,disabled,download,draggable,dropzone,enctype,enterkeyhint,for,form,formaction,formenctype,formmethod,formnovalidate,formtarget,headers,height,hidden,high,href,hreflang,http-equiv,icon,id,importance,inert,integrity,ismap,itemprop,keytype,kind,label,lang,language,loading,list,loop,low,manifest,max,maxlength,minlength,media,min,multiple,muted,name,novalidate,open,optimum,pattern,ping,placeholder,poster,preload,radiogroup,readonly,referrerpolicy,rel,required,reversed,rows,rowspan,sandbox,scope,scoped,selected,shape,size,sizes,slot,span,spellcheck,src,srcdoc,srclang,srcset,start,step,style,summary,tabindex,target,title,translate,type,usemap,value,width,wrap`\n);\nconst isKnownSvgAttr = /* @__PURE__ */ makeMap(\n `xmlns,accent-height,accumulate,additive,alignment-baseline,alphabetic,amplitude,arabic-form,ascent,attributeName,attributeType,azimuth,baseFrequency,baseline-shift,baseProfile,bbox,begin,bias,by,calcMode,cap-height,class,clip,clipPathUnits,clip-path,clip-rule,color,color-interpolation,color-interpolation-filters,color-profile,color-rendering,contentScriptType,contentStyleType,crossorigin,cursor,cx,cy,d,decelerate,descent,diffuseConstant,direction,display,divisor,dominant-baseline,dur,dx,dy,edgeMode,elevation,enable-background,end,exponent,fill,fill-opacity,fill-rule,filter,filterRes,filterUnits,flood-color,flood-opacity,font-family,font-size,font-size-adjust,font-stretch,font-style,font-variant,font-weight,format,from,fr,fx,fy,g1,g2,glyph-name,glyph-orientation-horizontal,glyph-orientation-vertical,glyphRef,gradientTransform,gradientUnits,hanging,height,href,hreflang,horiz-adv-x,horiz-origin-x,id,ideographic,image-rendering,in,in2,intercept,k,k1,k2,k3,k4,kernelMatrix,kernelUnitLength,kerning,keyPoints,keySplines,keyTimes,lang,lengthAdjust,letter-spacing,lighting-color,limitingConeAngle,local,marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mask,maskContentUnits,maskUnits,mathematical,max,media,method,min,mode,name,numOctaves,offset,opacity,operator,order,orient,orientation,origin,overflow,overline-position,overline-thickness,panose-1,paint-order,path,pathLength,patternContentUnits,patternTransform,patternUnits,ping,pointer-events,points,pointsAtX,pointsAtY,pointsAtZ,preserveAlpha,preserveAspectRatio,primitiveUnits,r,radius,referrerPolicy,refX,refY,rel,rendering-intent,repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,result,rotate,rx,ry,scale,seed,shape-rendering,slope,spacing,specularConstant,specularExponent,speed,spreadMethod,startOffset,stdDeviation,stemh,stemv,stitchTiles,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,string,stroke,stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,stroke-width,style,surfaceScale,systemLanguage,tabindex,tableValues,target,targetX,targetY,text-anchor,text-decoration,text-rendering,textLength,to,transform,transform-origin,type,u1,u2,underline-position,underline-thickness,unicode,unicode-bidi,unicode-range,units-per-em,v-alphabetic,v-hanging,v-ideographic,v-mathematical,values,vector-effect,version,vert-adv-y,vert-origin-x,vert-origin-y,viewBox,viewTarget,visibility,width,widths,word-spacing,writing-mode,x,x-height,x1,x2,xChannelSelector,xlink:actuate,xlink:arcrole,xlink:href,xlink:role,xlink:show,xlink:title,xlink:type,xmlns:xlink,xml:base,xml:lang,xml:space,y,y1,y2,yChannelSelector,z,zoomAndPan`\n);\nconst isKnownMathMLAttr = /* @__PURE__ */ makeMap(\n `accent,accentunder,actiontype,align,alignmentscope,altimg,altimg-height,altimg-valign,altimg-width,alttext,bevelled,close,columnsalign,columnlines,columnspan,denomalign,depth,dir,display,displaystyle,encoding,equalcolumns,equalrows,fence,fontstyle,fontweight,form,frame,framespacing,groupalign,height,href,id,indentalign,indentalignfirst,indentalignlast,indentshift,indentshiftfirst,indentshiftlast,indextype,justify,largetop,largeop,lquote,lspace,mathbackground,mathcolor,mathsize,mathvariant,maxsize,minlabelspacing,mode,other,overflow,position,rowalign,rowlines,rowspan,rquote,rspace,scriptlevel,scriptminsize,scriptsizemultiplier,selection,separator,separators,shift,side,src,stackalign,stretchy,subscriptshift,superscriptshift,symmetric,voffset,width,widths,xlink:href,xlink:show,xlink:type,xmlns`\n);\nfunction isRenderableAttrValue(value) {\n if (value == null) {\n return false;\n }\n const type = typeof value;\n return type === \"string\" || type === \"number\" || type === \"boolean\";\n}\n\nconst escapeRE = /[\"'&<>]/;\nfunction escapeHtml(string) {\n const str = \"\" + string;\n const match = escapeRE.exec(str);\n if (!match) {\n return str;\n }\n let html = \"\";\n let escaped;\n let index;\n let lastIndex = 0;\n for (index = match.index; index < str.length; index++) {\n switch (str.charCodeAt(index)) {\n case 34:\n escaped = \""\";\n break;\n case 38:\n escaped = \"&\";\n break;\n case 39:\n escaped = \"'\";\n break;\n case 60:\n escaped = \"<\";\n break;\n case 62:\n escaped = \">\";\n break;\n default:\n continue;\n }\n if (lastIndex !== index) {\n html += str.slice(lastIndex, index);\n }\n lastIndex = index + 1;\n html += escaped;\n }\n return lastIndex !== index ? html + str.slice(lastIndex, index) : html;\n}\nconst commentStripRE = /^-?>||--!>|?@[\\\\\\]^`{|}~]/g;\nfunction getEscapedCssVarName(key, doubleEscape) {\n return key.replace(\n cssVarNameEscapeSymbolsRE,\n (s) => doubleEscape ? s === '\"' ? '\\\\\\\\\\\\\"' : `\\\\\\\\${s}` : `\\\\${s}`\n );\n}\n\nfunction looseCompareArrays(a, b) {\n if (a.length !== b.length) return false;\n let equal = true;\n for (let i = 0; equal && i < a.length; i++) {\n equal = looseEqual(a[i], b[i]);\n }\n return equal;\n}\nfunction looseEqual(a, b) {\n if (a === b) return true;\n let aValidType = isDate(a);\n let bValidType = isDate(b);\n if (aValidType || bValidType) {\n return aValidType && bValidType ? a.getTime() === b.getTime() : false;\n }\n aValidType = isSymbol(a);\n bValidType = isSymbol(b);\n if (aValidType || bValidType) {\n return a === b;\n }\n aValidType = isArray(a);\n bValidType = isArray(b);\n if (aValidType || bValidType) {\n return aValidType && bValidType ? looseCompareArrays(a, b) : false;\n }\n aValidType = isObject(a);\n bValidType = isObject(b);\n if (aValidType || bValidType) {\n if (!aValidType || !bValidType) {\n return false;\n }\n const aKeysCount = Object.keys(a).length;\n const bKeysCount = Object.keys(b).length;\n if (aKeysCount !== bKeysCount) {\n return false;\n }\n for (const key in a) {\n const aHasKey = a.hasOwnProperty(key);\n const bHasKey = b.hasOwnProperty(key);\n if (aHasKey && !bHasKey || !aHasKey && bHasKey || !looseEqual(a[key], b[key])) {\n return false;\n }\n }\n }\n return String(a) === String(b);\n}\nfunction looseIndexOf(arr, val) {\n return arr.findIndex((item) => looseEqual(item, val));\n}\n\nconst isRef = (val) => {\n return !!(val && val[\"__v_isRef\"] === true);\n};\nconst toDisplayString = (val) => {\n return isString(val) ? val : val == null ? \"\" : isArray(val) || isObject(val) && (val.toString === objectToString || !isFunction(val.toString)) ? isRef(val) ? toDisplayString(val.value) : JSON.stringify(val, replacer, 2) : String(val);\n};\nconst replacer = (_key, val) => {\n if (isRef(val)) {\n return replacer(_key, val.value);\n } else if (isMap(val)) {\n return {\n [`Map(${val.size})`]: [...val.entries()].reduce(\n (entries, [key, val2], i) => {\n entries[stringifySymbol(key, i) + \" =>\"] = val2;\n return entries;\n },\n {}\n )\n };\n } else if (isSet(val)) {\n return {\n [`Set(${val.size})`]: [...val.values()].map((v) => stringifySymbol(v))\n };\n } else if (isSymbol(val)) {\n return stringifySymbol(val);\n } else if (isObject(val) && !isArray(val) && !isPlainObject(val)) {\n return String(val);\n }\n return val;\n};\nconst stringifySymbol = (v, i = \"\") => {\n var _a;\n return (\n // Symbol.description in es2019+ so we need to cast here to pass\n // the lib: es2016 check\n isSymbol(v) ? `Symbol(${(_a = v.description) != null ? _a : i})` : v\n );\n};\n\nfunction normalizeCssVarValue(value) {\n if (value == null) {\n return \"initial\";\n }\n if (typeof value === \"string\") {\n return value === \"\" ? \" \" : value;\n }\n if (typeof value !== \"number\" || !Number.isFinite(value)) {\n if (!!(process.env.NODE_ENV !== \"production\")) {\n console.warn(\n \"[Vue warn] Invalid value used for CSS binding. Expected a string or a finite number but received:\",\n value\n );\n }\n }\n return String(value);\n}\n\nexport { EMPTY_ARR, EMPTY_OBJ, NO, NOOP, PatchFlagNames, PatchFlags, ShapeFlags, SlotFlags, camelize, capitalize, cssVarNameEscapeSymbolsRE, def, escapeHtml, escapeHtmlComment, extend, genCacheKey, genPropsAccessExp, generateCodeFrame, getEscapedCssVarName, getGlobalThis, hasChanged, hasOwn, hyphenate, includeBooleanAttr, invokeArrayFns, isArray, isBooleanAttr, isBuiltInDirective, isDate, isFunction, isGloballyAllowed, isGloballyWhitelisted, isHTMLTag, isIntegerKey, isKnownHtmlAttr, isKnownMathMLAttr, isKnownSvgAttr, isMap, isMathMLTag, isModelListener, isObject, isOn, isPlainObject, isPromise, isRegExp, isRenderableAttrValue, isReservedProp, isSSRSafeAttrName, isSVGTag, isSet, isSpecialBooleanAttr, isString, isSymbol, isVoidTag, looseEqual, looseIndexOf, looseToNumber, makeMap, normalizeClass, normalizeCssVarValue, normalizeProps, normalizeStyle, objectToString, parseStringStyle, propsToAttrMap, remove, slotFlagsText, stringifyStyle, toDisplayString, toHandlerKey, toNumber, toRawType, toTypeString };\n","/**\n* @vue/runtime-core v3.5.22\n* (c) 2018-present Yuxi (Evan) You and Vue contributors\n* @license MIT\n**/\nimport { pauseTracking, resetTracking, isRef, toRaw, traverse, shallowRef, readonly, isReactive, ref, isShallow, isReadonly, shallowReadArray, toReadonly, toReactive, shallowReadonly, track, reactive, shallowReactive, trigger, ReactiveEffect, watch as watch$1, customRef, isProxy, proxyRefs, markRaw, EffectScope, computed as computed$1 } from '@vue/reactivity';\nexport { EffectScope, ReactiveEffect, TrackOpTypes, TriggerOpTypes, customRef, effect, effectScope, getCurrentScope, getCurrentWatcher, isProxy, isReactive, isReadonly, isRef, isShallow, markRaw, onScopeDispose, onWatcherCleanup, proxyRefs, reactive, readonly, ref, shallowReactive, shallowReadonly, shallowRef, stop, toRaw, toRef, toRefs, toValue, triggerRef, unref } from '@vue/reactivity';\nimport { isString, isFunction, EMPTY_OBJ, isPromise, isArray, NOOP, getGlobalThis, extend, isBuiltInDirective, NO, hasOwn, remove, def, isOn, isReservedProp, normalizeClass, stringifyStyle, normalizeStyle, isKnownSvgAttr, isBooleanAttr, isKnownHtmlAttr, includeBooleanAttr, isRenderableAttrValue, normalizeCssVarValue, getEscapedCssVarName, isObject, isRegExp, invokeArrayFns, toHandlerKey, camelize, capitalize, isSymbol, isGloballyAllowed, EMPTY_ARR, hyphenate, makeMap, toRawType, hasChanged, looseToNumber, isModelListener, toNumber } from '@vue/shared';\nexport { camelize, capitalize, normalizeClass, normalizeProps, normalizeStyle, toDisplayString, toHandlerKey } from '@vue/shared';\n\nconst stack = [];\nfunction pushWarningContext(vnode) {\n stack.push(vnode);\n}\nfunction popWarningContext() {\n stack.pop();\n}\nlet isWarning = false;\nfunction warn$1(msg, ...args) {\n if (isWarning) return;\n isWarning = true;\n pauseTracking();\n const instance = stack.length ? stack[stack.length - 1].component : null;\n const appWarnHandler = instance && instance.appContext.config.warnHandler;\n const trace = getComponentTrace();\n if (appWarnHandler) {\n callWithErrorHandling(\n appWarnHandler,\n instance,\n 11,\n [\n // eslint-disable-next-line no-restricted-syntax\n msg + args.map((a) => {\n var _a, _b;\n return (_b = (_a = a.toString) == null ? void 0 : _a.call(a)) != null ? _b : JSON.stringify(a);\n }).join(\"\"),\n instance && instance.proxy,\n trace.map(\n ({ vnode }) => `at <${formatComponentName(instance, vnode.type)}>`\n ).join(\"\\n\"),\n trace\n ]\n );\n } else {\n const warnArgs = [`[Vue warn]: ${msg}`, ...args];\n if (trace.length && // avoid spamming console during tests\n true) {\n warnArgs.push(`\n`, ...formatTrace(trace));\n }\n console.warn(...warnArgs);\n }\n resetTracking();\n isWarning = false;\n}\nfunction getComponentTrace() {\n let currentVNode = stack[stack.length - 1];\n if (!currentVNode) {\n return [];\n }\n const normalizedStack = [];\n while (currentVNode) {\n const last = normalizedStack[0];\n if (last && last.vnode === currentVNode) {\n last.recurseCount++;\n } else {\n normalizedStack.push({\n vnode: currentVNode,\n recurseCount: 0\n });\n }\n const parentInstance = currentVNode.component && currentVNode.component.parent;\n currentVNode = parentInstance && parentInstance.vnode;\n }\n return normalizedStack;\n}\nfunction formatTrace(trace) {\n const logs = [];\n trace.forEach((entry, i) => {\n logs.push(...i === 0 ? [] : [`\n`], ...formatTraceEntry(entry));\n });\n return logs;\n}\nfunction formatTraceEntry({ vnode, recurseCount }) {\n const postfix = recurseCount > 0 ? `... (${recurseCount} recursive calls)` : ``;\n const isRoot = vnode.component ? vnode.component.parent == null : false;\n const open = ` at <${formatComponentName(\n vnode.component,\n vnode.type,\n isRoot\n )}`;\n const close = `>` + postfix;\n return vnode.props ? [open, ...formatProps(vnode.props), close] : [open + close];\n}\nfunction formatProps(props) {\n const res = [];\n const keys = Object.keys(props);\n keys.slice(0, 3).forEach((key) => {\n res.push(...formatProp(key, props[key]));\n });\n if (keys.length > 3) {\n res.push(` ...`);\n }\n return res;\n}\nfunction formatProp(key, value, raw) {\n if (isString(value)) {\n value = JSON.stringify(value);\n return raw ? value : [`${key}=${value}`];\n } else if (typeof value === \"number\" || typeof value === \"boolean\" || value == null) {\n return raw ? value : [`${key}=${value}`];\n } else if (isRef(value)) {\n value = formatProp(key, toRaw(value.value), true);\n return raw ? value : [`${key}=Ref<`, value, `>`];\n } else if (isFunction(value)) {\n return [`${key}=fn${value.name ? `<${value.name}>` : ``}`];\n } else {\n value = toRaw(value);\n return raw ? value : [`${key}=`, value];\n }\n}\nfunction assertNumber(val, type) {\n if (!!!(process.env.NODE_ENV !== \"production\")) return;\n if (val === void 0) {\n return;\n } else if (typeof val !== \"number\") {\n warn$1(`${type} is not a valid number - got ${JSON.stringify(val)}.`);\n } else if (isNaN(val)) {\n warn$1(`${type} is NaN - the duration expression might be incorrect.`);\n }\n}\n\nconst ErrorCodes = {\n \"SETUP_FUNCTION\": 0,\n \"0\": \"SETUP_FUNCTION\",\n \"RENDER_FUNCTION\": 1,\n \"1\": \"RENDER_FUNCTION\",\n \"NATIVE_EVENT_HANDLER\": 5,\n \"5\": \"NATIVE_EVENT_HANDLER\",\n \"COMPONENT_EVENT_HANDLER\": 6,\n \"6\": \"COMPONENT_EVENT_HANDLER\",\n \"VNODE_HOOK\": 7,\n \"7\": \"VNODE_HOOK\",\n \"DIRECTIVE_HOOK\": 8,\n \"8\": \"DIRECTIVE_HOOK\",\n \"TRANSITION_HOOK\": 9,\n \"9\": \"TRANSITION_HOOK\",\n \"APP_ERROR_HANDLER\": 10,\n \"10\": \"APP_ERROR_HANDLER\",\n \"APP_WARN_HANDLER\": 11,\n \"11\": \"APP_WARN_HANDLER\",\n \"FUNCTION_REF\": 12,\n \"12\": \"FUNCTION_REF\",\n \"ASYNC_COMPONENT_LOADER\": 13,\n \"13\": \"ASYNC_COMPONENT_LOADER\",\n \"SCHEDULER\": 14,\n \"14\": \"SCHEDULER\",\n \"COMPONENT_UPDATE\": 15,\n \"15\": \"COMPONENT_UPDATE\",\n \"APP_UNMOUNT_CLEANUP\": 16,\n \"16\": \"APP_UNMOUNT_CLEANUP\"\n};\nconst ErrorTypeStrings$1 = {\n [\"sp\"]: \"serverPrefetch hook\",\n [\"bc\"]: \"beforeCreate hook\",\n [\"c\"]: \"created hook\",\n [\"bm\"]: \"beforeMount hook\",\n [\"m\"]: \"mounted hook\",\n [\"bu\"]: \"beforeUpdate hook\",\n [\"u\"]: \"updated\",\n [\"bum\"]: \"beforeUnmount hook\",\n [\"um\"]: \"unmounted hook\",\n [\"a\"]: \"activated hook\",\n [\"da\"]: \"deactivated hook\",\n [\"ec\"]: \"errorCaptured hook\",\n [\"rtc\"]: \"renderTracked hook\",\n [\"rtg\"]: \"renderTriggered hook\",\n [0]: \"setup function\",\n [1]: \"render function\",\n [2]: \"watcher getter\",\n [3]: \"watcher callback\",\n [4]: \"watcher cleanup function\",\n [5]: \"native event handler\",\n [6]: \"component event handler\",\n [7]: \"vnode hook\",\n [8]: \"directive hook\",\n [9]: \"transition hook\",\n [10]: \"app errorHandler\",\n [11]: \"app warnHandler\",\n [12]: \"ref function\",\n [13]: \"async component loader\",\n [14]: \"scheduler flush\",\n [15]: \"component update\",\n [16]: \"app unmount cleanup function\"\n};\nfunction callWithErrorHandling(fn, instance, type, args) {\n try {\n return args ? fn(...args) : fn();\n } catch (err) {\n handleError(err, instance, type);\n }\n}\nfunction callWithAsyncErrorHandling(fn, instance, type, args) {\n if (isFunction(fn)) {\n const res = callWithErrorHandling(fn, instance, type, args);\n if (res && isPromise(res)) {\n res.catch((err) => {\n handleError(err, instance, type);\n });\n }\n return res;\n }\n if (isArray(fn)) {\n const values = [];\n for (let i = 0; i < fn.length; i++) {\n values.push(callWithAsyncErrorHandling(fn[i], instance, type, args));\n }\n return values;\n } else if (!!(process.env.NODE_ENV !== \"production\")) {\n warn$1(\n `Invalid value type passed to callWithAsyncErrorHandling(): ${typeof fn}`\n );\n }\n}\nfunction handleError(err, instance, type, throwInDev = true) {\n const contextVNode = instance ? instance.vnode : null;\n const { errorHandler, throwUnhandledErrorInProduction } = instance && instance.appContext.config || EMPTY_OBJ;\n if (instance) {\n let cur = instance.parent;\n const exposedInstance = instance.proxy;\n const errorInfo = !!(process.env.NODE_ENV !== \"production\") ? ErrorTypeStrings$1[type] : `https://vuejs.org/error-reference/#runtime-${type}`;\n while (cur) {\n const errorCapturedHooks = cur.ec;\n if (errorCapturedHooks) {\n for (let i = 0; i < errorCapturedHooks.length; i++) {\n if (errorCapturedHooks[i](err, exposedInstance, errorInfo) === false) {\n return;\n }\n }\n }\n cur = cur.parent;\n }\n if (errorHandler) {\n pauseTracking();\n callWithErrorHandling(errorHandler, null, 10, [\n err,\n exposedInstance,\n errorInfo\n ]);\n resetTracking();\n return;\n }\n }\n logError(err, type, contextVNode, throwInDev, throwUnhandledErrorInProduction);\n}\nfunction logError(err, type, contextVNode, throwInDev = true, throwInProd = false) {\n if (!!(process.env.NODE_ENV !== \"production\")) {\n const info = ErrorTypeStrings$1[type];\n if (contextVNode) {\n pushWarningContext(contextVNode);\n }\n warn$1(`Unhandled error${info ? ` during execution of ${info}` : ``}`);\n if (contextVNode) {\n popWarningContext();\n }\n if (throwInDev) {\n throw err;\n } else {\n console.error(err);\n }\n } else if (throwInProd) {\n throw err;\n } else {\n console.error(err);\n }\n}\n\nconst queue = [];\nlet flushIndex = -1;\nconst pendingPostFlushCbs = [];\nlet activePostFlushCbs = null;\nlet postFlushIndex = 0;\nconst resolvedPromise = /* @__PURE__ */ Promise.resolve();\nlet currentFlushPromise = null;\nconst RECURSION_LIMIT = 100;\nfunction nextTick(fn) {\n const p = currentFlushPromise || resolvedPromise;\n return fn ? p.then(this ? fn.bind(this) : fn) : p;\n}\nfunction findInsertionIndex(id) {\n let start = flushIndex + 1;\n let end = queue.length;\n while (start < end) {\n const middle = start + end >>> 1;\n const middleJob = queue[middle];\n const middleJobId = getId(middleJob);\n if (middleJobId < id || middleJobId === id && middleJob.flags & 2) {\n start = middle + 1;\n } else {\n end = middle;\n }\n }\n return start;\n}\nfunction queueJob(job) {\n if (!(job.flags & 1)) {\n const jobId = getId(job);\n const lastJob = queue[queue.length - 1];\n if (!lastJob || // fast path when the job id is larger than the tail\n !(job.flags & 2) && jobId >= getId(lastJob)) {\n queue.push(job);\n } else {\n queue.splice(findInsertionIndex(jobId), 0, job);\n }\n job.flags |= 1;\n queueFlush();\n }\n}\nfunction queueFlush() {\n if (!currentFlushPromise) {\n currentFlushPromise = resolvedPromise.then(flushJobs);\n }\n}\nfunction queuePostFlushCb(cb) {\n if (!isArray(cb)) {\n if (activePostFlushCbs && cb.id === -1) {\n activePostFlushCbs.splice(postFlushIndex + 1, 0, cb);\n } else if (!(cb.flags & 1)) {\n pendingPostFlushCbs.push(cb);\n cb.flags |= 1;\n }\n } else {\n pendingPostFlushCbs.push(...cb);\n }\n queueFlush();\n}\nfunction flushPreFlushCbs(instance, seen, i = flushIndex + 1) {\n if (!!(process.env.NODE_ENV !== \"production\")) {\n seen = seen || /* @__PURE__ */ new Map();\n }\n for (; i < queue.length; i++) {\n const cb = queue[i];\n if (cb && cb.flags & 2) {\n if (instance && cb.id !== instance.uid) {\n continue;\n }\n if (!!(process.env.NODE_ENV !== \"production\") && checkRecursiveUpdates(seen, cb)) {\n continue;\n }\n queue.splice(i, 1);\n i--;\n if (cb.flags & 4) {\n cb.flags &= -2;\n }\n cb();\n if (!(cb.flags & 4)) {\n cb.flags &= -2;\n }\n }\n }\n}\nfunction flushPostFlushCbs(seen) {\n if (pendingPostFlushCbs.length) {\n const deduped = [...new Set(pendingPostFlushCbs)].sort(\n (a, b) => getId(a) - getId(b)\n );\n pendingPostFlushCbs.length = 0;\n if (activePostFlushCbs) {\n activePostFlushCbs.push(...deduped);\n return;\n }\n activePostFlushCbs = deduped;\n if (!!(process.env.NODE_ENV !== \"production\")) {\n seen = seen || /* @__PURE__ */ new Map();\n }\n for (postFlushIndex = 0; postFlushIndex < activePostFlushCbs.length; postFlushIndex++) {\n const cb = activePostFlushCbs[postFlushIndex];\n if (!!(process.env.NODE_ENV !== \"production\") && checkRecursiveUpdates(seen, cb)) {\n continue;\n }\n if (cb.flags & 4) {\n cb.flags &= -2;\n }\n if (!(cb.flags & 8)) cb();\n cb.flags &= -2;\n }\n activePostFlushCbs = null;\n postFlushIndex = 0;\n }\n}\nconst getId = (job) => job.id == null ? job.flags & 2 ? -1 : Infinity : job.id;\nfunction flushJobs(seen) {\n if (!!(process.env.NODE_ENV !== \"production\")) {\n seen = seen || /* @__PURE__ */ new Map();\n }\n const check = !!(process.env.NODE_ENV !== \"production\") ? (job) => checkRecursiveUpdates(seen, job) : NOOP;\n try {\n for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {\n const job = queue[flushIndex];\n if (job && !(job.flags & 8)) {\n if (!!(process.env.NODE_ENV !== \"production\") && check(job)) {\n continue;\n }\n if (job.flags & 4) {\n job.flags &= ~1;\n }\n callWithErrorHandling(\n job,\n job.i,\n job.i ? 15 : 14\n );\n if (!(job.flags & 4)) {\n job.flags &= ~1;\n }\n }\n }\n } finally {\n for (; flushIndex < queue.length; flushIndex++) {\n const job = queue[flushIndex];\n if (job) {\n job.flags &= -2;\n }\n }\n flushIndex = -1;\n queue.length = 0;\n flushPostFlushCbs(seen);\n currentFlushPromise = null;\n if (queue.length || pendingPostFlushCbs.length) {\n flushJobs(seen);\n }\n }\n}\nfunction checkRecursiveUpdates(seen, fn) {\n const count = seen.get(fn) || 0;\n if (count > RECURSION_LIMIT) {\n const instance = fn.i;\n const componentName = instance && getComponentName(instance.type);\n handleError(\n `Maximum recursive updates exceeded${componentName ? ` in component <${componentName}>` : ``}. This means you have a reactive effect that is mutating its own dependencies and thus recursively triggering itself. Possible sources include component template, render function, updated hook or watcher source function.`,\n null,\n 10\n );\n return true;\n }\n seen.set(fn, count + 1);\n return false;\n}\n\nlet isHmrUpdating = false;\nconst hmrDirtyComponents = /* @__PURE__ */ new Map();\nif (!!(process.env.NODE_ENV !== \"production\")) {\n getGlobalThis().__VUE_HMR_RUNTIME__ = {\n createRecord: tryWrap(createRecord),\n rerender: tryWrap(rerender),\n reload: tryWrap(reload)\n };\n}\nconst map = /* @__PURE__ */ new Map();\nfunction registerHMR(instance) {\n const id = instance.type.__hmrId;\n let record = map.get(id);\n if (!record) {\n createRecord(id, instance.type);\n record = map.get(id);\n }\n record.instances.add(instance);\n}\nfunction unregisterHMR(instance) {\n map.get(instance.type.__hmrId).instances.delete(instance);\n}\nfunction createRecord(id, initialDef) {\n if (map.has(id)) {\n return false;\n }\n map.set(id, {\n initialDef: normalizeClassComponent(initialDef),\n instances: /* @__PURE__ */ new Set()\n });\n return true;\n}\nfunction normalizeClassComponent(component) {\n return isClassComponent(component) ? component.__vccOpts : component;\n}\nfunction rerender(id, newRender) {\n const record = map.get(id);\n if (!record) {\n return;\n }\n record.initialDef.render = newRender;\n [...record.instances].forEach((instance) => {\n if (newRender) {\n instance.render = newRender;\n normalizeClassComponent(instance.type).render = newRender;\n }\n instance.renderCache = [];\n isHmrUpdating = true;\n if (!(instance.job.flags & 8)) {\n instance.update();\n }\n isHmrUpdating = false;\n });\n}\nfunction reload(id, newComp) {\n const record = map.get(id);\n if (!record) return;\n newComp = normalizeClassComponent(newComp);\n updateComponentDef(record.initialDef, newComp);\n const instances = [...record.instances];\n for (let i = 0; i < instances.length; i++) {\n const instance = instances[i];\n const oldComp = normalizeClassComponent(instance.type);\n let dirtyInstances = hmrDirtyComponents.get(oldComp);\n if (!dirtyInstances) {\n if (oldComp !== record.initialDef) {\n updateComponentDef(oldComp, newComp);\n }\n hmrDirtyComponents.set(oldComp, dirtyInstances = /* @__PURE__ */ new Set());\n }\n dirtyInstances.add(instance);\n instance.appContext.propsCache.delete(instance.type);\n instance.appContext.emitsCache.delete(instance.type);\n instance.appContext.optionsCache.delete(instance.type);\n if (instance.ceReload) {\n dirtyInstances.add(instance);\n instance.ceReload(newComp.styles);\n dirtyInstances.delete(instance);\n } else if (instance.parent) {\n queueJob(() => {\n if (!(instance.job.flags & 8)) {\n isHmrUpdating = true;\n instance.parent.update();\n isHmrUpdating = false;\n dirtyInstances.delete(instance);\n }\n });\n } else if (instance.appContext.reload) {\n instance.appContext.reload();\n } else if (typeof window !== \"undefined\") {\n window.location.reload();\n } else {\n console.warn(\n \"[HMR] Root or manually mounted instance modified. Full reload required.\"\n );\n }\n if (instance.root.ce && instance !== instance.root) {\n instance.root.ce._removeChildStyle(oldComp);\n }\n }\n queuePostFlushCb(() => {\n hmrDirtyComponents.clear();\n });\n}\nfunction updateComponentDef(oldComp, newComp) {\n extend(oldComp, newComp);\n for (const key in oldComp) {\n if (key !== \"__file\" && !(key in newComp)) {\n delete oldComp[key];\n }\n }\n}\nfunction tryWrap(fn) {\n return (id, arg) => {\n try {\n return fn(id, arg);\n } catch (e) {\n console.error(e);\n console.warn(\n `[HMR] Something went wrong during Vue component hot-reload. Full reload required.`\n );\n }\n };\n}\n\nlet devtools$1;\nlet buffer = [];\nlet devtoolsNotInstalled = false;\nfunction emit$1(event, ...args) {\n if (devtools$1) {\n devtools$1.emit(event, ...args);\n } else if (!devtoolsNotInstalled) {\n buffer.push({ event, args });\n }\n}\nfunction setDevtoolsHook$1(hook, target) {\n var _a, _b;\n devtools$1 = hook;\n if (devtools$1) {\n devtools$1.enabled = true;\n buffer.forEach(({ event, args }) => devtools$1.emit(event, ...args));\n buffer = [];\n } else if (\n // handle late devtools injection - only do this if we are in an actual\n // browser environment to avoid the timer handle stalling test runner exit\n // (#4815)\n typeof window !== \"undefined\" && // some envs mock window but not fully\n window.HTMLElement && // also exclude jsdom\n // eslint-disable-next-line no-restricted-syntax\n !((_b = (_a = window.navigator) == null ? void 0 : _a.userAgent) == null ? void 0 : _b.includes(\"jsdom\"))\n ) {\n const replay = target.__VUE_DEVTOOLS_HOOK_REPLAY__ = target.__VUE_DEVTOOLS_HOOK_REPLAY__ || [];\n replay.push((newHook) => {\n setDevtoolsHook$1(newHook, target);\n });\n setTimeout(() => {\n if (!devtools$1) {\n target.__VUE_DEVTOOLS_HOOK_REPLAY__ = null;\n devtoolsNotInstalled = true;\n buffer = [];\n }\n }, 3e3);\n } else {\n devtoolsNotInstalled = true;\n buffer = [];\n }\n}\nfunction devtoolsInitApp(app, version) {\n emit$1(\"app:init\" /* APP_INIT */, app, version, {\n Fragment,\n Text,\n Comment,\n Static\n });\n}\nfunction devtoolsUnmountApp(app) {\n emit$1(\"app:unmount\" /* APP_UNMOUNT */, app);\n}\nconst devtoolsComponentAdded = /* @__PURE__ */ createDevtoolsComponentHook(\"component:added\" /* COMPONENT_ADDED */);\nconst devtoolsComponentUpdated = /* @__PURE__ */ createDevtoolsComponentHook(\"component:updated\" /* COMPONENT_UPDATED */);\nconst _devtoolsComponentRemoved = /* @__PURE__ */ createDevtoolsComponentHook(\n \"component:removed\" /* COMPONENT_REMOVED */\n);\nconst devtoolsComponentRemoved = (component) => {\n if (devtools$1 && typeof devtools$1.cleanupBuffer === \"function\" && // remove the component if it wasn't buffered\n !devtools$1.cleanupBuffer(component)) {\n _devtoolsComponentRemoved(component);\n }\n};\n// @__NO_SIDE_EFFECTS__\nfunction createDevtoolsComponentHook(hook) {\n return (component) => {\n emit$1(\n hook,\n component.appContext.app,\n component.uid,\n component.parent ? component.parent.uid : void 0,\n component\n );\n };\n}\nconst devtoolsPerfStart = /* @__PURE__ */ createDevtoolsPerformanceHook(\"perf:start\" /* PERFORMANCE_START */);\nconst devtoolsPerfEnd = /* @__PURE__ */ createDevtoolsPerformanceHook(\"perf:end\" /* PERFORMANCE_END */);\nfunction createDevtoolsPerformanceHook(hook) {\n return (component, type, time) => {\n emit$1(hook, component.appContext.app, component.uid, component, type, time);\n };\n}\nfunction devtoolsComponentEmit(component, event, params) {\n emit$1(\n \"component:emit\" /* COMPONENT_EMIT */,\n component.appContext.app,\n component,\n event,\n params\n );\n}\n\nlet currentRenderingInstance = null;\nlet currentScopeId = null;\nfunction setCurrentRenderingInstance(instance) {\n const prev = currentRenderingInstance;\n currentRenderingInstance = instance;\n currentScopeId = instance && instance.type.__scopeId || null;\n return prev;\n}\nfunction pushScopeId(id) {\n currentScopeId = id;\n}\nfunction popScopeId() {\n currentScopeId = null;\n}\nconst withScopeId = (_id) => withCtx;\nfunction withCtx(fn, ctx = currentRenderingInstance, isNonScopedSlot) {\n if (!ctx) return fn;\n if (fn._n) {\n return fn;\n }\n const renderFnWithContext = (...args) => {\n if (renderFnWithContext._d) {\n setBlockTracking(-1);\n }\n const prevInstance = setCurrentRenderingInstance(ctx);\n let res;\n try {\n res = fn(...args);\n } finally {\n setCurrentRenderingInstance(prevInstance);\n if (renderFnWithContext._d) {\n setBlockTracking(1);\n }\n }\n if (!!(process.env.NODE_ENV !== \"production\") || __VUE_PROD_DEVTOOLS__) {\n devtoolsComponentUpdated(ctx);\n }\n return res;\n };\n renderFnWithContext._n = true;\n renderFnWithContext._c = true;\n renderFnWithContext._d = true;\n return renderFnWithContext;\n}\n\nfunction validateDirectiveName(name) {\n if (isBuiltInDirective(name)) {\n warn$1(\"Do not use built-in directive ids as custom directive id: \" + name);\n }\n}\nfunction withDirectives(vnode, directives) {\n if (currentRenderingInstance === null) {\n !!(process.env.NODE_ENV !== \"production\") && warn$1(`withDirectives can only be used inside render functions.`);\n return vnode;\n }\n const instance = getComponentPublicInstance(currentRenderingInstance);\n const bindings = vnode.dirs || (vnode.dirs = []);\n for (let i = 0; i < directives.length; i++) {\n let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i];\n if (dir) {\n if (isFunction(dir)) {\n dir = {\n mounted: dir,\n updated: dir\n };\n }\n if (dir.deep) {\n traverse(value);\n }\n bindings.push({\n dir,\n instance,\n value,\n oldValue: void 0,\n arg,\n modifiers\n });\n }\n }\n return vnode;\n}\nfunction invokeDirectiveHook(vnode, prevVNode, instance, name) {\n const bindings = vnode.dirs;\n const oldBindings = prevVNode && prevVNode.dirs;\n for (let i = 0; i < bindings.length; i++) {\n const binding = bindings[i];\n if (oldBindings) {\n binding.oldValue = oldBindings[i].value;\n }\n let hook = binding.dir[name];\n if (hook) {\n pauseTracking();\n callWithAsyncErrorHandling(hook, instance, 8, [\n vnode.el,\n binding,\n vnode,\n prevVNode\n ]);\n resetTracking();\n }\n }\n}\n\nconst TeleportEndKey = Symbol(\"_vte\");\nconst isTeleport = (type) => type.__isTeleport;\nconst isTeleportDisabled = (props) => props && (props.disabled || props.disabled === \"\");\nconst isTeleportDeferred = (props) => props && (props.defer || props.defer === \"\");\nconst isTargetSVG = (target) => typeof SVGElement !== \"undefined\" && target instanceof SVGElement;\nconst isTargetMathML = (target) => typeof MathMLElement === \"function\" && target instanceof MathMLElement;\nconst resolveTarget = (props, select) => {\n const targetSelector = props && props.to;\n if (isString(targetSelector)) {\n if (!select) {\n !!(process.env.NODE_ENV !== \"production\") && warn$1(\n `Current renderer does not support string target for Teleports. (missing querySelector renderer option)`\n );\n return null;\n } else {\n const target = select(targetSelector);\n if (!!(process.env.NODE_ENV !== \"production\") && !target && !isTeleportDisabled(props)) {\n warn$1(\n `Failed to locate Teleport target with selector \"${targetSelector}\". Note the target element must exist before the component is mounted - i.e. the target cannot be rendered by the component itself, and ideally should be outside of the entire Vue component tree.`\n );\n }\n return target;\n }\n } else {\n if (!!(process.env.NODE_ENV !== \"production\") && !targetSelector && !isTeleportDisabled(props)) {\n warn$1(`Invalid Teleport target: ${targetSelector}`);\n }\n return targetSelector;\n }\n};\nconst TeleportImpl = {\n name: \"Teleport\",\n __isTeleport: true,\n process(n1, n2, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized, internals) {\n const {\n mc: mountChildren,\n pc: patchChildren,\n pbc: patchBlockChildren,\n o: { insert, querySelector, createText, createComment }\n } = internals;\n const disabled = isTeleportDisabled(n2.props);\n let { shapeFlag, children, dynamicChildren } = n2;\n if (!!(process.env.NODE_ENV !== \"production\") && isHmrUpdating) {\n optimized = false;\n dynamicChildren = null;\n }\n if (n1 == null) {\n const placeholder = n2.el = !!(process.env.NODE_ENV !== \"production\") ? createComment(\"teleport start\") : createText(\"\");\n const mainAnchor = n2.anchor = !!(process.env.NODE_ENV !== \"production\") ? createComment(\"teleport end\") : createText(\"\");\n insert(placeholder, container, anchor);\n insert(mainAnchor, container, anchor);\n const mount = (container2, anchor2) => {\n if (shapeFlag & 16) {\n mountChildren(\n children,\n container2,\n anchor2,\n parentComponent,\n parentSuspense,\n namespace,\n slotScopeIds,\n optimized\n );\n }\n };\n const mountToTarget = () => {\n const target = n2.target = resolveTarget(n2.props, querySelector);\n const targetAnchor = prepareAnchor(target, n2, createText, insert);\n if (target) {\n if (namespace !== \"svg\" && isTargetSVG(target)) {\n namespace = \"svg\";\n } else if (namespace !== \"mathml\" && isTargetMathML(target)) {\n namespace = \"mathml\";\n }\n if (parentComponent && parentComponent.isCE) {\n (parentComponent.ce._teleportTargets || (parentComponent.ce._teleportTargets = /* @__PURE__ */ new Set())).add(target);\n }\n if (!disabled) {\n mount(target, targetAnchor);\n updateCssVars(n2, false);\n }\n } else if (!!(process.env.NODE_ENV !== \"production\") && !disabled) {\n warn$1(\n \"Invalid Teleport target on mount:\",\n target,\n `(${typeof target})`\n );\n }\n };\n if (disabled) {\n mount(container, mainAnchor);\n updateCssVars(n2, true);\n }\n if (isTeleportDeferred(n2.props)) {\n n2.el.__isMounted = false;\n queuePostRenderEffect(() => {\n mountToTarget();\n delete n2.el.__isMounted;\n }, parentSuspense);\n } else {\n mountToTarget();\n }\n } else {\n if (isTeleportDeferred(n2.props) && n1.el.__isMounted === false) {\n queuePostRenderEffect(() => {\n TeleportImpl.process(\n n1,\n n2,\n container,\n anchor,\n parentComponent,\n parentSuspense,\n namespace,\n slotScopeIds,\n optimized,\n internals\n );\n }, parentSuspense);\n return;\n }\n n2.el = n1.el;\n n2.targetStart = n1.targetStart;\n const mainAnchor = n2.anchor = n1.anchor;\n const target = n2.target = n1.target;\n const targetAnchor = n2.targetAnchor = n1.targetAnchor;\n const wasDisabled = isTeleportDisabled(n1.props);\n const currentContainer = wasDisabled ? container : target;\n const currentAnchor = wasDisabled ? mainAnchor : targetAnchor;\n if (namespace === \"svg\" || isTargetSVG(target)) {\n namespace = \"svg\";\n } else if (namespace === \"mathml\" || isTargetMathML(target)) {\n namespace = \"mathml\";\n }\n if (dynamicChildren) {\n patchBlockChildren(\n n1.dynamicChildren,\n dynamicChildren,\n currentContainer,\n parentComponent,\n parentSuspense,\n namespace,\n slotScopeIds\n );\n traverseStaticChildren(n1, n2, !!!(process.env.NODE_ENV !== \"production\"));\n } else if (!optimized) {\n patchChildren(\n n1,\n n2,\n currentContainer,\n currentAnchor,\n parentComponent,\n parentSuspense,\n namespace,\n slotScopeIds,\n false\n );\n }\n if (disabled) {\n if (!wasDisabled) {\n moveTeleport(\n n2,\n container,\n mainAnchor,\n internals,\n 1\n );\n } else {\n if (n2.props && n1.props && n2.props.to !== n1.props.to) {\n n2.props.to = n1.props.to;\n }\n }\n } else {\n if ((n2.props && n2.props.to) !== (n1.props && n1.props.to)) {\n const nextTarget = n2.target = resolveTarget(\n n2.props,\n querySelector\n );\n if (nextTarget) {\n moveTeleport(\n n2,\n nextTarget,\n null,\n internals,\n 0\n );\n } else if (!!(process.env.NODE_ENV !== \"production\")) {\n warn$1(\n \"Invalid Teleport target on update:\",\n target,\n `(${typeof target})`\n );\n }\n } else if (wasDisabled) {\n moveTeleport(\n n2,\n target,\n targetAnchor,\n internals,\n 1\n );\n }\n }\n updateCssVars(n2, disabled);\n }\n },\n remove(vnode, parentComponent, parentSuspense, { um: unmount, o: { remove: hostRemove } }, doRemove) {\n const {\n shapeFlag,\n children,\n anchor,\n targetStart,\n targetAnchor,\n target,\n props\n } = vnode;\n if (target) {\n hostRemove(targetStart);\n hostRemove(targetAnchor);\n }\n doRemove && hostRemove(anchor);\n if (shapeFlag & 16) {\n const shouldRemove = doRemove || !isTeleportDisabled(props);\n for (let i = 0; i < children.length; i++) {\n const child = children[i];\n unmount(\n child,\n parentComponent,\n parentSuspense,\n shouldRemove,\n !!child.dynamicChildren\n );\n }\n }\n },\n move: moveTeleport,\n hydrate: hydrateTeleport\n};\nfunction moveTeleport(vnode, container, parentAnchor, { o: { insert }, m: move }, moveType = 2) {\n if (moveType === 0) {\n insert(vnode.targetAnchor, container, parentAnchor);\n }\n const { el, anchor, shapeFlag, children, props } = vnode;\n const isReorder = moveType === 2;\n if (isReorder) {\n insert(el, container, parentAnchor);\n }\n if (!isReorder || isTeleportDisabled(props)) {\n if (shapeFlag & 16) {\n for (let i = 0; i < children.length; i++) {\n move(\n children[i],\n container,\n parentAnchor,\n 2\n );\n }\n }\n }\n if (isReorder) {\n insert(anchor, container, parentAnchor);\n }\n}\nfunction hydrateTeleport(node, vnode, parentComponent, parentSuspense, slotScopeIds, optimized, {\n o: { nextSibling, parentNode, querySelector, insert, createText }\n}, hydrateChildren) {\n function hydrateDisabledTeleport(node2, vnode2, targetStart, targetAnchor) {\n vnode2.anchor = hydrateChildren(\n nextSibling(node2),\n vnode2,\n parentNode(node2),\n parentComponent,\n parentSuspense,\n slotScopeIds,\n optimized\n );\n vnode2.targetStart = targetStart;\n vnode2.targetAnchor = targetAnchor;\n }\n const target = vnode.target = resolveTarget(\n vnode.props,\n querySelector\n );\n const disabled = isTeleportDisabled(vnode.props);\n if (target) {\n const targetNode = target._lpa || target.firstChild;\n if (vnode.shapeFlag & 16) {\n if (disabled) {\n hydrateDisabledTeleport(\n node,\n vnode,\n targetNode,\n targetNode && nextSibling(targetNode)\n );\n } else {\n vnode.anchor = nextSibling(node);\n let targetAnchor = targetNode;\n while (targetAnchor) {\n if (targetAnchor && targetAnchor.nodeType === 8) {\n if (targetAnchor.data === \"teleport start anchor\") {\n vnode.targetStart = targetAnchor;\n } else if (targetAnchor.data === \"teleport anchor\") {\n vnode.targetAnchor = targetAnchor;\n target._lpa = vnode.targetAnchor && nextSibling(vnode.targetAnchor);\n break;\n }\n }\n targetAnchor = nextSibling(targetAnchor);\n }\n if (!vnode.targetAnchor) {\n prepareAnchor(target, vnode, createText, insert);\n }\n hydrateChildren(\n targetNode && nextSibling(targetNode),\n vnode,\n target,\n parentComponent,\n parentSuspense,\n slotScopeIds,\n optimized\n );\n }\n }\n updateCssVars(vnode, disabled);\n } else if (disabled) {\n if (vnode.shapeFlag & 16) {\n hydrateDisabledTeleport(node, vnode, node, nextSibling(node));\n }\n }\n return vnode.anchor && nextSibling(vnode.anchor);\n}\nconst Teleport = TeleportImpl;\nfunction updateCssVars(vnode, isDisabled) {\n const ctx = vnode.ctx;\n if (ctx && ctx.ut) {\n let node, anchor;\n if (isDisabled) {\n node = vnode.el;\n anchor = vnode.anchor;\n } else {\n node = vnode.targetStart;\n anchor = vnode.targetAnchor;\n }\n while (node && node !== anchor) {\n if (node.nodeType === 1) node.setAttribute(\"data-v-owner\", ctx.uid);\n node = node.nextSibling;\n }\n ctx.ut();\n }\n}\nfunction prepareAnchor(target, vnode, createText, insert) {\n const targetStart = vnode.targetStart = createText(\"\");\n const targetAnchor = vnode.targetAnchor = createText(\"\");\n targetStart[TeleportEndKey] = targetAnchor;\n if (target) {\n insert(targetStart, target);\n insert(targetAnchor, target);\n }\n return targetAnchor;\n}\n\nconst leaveCbKey = Symbol(\"_leaveCb\");\nconst enterCbKey = Symbol(\"_enterCb\");\nfunction useTransitionState() {\n const state = {\n isMounted: false,\n isLeaving: false,\n isUnmounting: false,\n leavingVNodes: /* @__PURE__ */ new Map()\n };\n onMounted(() => {\n state.isMounted = true;\n });\n onBeforeUnmount(() => {\n state.isUnmounting = true;\n });\n return state;\n}\nconst TransitionHookValidator = [Function, Array];\nconst BaseTransitionPropsValidators = {\n mode: String,\n appear: Boolean,\n persisted: Boolean,\n // enter\n onBeforeEnter: TransitionHookValidator,\n onEnter: TransitionHookValidator,\n onAfterEnter: TransitionHookValidator,\n onEnterCancelled: TransitionHookValidator,\n // leave\n onBeforeLeave: TransitionHookValidator,\n onLeave: TransitionHookValidator,\n onAfterLeave: TransitionHookValidator,\n onLeaveCancelled: TransitionHookValidator,\n // appear\n onBeforeAppear: TransitionHookValidator,\n onAppear: TransitionHookValidator,\n onAfterAppear: TransitionHookValidator,\n onAppearCancelled: TransitionHookValidator\n};\nconst recursiveGetSubtree = (instance) => {\n const subTree = instance.subTree;\n return subTree.component ? recursiveGetSubtree(subTree.component) : subTree;\n};\nconst BaseTransitionImpl = {\n name: `BaseTransition`,\n props: BaseTransitionPropsValidators,\n setup(props, { slots }) {\n const instance = getCurrentInstance();\n const state = useTransitionState();\n return () => {\n const children = slots.default && getTransitionRawChildren(slots.default(), true);\n if (!children || !children.length) {\n return;\n }\n const child = findNonCommentChild(children);\n const rawProps = toRaw(props);\n const { mode } = rawProps;\n if (!!(process.env.NODE_ENV !== \"production\") && mode && mode !== \"in-out\" && mode !== \"out-in\" && mode !== \"default\") {\n warn$1(`invalid mode: ${mode}`);\n }\n if (state.isLeaving) {\n return emptyPlaceholder(child);\n }\n const innerChild = getInnerChild$1(child);\n if (!innerChild) {\n return emptyPlaceholder(child);\n }\n let enterHooks = resolveTransitionHooks(\n innerChild,\n rawProps,\n state,\n instance,\n // #11061, ensure enterHooks is fresh after clone\n (hooks) => enterHooks = hooks\n );\n if (innerChild.type !== Comment) {\n setTransitionHooks(innerChild, enterHooks);\n }\n let oldInnerChild = instance.subTree && getInnerChild$1(instance.subTree);\n if (oldInnerChild && oldInnerChild.type !== Comment && !isSameVNodeType(oldInnerChild, innerChild) && recursiveGetSubtree(instance).type !== Comment) {\n let leavingHooks = resolveTransitionHooks(\n oldInnerChild,\n rawProps,\n state,\n instance\n );\n setTransitionHooks(oldInnerChild, leavingHooks);\n if (mode === \"out-in\" && innerChild.type !== Comment) {\n state.isLeaving = true;\n leavingHooks.afterLeave = () => {\n state.isLeaving = false;\n if (!(instance.job.flags & 8)) {\n instance.update();\n }\n delete leavingHooks.afterLeave;\n oldInnerChild = void 0;\n };\n return emptyPlaceholder(child);\n } else if (mode === \"in-out\" && innerChild.type !== Comment) {\n leavingHooks.delayLeave = (el, earlyRemove, delayedLeave) => {\n const leavingVNodesCache = getLeavingNodesForType(\n state,\n oldInnerChild\n );\n leavingVNodesCache[String(oldInnerChild.key)] = oldInnerChild;\n el[leaveCbKey] = () => {\n earlyRemove();\n el[leaveCbKey] = void 0;\n delete enterHooks.delayedLeave;\n oldInnerChild = void 0;\n };\n enterHooks.delayedLeave = () => {\n delayedLeave();\n delete enterHooks.delayedLeave;\n oldInnerChild = void 0;\n };\n };\n } else {\n oldInnerChild = void 0;\n }\n } else if (oldInnerChild) {\n oldInnerChild = void 0;\n }\n return child;\n };\n }\n};\nfunction findNonCommentChild(children) {\n let child = children[0];\n if (children.length > 1) {\n let hasFound = false;\n for (const c of children) {\n if (c.type !== Comment) {\n if (!!(process.env.NODE_ENV !== \"production\") && hasFound) {\n warn$1(\n \" can only be used on a single element or component. Use for lists.\"\n );\n break;\n }\n child = c;\n hasFound = true;\n if (!!!(process.env.NODE_ENV !== \"production\")) break;\n }\n }\n }\n return child;\n}\nconst BaseTransition = BaseTransitionImpl;\nfunction getLeavingNodesForType(state, vnode) {\n const { leavingVNodes } = state;\n let leavingVNodesCache = leavingVNodes.get(vnode.type);\n if (!leavingVNodesCache) {\n leavingVNodesCache = /* @__PURE__ */ Object.create(null);\n leavingVNodes.set(vnode.type, leavingVNodesCache);\n }\n return leavingVNodesCache;\n}\nfunction resolveTransitionHooks(vnode, props, state, instance, postClone) {\n const {\n appear,\n mode,\n persisted = false,\n onBeforeEnter,\n onEnter,\n onAfterEnter,\n onEnterCancelled,\n onBeforeLeave,\n onLeave,\n onAfterLeave,\n onLeaveCancelled,\n onBeforeAppear,\n onAppear,\n onAfterAppear,\n onAppearCancelled\n } = props;\n const key = String(vnode.key);\n const leavingVNodesCache = getLeavingNodesForType(state, vnode);\n const callHook = (hook, args) => {\n hook && callWithAsyncErrorHandling(\n hook,\n instance,\n 9,\n args\n );\n };\n const callAsyncHook = (hook, args) => {\n const done = args[1];\n callHook(hook, args);\n if (isArray(hook)) {\n if (hook.every((hook2) => hook2.length <= 1)) done();\n } else if (hook.length <= 1) {\n done();\n }\n };\n const hooks = {\n mode,\n persisted,\n beforeEnter(el) {\n let hook = onBeforeEnter;\n if (!state.isMounted) {\n if (appear) {\n hook = onBeforeAppear || onBeforeEnter;\n } else {\n return;\n }\n }\n if (el[leaveCbKey]) {\n el[leaveCbKey](\n true\n /* cancelled */\n );\n }\n const leavingVNode = leavingVNodesCache[key];\n if (leavingVNode && isSameVNodeType(vnode, leavingVNode) && leavingVNode.el[leaveCbKey]) {\n leavingVNode.el[leaveCbKey]();\n }\n callHook(hook, [el]);\n },\n enter(el) {\n let hook = onEnter;\n let afterHook = onAfterEnter;\n let cancelHook = onEnterCancelled;\n if (!state.isMounted) {\n if (appear) {\n hook = onAppear || onEnter;\n afterHook = onAfterAppear || onAfterEnter;\n cancelHook = onAppearCancelled || onEnterCancelled;\n } else {\n return;\n }\n }\n let called = false;\n const done = el[enterCbKey] = (cancelled) => {\n if (called) return;\n called = true;\n if (cancelled) {\n callHook(cancelHook, [el]);\n } else {\n callHook(afterHook, [el]);\n }\n if (hooks.delayedLeave) {\n hooks.delayedLeave();\n }\n el[enterCbKey] = void 0;\n };\n if (hook) {\n callAsyncHook(hook, [el, done]);\n } else {\n done();\n }\n },\n leave(el, remove) {\n const key2 = String(vnode.key);\n if (el[enterCbKey]) {\n el[enterCbKey](\n true\n /* cancelled */\n );\n }\n if (state.isUnmounting) {\n return remove();\n }\n callHook(onBeforeLeave, [el]);\n let called = false;\n const done = el[leaveCbKey] = (cancelled) => {\n if (called) return;\n called = true;\n remove();\n if (cancelled) {\n callHook(onLeaveCancelled, [el]);\n } else {\n callHook(onAfterLeave, [el]);\n }\n el[leaveCbKey] = void 0;\n if (leavingVNodesCache[key2] === vnode) {\n delete leavingVNodesCache[key2];\n }\n };\n leavingVNodesCache[key2] = vnode;\n if (onLeave) {\n callAsyncHook(onLeave, [el, done]);\n } else {\n done();\n }\n },\n clone(vnode2) {\n const hooks2 = resolveTransitionHooks(\n vnode2,\n props,\n state,\n instance,\n postClone\n );\n if (postClone) postClone(hooks2);\n return hooks2;\n }\n };\n return hooks;\n}\nfunction emptyPlaceholder(vnode) {\n if (isKeepAlive(vnode)) {\n vnode = cloneVNode(vnode);\n vnode.children = null;\n return vnode;\n }\n}\nfunction getInnerChild$1(vnode) {\n if (!isKeepAlive(vnode)) {\n if (isTeleport(vnode.type) && vnode.children) {\n return findNonCommentChild(vnode.children);\n }\n return vnode;\n }\n if (vnode.component) {\n return vnode.component.subTree;\n }\n const { shapeFlag, children } = vnode;\n if (children) {\n if (shapeFlag & 16) {\n return children[0];\n }\n if (shapeFlag & 32 && isFunction(children.default)) {\n return children.default();\n }\n }\n}\nfunction setTransitionHooks(vnode, hooks) {\n if (vnode.shapeFlag & 6 && vnode.component) {\n vnode.transition = hooks;\n setTransitionHooks(vnode.component.subTree, hooks);\n } else if (vnode.shapeFlag & 128) {\n vnode.ssContent.transition = hooks.clone(vnode.ssContent);\n vnode.ssFallback.transition = hooks.clone(vnode.ssFallback);\n } else {\n vnode.transition = hooks;\n }\n}\nfunction getTransitionRawChildren(children, keepComment = false, parentKey) {\n let ret = [];\n let keyedFragmentCount = 0;\n for (let i = 0; i < children.length; i++) {\n let child = children[i];\n const key = parentKey == null ? child.key : String(parentKey) + String(child.key != null ? child.key : i);\n if (child.type === Fragment) {\n if (child.patchFlag & 128) keyedFragmentCount++;\n ret = ret.concat(\n getTransitionRawChildren(child.children, keepComment, key)\n );\n } else if (keepComment || child.type !== Comment) {\n ret.push(key != null ? cloneVNode(child, { key }) : child);\n }\n }\n if (keyedFragmentCount > 1) {\n for (let i = 0; i < ret.length; i++) {\n ret[i].patchFlag = -2;\n }\n }\n return ret;\n}\n\n// @__NO_SIDE_EFFECTS__\nfunction defineComponent(options, extraOptions) {\n return isFunction(options) ? (\n // #8236: extend call and options.name access are considered side-effects\n // by Rollup, so we have to wrap it in a pure-annotated IIFE.\n /* @__PURE__ */ (() => extend({ name: options.name }, extraOptions, { setup: options }))()\n ) : options;\n}\n\nfunction useId() {\n const i = getCurrentInstance();\n if (i) {\n return (i.appContext.config.idPrefix || \"v\") + \"-\" + i.ids[0] + i.ids[1]++;\n } else if (!!(process.env.NODE_ENV !== \"production\")) {\n warn$1(\n `useId() is called when there is no active component instance to be associated with.`\n );\n }\n return \"\";\n}\nfunction markAsyncBoundary(instance) {\n instance.ids = [instance.ids[0] + instance.ids[2]++ + \"-\", 0, 0];\n}\n\nconst knownTemplateRefs = /* @__PURE__ */ new WeakSet();\nfunction useTemplateRef(key) {\n const i = getCurrentInstance();\n const r = shallowRef(null);\n if (i) {\n const refs = i.refs === EMPTY_OBJ ? i.refs = {} : i.refs;\n let desc;\n if (!!(process.env.NODE_ENV !== \"production\") && (desc = Object.getOwnPropertyDescriptor(refs, key)) && !desc.configurable) {\n warn$1(`useTemplateRef('${key}') already exists.`);\n } else {\n Object.defineProperty(refs, key, {\n enumerable: true,\n get: () => r.value,\n set: (val) => r.value = val\n });\n }\n } else if (!!(process.env.NODE_ENV !== \"production\")) {\n warn$1(\n `useTemplateRef() is called when there is no active component instance to be associated with.`\n );\n }\n const ret = !!(process.env.NODE_ENV !== \"production\") ? readonly(r) : r;\n if (!!(process.env.NODE_ENV !== \"production\")) {\n knownTemplateRefs.add(ret);\n }\n return ret;\n}\n\nconst pendingSetRefMap = /* @__PURE__ */ new WeakMap();\nfunction setRef(rawRef, oldRawRef, parentSuspense, vnode, isUnmount = false) {\n if (isArray(rawRef)) {\n rawRef.forEach(\n (r, i) => setRef(\n r,\n oldRawRef && (isArray(oldRawRef) ? oldRawRef[i] : oldRawRef),\n parentSuspense,\n vnode,\n isUnmount\n )\n );\n return;\n }\n if (isAsyncWrapper(vnode) && !isUnmount) {\n if (vnode.shapeFlag & 512 && vnode.type.__asyncResolved && vnode.component.subTree.component) {\n setRef(rawRef, oldRawRef, parentSuspense, vnode.component.subTree);\n }\n return;\n }\n const refValue = vnode.shapeFlag & 4 ? getComponentPublicInstance(vnode.component) : vnode.el;\n const value = isUnmount ? null : refValue;\n const { i: owner, r: ref } = rawRef;\n if (!!(process.env.NODE_ENV !== \"production\") && !owner) {\n warn$1(\n `Missing ref owner context. ref cannot be used on hoisted vnodes. A vnode with ref must be created inside the render function.`\n );\n return;\n }\n const oldRef = oldRawRef && oldRawRef.r;\n const refs = owner.refs === EMPTY_OBJ ? owner.refs = {} : owner.refs;\n const setupState = owner.setupState;\n const rawSetupState = toRaw(setupState);\n const canSetSetupRef = setupState === EMPTY_OBJ ? NO : (key) => {\n if (!!(process.env.NODE_ENV !== \"production\")) {\n if (hasOwn(rawSetupState, key) && !isRef(rawSetupState[key])) {\n warn$1(\n `Template ref \"${key}\" used on a non-ref value. It will not work in the production build.`\n );\n }\n if (knownTemplateRefs.has(rawSetupState[key])) {\n return false;\n }\n }\n return hasOwn(rawSetupState, key);\n };\n const canSetRef = (ref2) => {\n return !!!(process.env.NODE_ENV !== \"production\") || !knownTemplateRefs.has(ref2);\n };\n if (oldRef != null && oldRef !== ref) {\n invalidatePendingSetRef(oldRawRef);\n if (isString(oldRef)) {\n refs[oldRef] = null;\n if (canSetSetupRef(oldRef)) {\n setupState[oldRef] = null;\n }\n } else if (isRef(oldRef)) {\n if (canSetRef(oldRef)) {\n oldRef.value = null;\n }\n const oldRawRefAtom = oldRawRef;\n if (oldRawRefAtom.k) refs[oldRawRefAtom.k] = null;\n }\n }\n if (isFunction(ref)) {\n callWithErrorHandling(ref, owner, 12, [value, refs]);\n } else {\n const _isString = isString(ref);\n const _isRef = isRef(ref);\n if (_isString || _isRef) {\n const doSet = () => {\n if (rawRef.f) {\n const existing = _isString ? canSetSetupRef(ref) ? setupState[ref] : refs[ref] : canSetRef(ref) || !rawRef.k ? ref.value : refs[rawRef.k];\n if (isUnmount) {\n isArray(existing) && remove(existing, refValue);\n } else {\n if (!isArray(existing)) {\n if (_isString) {\n refs[ref] = [refValue];\n if (canSetSetupRef(ref)) {\n setupState[ref] = refs[ref];\n }\n } else {\n const newVal = [refValue];\n if (canSetRef(ref)) {\n ref.value = newVal;\n }\n if (rawRef.k) refs[rawRef.k] = newVal;\n }\n } else if (!existing.includes(refValue)) {\n existing.push(refValue);\n }\n }\n } else if (_isString) {\n refs[ref] = value;\n if (canSetSetupRef(ref)) {\n setupState[ref] = value;\n }\n } else if (_isRef) {\n if (canSetRef(ref)) {\n ref.value = value;\n }\n if (rawRef.k) refs[rawRef.k] = value;\n } else if (!!(process.env.NODE_ENV !== \"production\")) {\n warn$1(\"Invalid template ref type:\", ref, `(${typeof ref})`);\n }\n };\n if (value) {\n const job = () => {\n doSet();\n pendingSetRefMap.delete(rawRef);\n };\n job.id = -1;\n pendingSetRefMap.set(rawRef, job);\n queuePostRenderEffect(job, parentSuspense);\n } else {\n invalidatePendingSetRef(rawRef);\n doSet();\n }\n } else if (!!(process.env.NODE_ENV !== \"production\")) {\n warn$1(\"Invalid template ref type:\", ref, `(${typeof ref})`);\n }\n }\n}\nfunction invalidatePendingSetRef(rawRef) {\n const pendingSetRef = pendingSetRefMap.get(rawRef);\n if (pendingSetRef) {\n pendingSetRef.flags |= 8;\n pendingSetRefMap.delete(rawRef);\n }\n}\n\nlet hasLoggedMismatchError = false;\nconst logMismatchError = () => {\n if (hasLoggedMismatchError) {\n return;\n }\n console.error(\"Hydration completed but contains mismatches.\");\n hasLoggedMismatchError = true;\n};\nconst isSVGContainer = (container) => container.namespaceURI.includes(\"svg\") && container.tagName !== \"foreignObject\";\nconst isMathMLContainer = (container) => container.namespaceURI.includes(\"MathML\");\nconst getContainerType = (container) => {\n if (container.nodeType !== 1) return void 0;\n if (isSVGContainer(container)) return \"svg\";\n if (isMathMLContainer(container)) return \"mathml\";\n return void 0;\n};\nconst isComment = (node) => node.nodeType === 8;\nfunction createHydrationFunctions(rendererInternals) {\n const {\n mt: mountComponent,\n p: patch,\n o: {\n patchProp,\n createText,\n nextSibling,\n parentNode,\n remove,\n insert,\n createComment\n }\n } = rendererInternals;\n const hydrate = (vnode, container) => {\n if (!container.hasChildNodes()) {\n (!!(process.env.NODE_ENV !== \"production\") || __VUE_PROD_HYDRATION_MISMATCH_DETAILS__) && warn$1(\n `Attempting to hydrate existing markup but container is empty. Performing full mount instead.`\n );\n patch(null, vnode, container);\n flushPostFlushCbs();\n container._vnode = vnode;\n return;\n }\n hydrateNode(container.firstChild, vnode, null, null, null);\n flushPostFlushCbs();\n container._vnode = vnode;\n };\n const hydrateNode = (node, vnode, parentComponent, parentSuspense, slotScopeIds, optimized = false) => {\n optimized = optimized || !!vnode.dynamicChildren;\n const isFragmentStart = isComment(node) && node.data === \"[\";\n const onMismatch = () => handleMismatch(\n node,\n vnode,\n parentComponent,\n parentSuspense,\n slotScopeIds,\n isFragmentStart\n );\n const { type, ref, shapeFlag, patchFlag } = vnode;\n let domType = node.nodeType;\n vnode.el = node;\n if (!!(process.env.NODE_ENV !== \"production\") || __VUE_PROD_DEVTOOLS__) {\n def(node, \"__vnode\", vnode, true);\n def(node, \"__vueParentComponent\", parentComponent, true);\n }\n if (patchFlag === -2) {\n optimized = false;\n vnode.dynamicChildren = null;\n }\n let nextNode = null;\n switch (type) {\n case Text:\n if (domType !== 3) {\n if (vnode.children === \"\") {\n insert(vnode.el = createText(\"\"), parentNode(node), node);\n nextNode = node;\n } else {\n nextNode = onMismatch();\n }\n } else {\n if (node.data !== vnode.children) {\n (!!(process.env.NODE_ENV !== \"production\") || __VUE_PROD_HYDRATION_MISMATCH_DETAILS__) && warn$1(\n `Hydration text mismatch in`,\n node.parentNode,\n `\n - rendered on server: ${JSON.stringify(\n node.data\n )}\n - expected on client: ${JSON.stringify(vnode.children)}`\n );\n logMismatchError();\n node.data = vnode.children;\n }\n nextNode = nextSibling(node);\n }\n break;\n case Comment:\n if (isTemplateNode(node)) {\n nextNode = nextSibling(node);\n replaceNode(\n vnode.el = node.content.firstChild,\n node,\n parentComponent\n );\n } else if (domType !== 8 || isFragmentStart) {\n nextNode = onMismatch();\n } else {\n nextNode = nextSibling(node);\n }\n break;\n case Static:\n if (isFragmentStart) {\n node = nextSibling(node);\n domType = node.nodeType;\n }\n if (domType === 1 || domType === 3) {\n nextNode = node;\n const needToAdoptContent = !vnode.children.length;\n for (let i = 0; i < vnode.staticCount; i++) {\n if (needToAdoptContent)\n vnode.children += nextNode.nodeType === 1 ? nextNode.outerHTML : nextNode.data;\n if (i === vnode.staticCount - 1) {\n vnode.anchor = nextNode;\n }\n nextNode = nextSibling(nextNode);\n }\n return isFragmentStart ? nextSibling(nextNode) : nextNode;\n } else {\n onMismatch();\n }\n break;\n case Fragment:\n if (!isFragmentStart) {\n nextNode = onMismatch();\n } else {\n nextNode = hydrateFragment(\n node,\n vnode,\n parentComponent,\n parentSuspense,\n slotScopeIds,\n optimized\n );\n }\n break;\n default:\n if (shapeFlag & 1) {\n if ((domType !== 1 || vnode.type.toLowerCase() !== node.tagName.toLowerCase()) && !isTemplateNode(node)) {\n nextNode = onMismatch();\n } else {\n nextNode = hydrateElement(\n node,\n vnode,\n parentComponent,\n parentSuspense,\n slotScopeIds,\n optimized\n );\n }\n } else if (shapeFlag & 6) {\n vnode.slotScopeIds = slotScopeIds;\n const container = parentNode(node);\n if (isFragmentStart) {\n nextNode = locateClosingAnchor(node);\n } else if (isComment(node) && node.data === \"teleport start\") {\n nextNode = locateClosingAnchor(node, node.data, \"teleport end\");\n } else {\n nextNode = nextSibling(node);\n }\n mountComponent(\n vnode,\n container,\n null,\n parentComponent,\n parentSuspense,\n getContainerType(container),\n optimized\n );\n if (isAsyncWrapper(vnode) && !vnode.type.__asyncResolved) {\n let subTree;\n if (isFragmentStart) {\n subTree = createVNode(Fragment);\n subTree.anchor = nextNode ? nextNode.previousSibling : container.lastChild;\n } else {\n subTree = node.nodeType === 3 ? createTextVNode(\"\") : createVNode(\"div\");\n }\n subTree.el = node;\n vnode.component.subTree = subTree;\n }\n } else if (shapeFlag & 64) {\n if (domType !== 8) {\n nextNode = onMismatch();\n } else {\n nextNode = vnode.type.hydrate(\n node,\n vnode,\n parentComponent,\n parentSuspense,\n slotScopeIds,\n optimized,\n rendererInternals,\n hydrateChildren\n );\n }\n } else if (shapeFlag & 128) {\n nextNode = vnode.type.hydrate(\n node,\n vnode,\n parentComponent,\n parentSuspense,\n getContainerType(parentNode(node)),\n slotScopeIds,\n optimized,\n rendererInternals,\n hydrateNode\n );\n } else if (!!(process.env.NODE_ENV !== \"production\") || __VUE_PROD_HYDRATION_MISMATCH_DETAILS__) {\n warn$1(\"Invalid HostVNode type:\", type, `(${typeof type})`);\n }\n }\n if (ref != null) {\n setRef(ref, null, parentSuspense, vnode);\n }\n return nextNode;\n };\n const hydrateElement = (el, vnode, parentComponent, parentSuspense, slotScopeIds, optimized) => {\n optimized = optimized || !!vnode.dynamicChildren;\n const { type, props, patchFlag, shapeFlag, dirs, transition } = vnode;\n const forcePatch = type === \"input\" || type === \"option\";\n if (!!(process.env.NODE_ENV !== \"production\") || forcePatch || patchFlag !== -1) {\n if (dirs) {\n invokeDirectiveHook(vnode, null, parentComponent, \"created\");\n }\n let needCallTransitionHooks = false;\n if (isTemplateNode(el)) {\n needCallTransitionHooks = needTransition(\n null,\n // no need check parentSuspense in hydration\n transition\n ) && parentComponent && parentComponent.vnode.props && parentComponent.vnode.props.appear;\n const content = el.content.firstChild;\n if (needCallTransitionHooks) {\n const cls = content.getAttribute(\"class\");\n if (cls) content.$cls = cls;\n transition.beforeEnter(content);\n }\n replaceNode(content, el, parentComponent);\n vnode.el = el = content;\n }\n if (shapeFlag & 16 && // skip if element has innerHTML / textContent\n !(props && (props.innerHTML || props.textContent))) {\n let next = hydrateChildren(\n el.firstChild,\n vnode,\n el,\n parentComponent,\n parentSuspense,\n slotScopeIds,\n optimized\n );\n let hasWarned = false;\n while (next) {\n if (!isMismatchAllowed(el, 1 /* CHILDREN */)) {\n if ((!!(process.env.NODE_ENV !== \"production\") || __VUE_PROD_HYDRATION_MISMATCH_DETAILS__) && !hasWarned) {\n warn$1(\n `Hydration children mismatch on`,\n el,\n `\nServer rendered element contains more child nodes than client vdom.`\n );\n hasWarned = true;\n }\n logMismatchError();\n }\n const cur = next;\n next = next.nextSibling;\n remove(cur);\n }\n } else if (shapeFlag & 8) {\n let clientText = vnode.children;\n if (clientText[0] === \"\\n\" && (el.tagName === \"PRE\" || el.tagName === \"TEXTAREA\")) {\n clientText = clientText.slice(1);\n }\n if (el.textContent !== clientText) {\n if (!isMismatchAllowed(el, 0 /* TEXT */)) {\n (!!(process.env.NODE_ENV !== \"production\") || __VUE_PROD_HYDRATION_MISMATCH_DETAILS__) && warn$1(\n `Hydration text content mismatch on`,\n el,\n `\n - rendered on server: ${el.textContent}\n - expected on client: ${vnode.children}`\n );\n logMismatchError();\n }\n el.textContent = vnode.children;\n }\n }\n if (props) {\n if (!!(process.env.NODE_ENV !== \"production\") || __VUE_PROD_HYDRATION_MISMATCH_DETAILS__ || forcePatch || !optimized || patchFlag & (16 | 32)) {\n const isCustomElement = el.tagName.includes(\"-\");\n for (const key in props) {\n if ((!!(process.env.NODE_ENV !== \"production\") || __VUE_PROD_HYDRATION_MISMATCH_DETAILS__) && // #11189 skip if this node has directives that have created hooks\n // as it could have mutated the DOM in any possible way\n !(dirs && dirs.some((d) => d.dir.created)) && propHasMismatch(el, key, props[key], vnode, parentComponent)) {\n logMismatchError();\n }\n if (forcePatch && (key.endsWith(\"value\") || key === \"indeterminate\") || isOn(key) && !isReservedProp(key) || // force hydrate v-bind with .prop modifiers\n key[0] === \".\" || isCustomElement) {\n patchProp(el, key, null, props[key], void 0, parentComponent);\n }\n }\n } else if (props.onClick) {\n patchProp(\n el,\n \"onClick\",\n null,\n props.onClick,\n void 0,\n parentComponent\n );\n } else if (patchFlag & 4 && isReactive(props.style)) {\n for (const key in props.style) props.style[key];\n }\n }\n let vnodeHooks;\n if (vnodeHooks = props && props.onVnodeBeforeMount) {\n invokeVNodeHook(vnodeHooks, parentComponent, vnode);\n }\n if (dirs) {\n invokeDirectiveHook(vnode, null, parentComponent, \"beforeMount\");\n }\n if ((vnodeHooks = props && props.onVnodeMounted) || dirs || needCallTransitionHooks) {\n queueEffectWithSuspense(() => {\n vnodeHooks && invokeVNodeHook(vnodeHooks, parentComponent, vnode);\n needCallTransitionHooks && transition.enter(el);\n dirs && invokeDirectiveHook(vnode, null, parentComponent, \"mounted\");\n }, parentSuspense);\n }\n }\n return el.nextSibling;\n };\n const hydrateChildren = (node, parentVNode, container, parentComponent, parentSuspense, slotScopeIds, optimized) => {\n optimized = optimized || !!parentVNode.dynamicChildren;\n const children = parentVNode.children;\n const l = children.length;\n let hasWarned = false;\n for (let i = 0; i < l; i++) {\n const vnode = optimized ? children[i] : children[i] = normalizeVNode(children[i]);\n const isText = vnode.type === Text;\n if (node) {\n if (isText && !optimized) {\n if (i + 1 < l && normalizeVNode(children[i + 1]).type === Text) {\n insert(\n createText(\n node.data.slice(vnode.children.length)\n ),\n container,\n nextSibling(node)\n );\n node.data = vnode.children;\n }\n }\n node = hydrateNode(\n node,\n vnode,\n parentComponent,\n parentSuspense,\n slotScopeIds,\n optimized\n );\n } else if (isText && !vnode.children) {\n insert(vnode.el = createText(\"\"), container);\n } else {\n if (!isMismatchAllowed(container, 1 /* CHILDREN */)) {\n if ((!!(process.env.NODE_ENV !== \"production\") || __VUE_PROD_HYDRATION_MISMATCH_DETAILS__) && !hasWarned) {\n warn$1(\n `Hydration children mismatch on`,\n container,\n `\nServer rendered element contains fewer child nodes than client vdom.`\n );\n hasWarned = true;\n }\n logMismatchError();\n }\n patch(\n null,\n vnode,\n container,\n null,\n parentComponent,\n parentSuspense,\n getContainerType(container),\n slotScopeIds\n );\n }\n }\n return node;\n };\n const hydrateFragment = (node, vnode, parentComponent, parentSuspense, slotScopeIds, optimized) => {\n const { slotScopeIds: fragmentSlotScopeIds } = vnode;\n if (fragmentSlotScopeIds) {\n slotScopeIds = slotScopeIds ? slotScopeIds.concat(fragmentSlotScopeIds) : fragmentSlotScopeIds;\n }\n const container = parentNode(node);\n const next = hydrateChildren(\n nextSibling(node),\n vnode,\n container,\n parentComponent,\n parentSuspense,\n slotScopeIds,\n optimized\n );\n if (next && isComment(next) && next.data === \"]\") {\n return nextSibling(vnode.anchor = next);\n } else {\n logMismatchError();\n insert(vnode.anchor = createComment(`]`), container, next);\n return next;\n }\n };\n const handleMismatch = (node, vnode, parentComponent, parentSuspense, slotScopeIds, isFragment) => {\n if (!isMismatchAllowed(node.parentElement, 1 /* CHILDREN */)) {\n (!!(process.env.NODE_ENV !== \"production\") || __VUE_PROD_HYDRATION_MISMATCH_DETAILS__) && warn$1(\n `Hydration node mismatch:\n- rendered on server:`,\n node,\n node.nodeType === 3 ? `(text)` : isComment(node) && node.data === \"[\" ? `(start of fragment)` : ``,\n `\n- expected on client:`,\n vnode.type\n );\n logMismatchError();\n }\n vnode.el = null;\n if (isFragment) {\n const end = locateClosingAnchor(node);\n while (true) {\n const next2 = nextSibling(node);\n if (next2 && next2 !== end) {\n remove(next2);\n } else {\n break;\n }\n }\n }\n const next = nextSibling(node);\n const container = parentNode(node);\n remove(node);\n patch(\n null,\n vnode,\n container,\n next,\n parentComponent,\n parentSuspense,\n getContainerType(container),\n slotScopeIds\n );\n if (parentComponent) {\n parentComponent.vnode.el = vnode.el;\n updateHOCHostEl(parentComponent, vnode.el);\n }\n return next;\n };\n const locateClosingAnchor = (node, open = \"[\", close = \"]\") => {\n let match = 0;\n while (node) {\n node = nextSibling(node);\n if (node && isComment(node)) {\n if (node.data === open) match++;\n if (node.data === close) {\n if (match === 0) {\n return nextSibling(node);\n } else {\n match--;\n }\n }\n }\n }\n return node;\n };\n const replaceNode = (newNode, oldNode, parentComponent) => {\n const parentNode2 = oldNode.parentNode;\n if (parentNode2) {\n parentNode2.replaceChild(newNode, oldNode);\n }\n let parent = parentComponent;\n while (parent) {\n if (parent.vnode.el === oldNode) {\n parent.vnode.el = parent.subTree.el = newNode;\n }\n parent = parent.parent;\n }\n };\n const isTemplateNode = (node) => {\n return node.nodeType === 1 && node.tagName === \"TEMPLATE\";\n };\n return [hydrate, hydrateNode];\n}\nfunction propHasMismatch(el, key, clientValue, vnode, instance) {\n let mismatchType;\n let mismatchKey;\n let actual;\n let expected;\n if (key === \"class\") {\n if (el.$cls) {\n actual = el.$cls;\n delete el.$cls;\n } else {\n actual = el.getAttribute(\"class\");\n }\n expected = normalizeClass(clientValue);\n if (!isSetEqual(toClassSet(actual || \"\"), toClassSet(expected))) {\n mismatchType = 2 /* CLASS */;\n mismatchKey = `class`;\n }\n } else if (key === \"style\") {\n actual = el.getAttribute(\"style\") || \"\";\n expected = isString(clientValue) ? clientValue : stringifyStyle(normalizeStyle(clientValue));\n const actualMap = toStyleMap(actual);\n const expectedMap = toStyleMap(expected);\n if (vnode.dirs) {\n for (const { dir, value } of vnode.dirs) {\n if (dir.name === \"show\" && !value) {\n expectedMap.set(\"display\", \"none\");\n }\n }\n }\n if (instance) {\n resolveCssVars(instance, vnode, expectedMap);\n }\n if (!isMapEqual(actualMap, expectedMap)) {\n mismatchType = 3 /* STYLE */;\n mismatchKey = \"style\";\n }\n } else if (el instanceof SVGElement && isKnownSvgAttr(key) || el instanceof HTMLElement && (isBooleanAttr(key) || isKnownHtmlAttr(key))) {\n if (isBooleanAttr(key)) {\n actual = el.hasAttribute(key);\n expected = includeBooleanAttr(clientValue);\n } else if (clientValue == null) {\n actual = el.hasAttribute(key);\n expected = false;\n } else {\n if (el.hasAttribute(key)) {\n actual = el.getAttribute(key);\n } else if (key === \"value\" && el.tagName === \"TEXTAREA\") {\n actual = el.value;\n } else {\n actual = false;\n }\n expected = isRenderableAttrValue(clientValue) ? String(clientValue) : false;\n }\n if (actual !== expected) {\n mismatchType = 4 /* ATTRIBUTE */;\n mismatchKey = key;\n }\n }\n if (mismatchType != null && !isMismatchAllowed(el, mismatchType)) {\n const format = (v) => v === false ? `(not rendered)` : `${mismatchKey}=\"${v}\"`;\n const preSegment = `Hydration ${MismatchTypeString[mismatchType]} mismatch on`;\n const postSegment = `\n - rendered on server: ${format(actual)}\n - expected on client: ${format(expected)}\n Note: this mismatch is check-only. The DOM will not be rectified in production due to performance overhead.\n You should fix the source of the mismatch.`;\n {\n warn$1(preSegment, el, postSegment);\n }\n return true;\n }\n return false;\n}\nfunction toClassSet(str) {\n return new Set(str.trim().split(/\\s+/));\n}\nfunction isSetEqual(a, b) {\n if (a.size !== b.size) {\n return false;\n }\n for (const s of a) {\n if (!b.has(s)) {\n return false;\n }\n }\n return true;\n}\nfunction toStyleMap(str) {\n const styleMap = /* @__PURE__ */ new Map();\n for (const item of str.split(\";\")) {\n let [key, value] = item.split(\":\");\n key = key.trim();\n value = value && value.trim();\n if (key && value) {\n styleMap.set(key, value);\n }\n }\n return styleMap;\n}\nfunction isMapEqual(a, b) {\n if (a.size !== b.size) {\n return false;\n }\n for (const [key, value] of a) {\n if (value !== b.get(key)) {\n return false;\n }\n }\n return true;\n}\nfunction resolveCssVars(instance, vnode, expectedMap) {\n const root = instance.subTree;\n if (instance.getCssVars && (vnode === root || root && root.type === Fragment && root.children.includes(vnode))) {\n const cssVars = instance.getCssVars();\n for (const key in cssVars) {\n const value = normalizeCssVarValue(cssVars[key]);\n expectedMap.set(`--${getEscapedCssVarName(key, false)}`, value);\n }\n }\n if (vnode === root && instance.parent) {\n resolveCssVars(instance.parent, instance.vnode, expectedMap);\n }\n}\nconst allowMismatchAttr = \"data-allow-mismatch\";\nconst MismatchTypeString = {\n [0 /* TEXT */]: \"text\",\n [1 /* CHILDREN */]: \"children\",\n [2 /* CLASS */]: \"class\",\n [3 /* STYLE */]: \"style\",\n [4 /* ATTRIBUTE */]: \"attribute\"\n};\nfunction isMismatchAllowed(el, allowedType) {\n if (allowedType === 0 /* TEXT */ || allowedType === 1 /* CHILDREN */) {\n while (el && !el.hasAttribute(allowMismatchAttr)) {\n el = el.parentElement;\n }\n }\n const allowedAttr = el && el.getAttribute(allowMismatchAttr);\n if (allowedAttr == null) {\n return false;\n } else if (allowedAttr === \"\") {\n return true;\n } else {\n const list = allowedAttr.split(\",\");\n if (allowedType === 0 /* TEXT */ && list.includes(\"children\")) {\n return true;\n }\n return list.includes(MismatchTypeString[allowedType]);\n }\n}\n\nconst requestIdleCallback = getGlobalThis().requestIdleCallback || ((cb) => setTimeout(cb, 1));\nconst cancelIdleCallback = getGlobalThis().cancelIdleCallback || ((id) => clearTimeout(id));\nconst hydrateOnIdle = (timeout = 1e4) => (hydrate) => {\n const id = requestIdleCallback(hydrate, { timeout });\n return () => cancelIdleCallback(id);\n};\nfunction elementIsVisibleInViewport(el) {\n const { top, left, bottom, right } = el.getBoundingClientRect();\n const { innerHeight, innerWidth } = window;\n return (top > 0 && top < innerHeight || bottom > 0 && bottom < innerHeight) && (left > 0 && left < innerWidth || right > 0 && right < innerWidth);\n}\nconst hydrateOnVisible = (opts) => (hydrate, forEach) => {\n const ob = new IntersectionObserver((entries) => {\n for (const e of entries) {\n if (!e.isIntersecting) continue;\n ob.disconnect();\n hydrate();\n break;\n }\n }, opts);\n forEach((el) => {\n if (!(el instanceof Element)) return;\n if (elementIsVisibleInViewport(el)) {\n hydrate();\n ob.disconnect();\n return false;\n }\n ob.observe(el);\n });\n return () => ob.disconnect();\n};\nconst hydrateOnMediaQuery = (query) => (hydrate) => {\n if (query) {\n const mql = matchMedia(query);\n if (mql.matches) {\n hydrate();\n } else {\n mql.addEventListener(\"change\", hydrate, { once: true });\n return () => mql.removeEventListener(\"change\", hydrate);\n }\n }\n};\nconst hydrateOnInteraction = (interactions = []) => (hydrate, forEach) => {\n if (isString(interactions)) interactions = [interactions];\n let hasHydrated = false;\n const doHydrate = (e) => {\n if (!hasHydrated) {\n hasHydrated = true;\n teardown();\n hydrate();\n e.target.dispatchEvent(new e.constructor(e.type, e));\n }\n };\n const teardown = () => {\n forEach((el) => {\n for (const i of interactions) {\n el.removeEventListener(i, doHydrate);\n }\n });\n };\n forEach((el) => {\n for (const i of interactions) {\n el.addEventListener(i, doHydrate, { once: true });\n }\n });\n return teardown;\n};\nfunction forEachElement(node, cb) {\n if (isComment(node) && node.data === \"[\") {\n let depth = 1;\n let next = node.nextSibling;\n while (next) {\n if (next.nodeType === 1) {\n const result = cb(next);\n if (result === false) {\n break;\n }\n } else if (isComment(next)) {\n if (next.data === \"]\") {\n if (--depth === 0) break;\n } else if (next.data === \"[\") {\n depth++;\n }\n }\n next = next.nextSibling;\n }\n } else {\n cb(node);\n }\n}\n\nconst isAsyncWrapper = (i) => !!i.type.__asyncLoader;\n// @__NO_SIDE_EFFECTS__\nfunction defineAsyncComponent(source) {\n if (isFunction(source)) {\n source = { loader: source };\n }\n const {\n loader,\n loadingComponent,\n errorComponent,\n delay = 200,\n hydrate: hydrateStrategy,\n timeout,\n // undefined = never times out\n suspensible = true,\n onError: userOnError\n } = source;\n let pendingRequest = null;\n let resolvedComp;\n let retries = 0;\n const retry = () => {\n retries++;\n pendingRequest = null;\n return load();\n };\n const load = () => {\n let thisRequest;\n return pendingRequest || (thisRequest = pendingRequest = loader().catch((err) => {\n err = err instanceof Error ? err : new Error(String(err));\n if (userOnError) {\n return new Promise((resolve, reject) => {\n const userRetry = () => resolve(retry());\n const userFail = () => reject(err);\n userOnError(err, userRetry, userFail, retries + 1);\n });\n } else {\n throw err;\n }\n }).then((comp) => {\n if (thisRequest !== pendingRequest && pendingRequest) {\n return pendingRequest;\n }\n if (!!(process.env.NODE_ENV !== \"production\") && !comp) {\n warn$1(\n `Async component loader resolved to undefined. If you are using retry(), make sure to return its return value.`\n );\n }\n if (comp && (comp.__esModule || comp[Symbol.toStringTag] === \"Module\")) {\n comp = comp.default;\n }\n if (!!(process.env.NODE_ENV !== \"production\") && comp && !isObject(comp) && !isFunction(comp)) {\n throw new Error(`Invalid async component load result: ${comp}`);\n }\n resolvedComp = comp;\n return comp;\n }));\n };\n return defineComponent({\n name: \"AsyncComponentWrapper\",\n __asyncLoader: load,\n __asyncHydrate(el, instance, hydrate) {\n let patched = false;\n (instance.bu || (instance.bu = [])).push(() => patched = true);\n const performHydrate = () => {\n if (patched) {\n if (!!(process.env.NODE_ENV !== \"production\")) {\n warn$1(\n `Skipping lazy hydration for component '${getComponentName(resolvedComp) || resolvedComp.__file}': it was updated before lazy hydration performed.`\n );\n }\n return;\n }\n hydrate();\n };\n const doHydrate = hydrateStrategy ? () => {\n const teardown = hydrateStrategy(\n performHydrate,\n (cb) => forEachElement(el, cb)\n );\n if (teardown) {\n (instance.bum || (instance.bum = [])).push(teardown);\n }\n } : performHydrate;\n if (resolvedComp) {\n doHydrate();\n } else {\n load().then(() => !instance.isUnmounted && doHydrate());\n }\n },\n get __asyncResolved() {\n return resolvedComp;\n },\n setup() {\n const instance = currentInstance;\n markAsyncBoundary(instance);\n if (resolvedComp) {\n return () => createInnerComp(resolvedComp, instance);\n }\n const onError = (err) => {\n pendingRequest = null;\n handleError(\n err,\n instance,\n 13,\n !errorComponent\n );\n };\n if (suspensible && instance.suspense || isInSSRComponentSetup) {\n return load().then((comp) => {\n return () => createInnerComp(comp, instance);\n }).catch((err) => {\n onError(err);\n return () => errorComponent ? createVNode(errorComponent, {\n error: err\n }) : null;\n });\n }\n const loaded = ref(false);\n const error = ref();\n const delayed = ref(!!delay);\n if (delay) {\n setTimeout(() => {\n delayed.value = false;\n }, delay);\n }\n if (timeout != null) {\n setTimeout(() => {\n if (!loaded.value && !error.value) {\n const err = new Error(\n `Async component timed out after ${timeout}ms.`\n );\n onError(err);\n error.value = err;\n }\n }, timeout);\n }\n load().then(() => {\n loaded.value = true;\n if (instance.parent && isKeepAlive(instance.parent.vnode)) {\n instance.parent.update();\n }\n }).catch((err) => {\n onError(err);\n error.value = err;\n });\n return () => {\n if (loaded.value && resolvedComp) {\n return createInnerComp(resolvedComp, instance);\n } else if (error.value && errorComponent) {\n return createVNode(errorComponent, {\n error: error.value\n });\n } else if (loadingComponent && !delayed.value) {\n return createVNode(loadingComponent);\n }\n };\n }\n });\n}\nfunction createInnerComp(comp, parent) {\n const { ref: ref2, props, children, ce } = parent.vnode;\n const vnode = createVNode(comp, props, children);\n vnode.ref = ref2;\n vnode.ce = ce;\n delete parent.vnode.ce;\n return vnode;\n}\n\nconst isKeepAlive = (vnode) => vnode.type.__isKeepAlive;\nconst KeepAliveImpl = {\n name: `KeepAlive`,\n // Marker for special handling inside the renderer. We are not using a ===\n // check directly on KeepAlive in the renderer, because importing it directly\n // would prevent it from being tree-shaken.\n __isKeepAlive: true,\n props: {\n include: [String, RegExp, Array],\n exclude: [String, RegExp, Array],\n max: [String, Number]\n },\n setup(props, { slots }) {\n const instance = getCurrentInstance();\n const sharedContext = instance.ctx;\n if (!sharedContext.renderer) {\n return () => {\n const children = slots.default && slots.default();\n return children && children.length === 1 ? children[0] : children;\n };\n }\n const cache = /* @__PURE__ */ new Map();\n const keys = /* @__PURE__ */ new Set();\n let current = null;\n if (!!(process.env.NODE_ENV !== \"production\") || __VUE_PROD_DEVTOOLS__) {\n instance.__v_cache = cache;\n }\n const parentSuspense = instance.suspense;\n const {\n renderer: {\n p: patch,\n m: move,\n um: _unmount,\n o: { createElement }\n }\n } = sharedContext;\n const storageContainer = createElement(\"div\");\n sharedContext.activate = (vnode, container, anchor, namespace, optimized) => {\n const instance2 = vnode.component;\n move(vnode, container, anchor, 0, parentSuspense);\n patch(\n instance2.vnode,\n vnode,\n container,\n anchor,\n instance2,\n parentSuspense,\n namespace,\n vnode.slotScopeIds,\n optimized\n );\n queuePostRenderEffect(() => {\n instance2.isDeactivated = false;\n if (instance2.a) {\n invokeArrayFns(instance2.a);\n }\n const vnodeHook = vnode.props && vnode.props.onVnodeMounted;\n if (vnodeHook) {\n invokeVNodeHook(vnodeHook, instance2.parent, vnode);\n }\n }, parentSuspense);\n if (!!(process.env.NODE_ENV !== \"production\") || __VUE_PROD_DEVTOOLS__) {\n devtoolsComponentAdded(instance2);\n }\n };\n sharedContext.deactivate = (vnode) => {\n const instance2 = vnode.component;\n invalidateMount(instance2.m);\n invalidateMount(instance2.a);\n move(vnode, storageContainer, null, 1, parentSuspense);\n queuePostRenderEffect(() => {\n if (instance2.da) {\n invokeArrayFns(instance2.da);\n }\n const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted;\n if (vnodeHook) {\n invokeVNodeHook(vnodeHook, instance2.parent, vnode);\n }\n instance2.isDeactivated = true;\n }, parentSuspense);\n if (!!(process.env.NODE_ENV !== \"production\") || __VUE_PROD_DEVTOOLS__) {\n devtoolsComponentAdded(instance2);\n }\n if (!!(process.env.NODE_ENV !== \"production\") && true) {\n instance2.__keepAliveStorageContainer = storageContainer;\n }\n };\n function unmount(vnode) {\n resetShapeFlag(vnode);\n _unmount(vnode, instance, parentSuspense, true);\n }\n function pruneCache(filter) {\n cache.forEach((vnode, key) => {\n const name = getComponentName(vnode.type);\n if (name && !filter(name)) {\n pruneCacheEntry(key);\n }\n });\n }\n function pruneCacheEntry(key) {\n const cached = cache.get(key);\n if (cached && (!current || !isSameVNodeType(cached, current))) {\n unmount(cached);\n } else if (current) {\n resetShapeFlag(current);\n }\n cache.delete(key);\n keys.delete(key);\n }\n watch(\n () => [props.include, props.exclude],\n ([include, exclude]) => {\n include && pruneCache((name) => matches(include, name));\n exclude && pruneCache((name) => !matches(exclude, name));\n },\n // prune post-render after `current` has been updated\n { flush: \"post\", deep: true }\n );\n let pendingCacheKey = null;\n const cacheSubtree = () => {\n if (pendingCacheKey != null) {\n if (isSuspense(instance.subTree.type)) {\n queuePostRenderEffect(() => {\n cache.set(pendingCacheKey, getInnerChild(instance.subTree));\n }, instance.subTree.suspense);\n } else {\n cache.set(pendingCacheKey, getInnerChild(instance.subTree));\n }\n }\n };\n onMounted(cacheSubtree);\n onUpdated(cacheSubtree);\n onBeforeUnmount(() => {\n cache.forEach((cached) => {\n const { subTree, suspense } = instance;\n const vnode = getInnerChild(subTree);\n if (cached.type === vnode.type && cached.key === vnode.key) {\n resetShapeFlag(vnode);\n const da = vnode.component.da;\n da && queuePostRenderEffect(da, suspense);\n return;\n }\n unmount(cached);\n });\n });\n return () => {\n pendingCacheKey = null;\n if (!slots.default) {\n return current = null;\n }\n const children = slots.default();\n const rawVNode = children[0];\n if (children.length > 1) {\n if (!!(process.env.NODE_ENV !== \"production\")) {\n warn$1(`KeepAlive should contain exactly one component child.`);\n }\n current = null;\n return children;\n } else if (!isVNode(rawVNode) || !(rawVNode.shapeFlag & 4) && !(rawVNode.shapeFlag & 128)) {\n current = null;\n return rawVNode;\n }\n let vnode = getInnerChild(rawVNode);\n if (vnode.type === Comment) {\n current = null;\n return vnode;\n }\n const comp = vnode.type;\n const name = getComponentName(\n isAsyncWrapper(vnode) ? vnode.type.__asyncResolved || {} : comp\n );\n const { include, exclude, max } = props;\n if (include && (!name || !matches(include, name)) || exclude && name && matches(exclude, name)) {\n vnode.shapeFlag &= -257;\n current = vnode;\n return rawVNode;\n }\n const key = vnode.key == null ? comp : vnode.key;\n const cachedVNode = cache.get(key);\n if (vnode.el) {\n vnode = cloneVNode(vnode);\n if (rawVNode.shapeFlag & 128) {\n rawVNode.ssContent = vnode;\n }\n }\n pendingCacheKey = key;\n if (cachedVNode) {\n vnode.el = cachedVNode.el;\n vnode.component = cachedVNode.component;\n if (vnode.transition) {\n setTransitionHooks(vnode, vnode.transition);\n }\n vnode.shapeFlag |= 512;\n keys.delete(key);\n keys.add(key);\n } else {\n keys.add(key);\n if (max && keys.size > parseInt(max, 10)) {\n pruneCacheEntry(keys.values().next().value);\n }\n }\n vnode.shapeFlag |= 256;\n current = vnode;\n return isSuspense(rawVNode.type) ? rawVNode : vnode;\n };\n }\n};\nconst KeepAlive = KeepAliveImpl;\nfunction matches(pattern, name) {\n if (isArray(pattern)) {\n return pattern.some((p) => matches(p, name));\n } else if (isString(pattern)) {\n return pattern.split(\",\").includes(name);\n } else if (isRegExp(pattern)) {\n pattern.lastIndex = 0;\n return pattern.test(name);\n }\n return false;\n}\nfunction onActivated(hook, target) {\n registerKeepAliveHook(hook, \"a\", target);\n}\nfunction onDeactivated(hook, target) {\n registerKeepAliveHook(hook, \"da\", target);\n}\nfunction registerKeepAliveHook(hook, type, target = currentInstance) {\n const wrappedHook = hook.__wdc || (hook.__wdc = () => {\n let current = target;\n while (current) {\n if (current.isDeactivated) {\n return;\n }\n current = current.parent;\n }\n return hook();\n });\n injectHook(type, wrappedHook, target);\n if (target) {\n let current = target.parent;\n while (current && current.parent) {\n if (isKeepAlive(current.parent.vnode)) {\n injectToKeepAliveRoot(wrappedHook, type, target, current);\n }\n current = current.parent;\n }\n }\n}\nfunction injectToKeepAliveRoot(hook, type, target, keepAliveRoot) {\n const injected = injectHook(\n type,\n hook,\n keepAliveRoot,\n true\n /* prepend */\n );\n onUnmounted(() => {\n remove(keepAliveRoot[type], injected);\n }, target);\n}\nfunction resetShapeFlag(vnode) {\n vnode.shapeFlag &= -257;\n vnode.shapeFlag &= -513;\n}\nfunction getInnerChild(vnode) {\n return vnode.shapeFlag & 128 ? vnode.ssContent : vnode;\n}\n\nfunction injectHook(type, hook, target = currentInstance, prepend = false) {\n if (target) {\n const hooks = target[type] || (target[type] = []);\n const wrappedHook = hook.__weh || (hook.__weh = (...args) => {\n pauseTracking();\n const reset = setCurrentInstance(target);\n const res = callWithAsyncErrorHandling(hook, target, type, args);\n reset();\n resetTracking();\n return res;\n });\n if (prepend) {\n hooks.unshift(wrappedHook);\n } else {\n hooks.push(wrappedHook);\n }\n return wrappedHook;\n } else if (!!(process.env.NODE_ENV !== \"production\")) {\n const apiName = toHandlerKey(ErrorTypeStrings$1[type].replace(/ hook$/, \"\"));\n warn$1(\n `${apiName} is called when there is no active component instance to be associated with. Lifecycle injection APIs can only be used during execution of setup().` + (` If you are using async setup(), make sure to register lifecycle hooks before the first await statement.` )\n );\n }\n}\nconst createHook = (lifecycle) => (hook, target = currentInstance) => {\n if (!isInSSRComponentSetup || lifecycle === \"sp\") {\n injectHook(lifecycle, (...args) => hook(...args), target);\n }\n};\nconst onBeforeMount = createHook(\"bm\");\nconst onMounted = createHook(\"m\");\nconst onBeforeUpdate = createHook(\n \"bu\"\n);\nconst onUpdated = createHook(\"u\");\nconst onBeforeUnmount = createHook(\n \"bum\"\n);\nconst onUnmounted = createHook(\"um\");\nconst onServerPrefetch = createHook(\n \"sp\"\n);\nconst onRenderTriggered = createHook(\"rtg\");\nconst onRenderTracked = createHook(\"rtc\");\nfunction onErrorCaptured(hook, target = currentInstance) {\n injectHook(\"ec\", hook, target);\n}\n\nconst COMPONENTS = \"components\";\nconst DIRECTIVES = \"directives\";\nfunction resolveComponent(name, maybeSelfReference) {\n return resolveAsset(COMPONENTS, name, true, maybeSelfReference) || name;\n}\nconst NULL_DYNAMIC_COMPONENT = Symbol.for(\"v-ndc\");\nfunction resolveDynamicComponent(component) {\n if (isString(component)) {\n return resolveAsset(COMPONENTS, component, false) || component;\n } else {\n return component || NULL_DYNAMIC_COMPONENT;\n }\n}\nfunction resolveDirective(name) {\n return resolveAsset(DIRECTIVES, name);\n}\nfunction resolveAsset(type, name, warnMissing = true, maybeSelfReference = false) {\n const instance = currentRenderingInstance || currentInstance;\n if (instance) {\n const Component = instance.type;\n if (type === COMPONENTS) {\n const selfName = getComponentName(\n Component,\n false\n );\n if (selfName && (selfName === name || selfName === camelize(name) || selfName === capitalize(camelize(name)))) {\n return Component;\n }\n }\n const res = (\n // local registration\n // check instance[type] first which is resolved for options API\n resolve(instance[type] || Component[type], name) || // global registration\n resolve(instance.appContext[type], name)\n );\n if (!res && maybeSelfReference) {\n return Component;\n }\n if (!!(process.env.NODE_ENV !== \"production\") && warnMissing && !res) {\n const extra = type === COMPONENTS ? `\nIf this is a native custom element, make sure to exclude it from component resolution via compilerOptions.isCustomElement.` : ``;\n warn$1(`Failed to resolve ${type.slice(0, -1)}: ${name}${extra}`);\n }\n return res;\n } else if (!!(process.env.NODE_ENV !== \"production\")) {\n warn$1(\n `resolve${capitalize(type.slice(0, -1))} can only be used in render() or setup().`\n );\n }\n}\nfunction resolve(registry, name) {\n return registry && (registry[name] || registry[camelize(name)] || registry[capitalize(camelize(name))]);\n}\n\nfunction renderList(source, renderItem, cache, index) {\n let ret;\n const cached = cache && cache[index];\n const sourceIsArray = isArray(source);\n if (sourceIsArray || isString(source)) {\n const sourceIsReactiveArray = sourceIsArray && isReactive(source);\n let needsWrap = false;\n let isReadonlySource = false;\n if (sourceIsReactiveArray) {\n needsWrap = !isShallow(source);\n isReadonlySource = isReadonly(source);\n source = shallowReadArray(source);\n }\n ret = new Array(source.length);\n for (let i = 0, l = source.length; i < l; i++) {\n ret[i] = renderItem(\n needsWrap ? isReadonlySource ? toReadonly(toReactive(source[i])) : toReactive(source[i]) : source[i],\n i,\n void 0,\n cached && cached[i]\n );\n }\n } else if (typeof source === \"number\") {\n if (!!(process.env.NODE_ENV !== \"production\") && !Number.isInteger(source)) {\n warn$1(`The v-for range expect an integer value but got ${source}.`);\n }\n ret = new Array(source);\n for (let i = 0; i < source; i++) {\n ret[i] = renderItem(i + 1, i, void 0, cached && cached[i]);\n }\n } else if (isObject(source)) {\n if (source[Symbol.iterator]) {\n ret = Array.from(\n source,\n (item, i) => renderItem(item, i, void 0, cached && cached[i])\n );\n } else {\n const keys = Object.keys(source);\n ret = new Array(keys.length);\n for (let i = 0, l = keys.length; i < l; i++) {\n const key = keys[i];\n ret[i] = renderItem(source[key], key, i, cached && cached[i]);\n }\n }\n } else {\n ret = [];\n }\n if (cache) {\n cache[index] = ret;\n }\n return ret;\n}\n\nfunction createSlots(slots, dynamicSlots) {\n for (let i = 0; i < dynamicSlots.length; i++) {\n const slot = dynamicSlots[i];\n if (isArray(slot)) {\n for (let j = 0; j < slot.length; j++) {\n slots[slot[j].name] = slot[j].fn;\n }\n } else if (slot) {\n slots[slot.name] = slot.key ? (...args) => {\n const res = slot.fn(...args);\n if (res) res.key = slot.key;\n return res;\n } : slot.fn;\n }\n }\n return slots;\n}\n\nfunction renderSlot(slots, name, props = {}, fallback, noSlotted) {\n if (currentRenderingInstance.ce || currentRenderingInstance.parent && isAsyncWrapper(currentRenderingInstance.parent) && currentRenderingInstance.parent.ce) {\n const hasProps = Object.keys(props).length > 0;\n if (name !== \"default\") props.name = name;\n return openBlock(), createBlock(\n Fragment,\n null,\n [createVNode(\"slot\", props, fallback && fallback())],\n hasProps ? -2 : 64\n );\n }\n let slot = slots[name];\n if (!!(process.env.NODE_ENV !== \"production\") && slot && slot.length > 1) {\n warn$1(\n `SSR-optimized slot function detected in a non-SSR-optimized render function. You need to mark this component with $dynamic-slots in the parent template.`\n );\n slot = () => [];\n }\n if (slot && slot._c) {\n slot._d = false;\n }\n openBlock();\n const validSlotContent = slot && ensureValidVNode(slot(props));\n const slotKey = props.key || // slot content array of a dynamic conditional slot may have a branch\n // key attached in the `createSlots` helper, respect that\n validSlotContent && validSlotContent.key;\n const rendered = createBlock(\n Fragment,\n {\n key: (slotKey && !isSymbol(slotKey) ? slotKey : `_${name}`) + // #7256 force differentiate fallback content from actual content\n (!validSlotContent && fallback ? \"_fb\" : \"\")\n },\n validSlotContent || (fallback ? fallback() : []),\n validSlotContent && slots._ === 1 ? 64 : -2\n );\n if (!noSlotted && rendered.scopeId) {\n rendered.slotScopeIds = [rendered.scopeId + \"-s\"];\n }\n if (slot && slot._c) {\n slot._d = true;\n }\n return rendered;\n}\nfunction ensureValidVNode(vnodes) {\n return vnodes.some((child) => {\n if (!isVNode(child)) return true;\n if (child.type === Comment) return false;\n if (child.type === Fragment && !ensureValidVNode(child.children))\n return false;\n return true;\n }) ? vnodes : null;\n}\n\nfunction toHandlers(obj, preserveCaseIfNecessary) {\n const ret = {};\n if (!!(process.env.NODE_ENV !== \"production\") && !isObject(obj)) {\n warn$1(`v-on with no argument expects an object value.`);\n return ret;\n }\n for (const key in obj) {\n ret[preserveCaseIfNecessary && /[A-Z]/.test(key) ? `on:${key}` : toHandlerKey(key)] = obj[key];\n }\n return ret;\n}\n\nconst getPublicInstance = (i) => {\n if (!i) return null;\n if (isStatefulComponent(i)) return getComponentPublicInstance(i);\n return getPublicInstance(i.parent);\n};\nconst publicPropertiesMap = (\n // Move PURE marker to new line to workaround compiler discarding it\n // due to type annotation\n /* @__PURE__ */ extend(/* @__PURE__ */ Object.create(null), {\n $: (i) => i,\n $el: (i) => i.vnode.el,\n $data: (i) => i.data,\n $props: (i) => !!(process.env.NODE_ENV !== \"production\") ? shallowReadonly(i.props) : i.props,\n $attrs: (i) => !!(process.env.NODE_ENV !== \"production\") ? shallowReadonly(i.attrs) : i.attrs,\n $slots: (i) => !!(process.env.NODE_ENV !== \"production\") ? shallowReadonly(i.slots) : i.slots,\n $refs: (i) => !!(process.env.NODE_ENV !== \"production\") ? shallowReadonly(i.refs) : i.refs,\n $parent: (i) => getPublicInstance(i.parent),\n $root: (i) => getPublicInstance(i.root),\n $host: (i) => i.ce,\n $emit: (i) => i.emit,\n $options: (i) => __VUE_OPTIONS_API__ ? resolveMergedOptions(i) : i.type,\n $forceUpdate: (i) => i.f || (i.f = () => {\n queueJob(i.update);\n }),\n $nextTick: (i) => i.n || (i.n = nextTick.bind(i.proxy)),\n $watch: (i) => __VUE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP\n })\n);\nconst isReservedPrefix = (key) => key === \"_\" || key === \"$\";\nconst hasSetupBinding = (state, key) => state !== EMPTY_OBJ && !state.__isScriptSetup && hasOwn(state, key);\nconst PublicInstanceProxyHandlers = {\n get({ _: instance }, key) {\n if (key === \"__v_skip\") {\n return true;\n }\n const { ctx, setupState, data, props, accessCache, type, appContext } = instance;\n if (!!(process.env.NODE_ENV !== \"production\") && key === \"__isVue\") {\n return true;\n }\n let normalizedProps;\n if (key[0] !== \"$\") {\n const n = accessCache[key];\n if (n !== void 0) {\n switch (n) {\n case 1 /* SETUP */:\n return setupState[key];\n case 2 /* DATA */:\n return data[key];\n case 4 /* CONTEXT */:\n return ctx[key];\n case 3 /* PROPS */:\n return props[key];\n }\n } else if (hasSetupBinding(setupState, key)) {\n accessCache[key] = 1 /* SETUP */;\n return setupState[key];\n } else if (data !== EMPTY_OBJ && hasOwn(data, key)) {\n accessCache[key] = 2 /* DATA */;\n return data[key];\n } else if (\n // only cache other properties when instance has declared (thus stable)\n // props\n (normalizedProps = instance.propsOptions[0]) && hasOwn(normalizedProps, key)\n ) {\n accessCache[key] = 3 /* PROPS */;\n return props[key];\n } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {\n accessCache[key] = 4 /* CONTEXT */;\n return ctx[key];\n } else if (!__VUE_OPTIONS_API__ || shouldCacheAccess) {\n accessCache[key] = 0 /* OTHER */;\n }\n }\n const publicGetter = publicPropertiesMap[key];\n let cssModule, globalProperties;\n if (publicGetter) {\n if (key === \"$attrs\") {\n track(instance.attrs, \"get\", \"\");\n !!(process.env.NODE_ENV !== \"production\") && markAttrsAccessed();\n } else if (!!(process.env.NODE_ENV !== \"production\") && key === \"$slots\") {\n track(instance, \"get\", key);\n }\n return publicGetter(instance);\n } else if (\n // css module (injected by vue-loader)\n (cssModule = type.__cssModules) && (cssModule = cssModule[key])\n ) {\n return cssModule;\n } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {\n accessCache[key] = 4 /* CONTEXT */;\n return ctx[key];\n } else if (\n // global properties\n globalProperties = appContext.config.globalProperties, hasOwn(globalProperties, key)\n ) {\n {\n return globalProperties[key];\n }\n } else if (!!(process.env.NODE_ENV !== \"production\") && currentRenderingInstance && (!isString(key) || // #1091 avoid internal isRef/isVNode checks on component instance leading\n // to infinite warning loop\n key.indexOf(\"__v\") !== 0)) {\n if (data !== EMPTY_OBJ && isReservedPrefix(key[0]) && hasOwn(data, key)) {\n warn$1(\n `Property ${JSON.stringify(\n key\n )} must be accessed via $data because it starts with a reserved character (\"$\" or \"_\") and is not proxied on the render context.`\n );\n } else if (instance === currentRenderingInstance) {\n warn$1(\n `Property ${JSON.stringify(key)} was accessed during render but is not defined on instance.`\n );\n }\n }\n },\n set({ _: instance }, key, value) {\n const { data, setupState, ctx } = instance;\n if (hasSetupBinding(setupState, key)) {\n setupState[key] = value;\n return true;\n } else if (!!(process.env.NODE_ENV !== \"production\") && setupState.__isScriptSetup && hasOwn(setupState, key)) {\n warn$1(`Cannot mutate \n\n\n","import { mapState } from 'pinia'\n\nimport FeaturesPanel from 'src/components/features_panel/features_panel.vue'\nimport InstanceSpecificPanel from 'src/components/instance_specific_panel/instance_specific_panel.vue'\nimport MRFTransparencyPanel from 'src/components/mrf_transparency_panel/mrf_transparency_panel.vue'\nimport StaffPanel from 'src/components/staff_panel/staff_panel.vue'\nimport TermsOfServicePanel from 'src/components/terms_of_service_panel/terms_of_service_panel.vue'\n\nimport { useInstanceStore } from 'src/stores/instance.js'\nimport { useMergedConfigStore } from 'src/stores/merged_config.js'\n\nconst pleromaFeCommitUrl =\n 'https://git.pleroma.social/pleroma/pleroma-fe/commit/'\n\nconst About = {\n components: {\n InstanceSpecificPanel,\n FeaturesPanel,\n TermsOfServicePanel,\n StaffPanel,\n MRFTransparencyPanel,\n },\n computed: {\n showFeaturesPanel() {\n return useInstanceStore().instanceIdentity.showFeaturesPanel\n },\n frontendVersionLink() {\n return pleromaFeCommitUrl + this.frontendVersion\n },\n ...mapState(useInstanceStore, [\n 'backendVersion',\n 'backendRepository',\n 'frontendVersion',\n ]),\n showInstanceSpecificPanel() {\n return (\n useInstanceStore().instanceIdentity.showInstanceSpecificPanel &&\n !useMergedConfigStore().mergedConfig.hideISP &&\n useInstanceStore().instanceIdentity.instanceSpecificPanelContent\n )\n },\n },\n}\n\nexport default About\n","\n\n\n\n"],"mappings":"2dAWM,GAA0B,EAAW,EAAM,IACxC,EAAU,IAAK,GAChB,EAAK,IAAQ,EAAK,GAAK,IAAa,EAAK,GAAK,GAAU,OACnD,CAAE,WAAU,OAAQ,EAAK,GAAK,GAAU,MAAO,EAEjD,CAAE,WAAU,OAAQ,EAAG,CAC/B,EAGG,EAAuB,CAC3B,SAAU,CACR,GAAG,EAAS,EAAkB,CAC5B,iBAAmB,GAAU,EAAM,iBACnC,YAAc,IAAA,EAAA,EAAA,KAAc,EAAO,gCAAiC,CAAC,CAAC,EACtE,oBAAsB,GACpB,GAAA,EAAA,EAAA,KACM,EAAO,yCAA0C,CAAC,CAAC,GAAA,EAAA,EAAA,KACnD,EAAO,8CAA+C,CAAC,CAAC,EAC5D,uBACF,EACF,gBAAkB,GAChB,GAAA,EAAA,EAAA,KACM,EAAO,qCAAsC,CAAC,CAAC,GAAA,EAAA,EAAA,KAC/C,EAAO,mCAAoC,CAAC,CAAC,EACjD,QACF,EACF,gBAAkB,GAChB,GAAA,EAAA,EAAA,KACM,EAAO,qCAAsC,CAAC,CAAC,GAAA,EAAA,EAAA,KAC/C,EAAO,mCAAoC,CAAC,CAAC,EACjD,QACF,EACF,oBAAsB,GACpB,GAAA,EAAA,EAAA,KAEI,EACA,yDACA,CAAC,CACH,GAAA,EAAA,EAAA,KACI,EAAO,mCAAoC,CAAC,CAAC,EACjD,4BACF,EACF,mBAAqB,GACnB,GAAA,EAAA,EAAA,KACM,EAAO,yCAA0C,CAAC,CAAC,GAAA,EAAA,EAAA,KACnD,EAAO,mCAAoC,CAAC,CAAC,EACjD,YACF,EACF,sBAAwB,GACtB,GAAA,EAAA,EAAA,KACM,EAAO,4CAA6C,CAAC,CAAC,GAAA,EAAA,EAAA,KACtD,EAAO,mCAAoC,CAAC,CAAC,EACjD,eACF,EACF,mBAAqB,IAAA,EAAA,EAAA,KAEjB,EACA,0DACA,CAAC,CACH,EACF,eAAiB,IAAA,EAAA,EAAA,KACX,EAAO,sCAAuC,CAAC,CAAC,EACtD,gBAAkB,IAAA,EAAA,EAAA,KACZ,EAAO,uCAAwC,CAAC,CAAC,CACzD,CAAC,EACD,6BAA8B,CAC5B,OACE,KAAK,oBAAoB,QACzB,KAAK,gBAAgB,QACrB,KAAK,gBAAgB,QACrB,KAAK,oBAAoB,QACzB,KAAK,mBAAmB,QACxB,KAAK,sBAAsB,MAE/B,EACA,oBAAqB,CACnB,OACE,KAAK,mBAAmB,QACxB,KAAK,eAAe,QACpB,KAAK,gBAAgB,MAEzB,CACF,CACF,WC3FI,MAAM,6BAED,MAAM,uCAAuC,KAC3C,MAAM,0DAA0D,KAC9D,MAAM,OAAO,KAIf,MAAM,YAAY,KAChB,MAAM,aAAa,iUAVtB,EAAA,kBAAA,EAAA,EADR,EA0OM,MA1ON,EA0OM,CAtOJ,EAqOM,MArON,EAqOM,CApOJ,EAIM,MAJN,EAIM,CAHJ,EAEM,MAFN,EAEM,EADD,EAAA,GAAE,sBAAA,CAAA,EAAA,CAAA,CAAA,CAAA,EAGT,EA8NM,MA9NN,EA8NM,CA7NJ,EA4NM,MA5NN,EA4NM,CA3NJ,EAA2C,KAAA,KAAA,EAApC,EAAA,GAAE,wBAAA,CAAA,EAAA,CAAA,EACT,EAA8C,IAAA,KAAA,EAAxC,EAAA,GAAE,6BAAA,CAAA,EAAA,CAAA,EAER,EAMK,KAAA,KAAA,EAAA,EAAA,EAAA,EALH,EAIE,EAAA,KAAA,EAHiB,EAAA,YAAV,QADT,EAIE,KAAA,CAFC,IAAK,cACN,EAAe,CAAD,uBAIR,EAAA,6BAAA,EAAA,EAAV,EAEK,KAAA,EAAA,EADA,EAAA,GAAE,kCAAA,CAAA,EAAA,CAAA,GAAA,EAAA,GAAA,EAAA,EAGI,EAAA,gBAAgB,QAAA,EAAA,EAA3B,EAyBM,MAAA,EAAA,CAxBJ,EAA4C,KAAA,KAAA,EAArC,EAAA,GAAE,yBAAA,CAAA,EAAA,CAAA,EAET,EAA+C,IAAA,KAAA,EAAzC,EAAA,GAAE,8BAAA,CAAA,EAAA,CAAA,EAER,EAmBQ,QAAA,KAAA,CAlBN,EAiBQ,QAAA,KAAA,CAhBN,EAGK,KAAA,KAAA,CAFH,EAA8C,KAAA,KAAA,EAAvC,EAAA,GAAE,2BAAA,CAAA,EAAA,CAAA,EACT,EAA4C,KAAA,KAAA,EAArC,EAAA,GAAE,yBAAA,CAAA,EAAA,CAAA,CAAA,CAAA,GAAA,EAAA,EAAA,EAEX,EAWK,EAAA,KAAA,EAVa,EAAA,gBAAT,QADT,EAWK,KAAA,CATF,IAAK,EAAM,SAAQ,SAAA,EAAA,CAEpB,EAA6B,KAAA,KAAA,EAAtB,EAAM,QAAQ,EAAA,CAAA,EACX,EAAM,SAAM,IAAA,EAAA,EAAtB,EAEK,KAAA,EAAA,EADA,EAAA,GAAE,iCAAA,CAAA,EAAA,CAAA,IAAA,EAAA,EAEP,EAEK,KAAA,EAAA,EADA,EAAM,MAAM,EAAA,CAAA,EAAA,CAAA,0BAOd,EAAA,gBAAgB,QAAA,EAAA,EAA3B,EAyBM,MAAA,EAAA,CAxBJ,EAA4C,KAAA,KAAA,EAArC,EAAA,GAAE,yBAAA,CAAA,EAAA,CAAA,EAET,EAA+C,IAAA,KAAA,EAAzC,EAAA,GAAE,8BAAA,CAAA,EAAA,CAAA,EAER,EAmBQ,QAAA,KAAA,CAlBN,EAiBQ,QAAA,KAAA,CAhBN,EAGK,KAAA,KAAA,CAFH,EAA8C,KAAA,KAAA,EAAvC,EAAA,GAAE,2BAAA,CAAA,EAAA,CAAA,EACT,EAA4C,KAAA,KAAA,EAArC,EAAA,GAAE,yBAAA,CAAA,EAAA,CAAA,CAAA,CAAA,GAAA,EAAA,EAAA,EAEX,EAWK,EAAA,KAAA,EAVa,EAAA,gBAAT,QADT,EAWK,KAAA,CATF,IAAK,EAAM,SAAQ,SAAA,EAAA,CAEpB,EAA6B,KAAA,KAAA,EAAtB,EAAM,QAAQ,EAAA,CAAA,EACX,EAAM,SAAM,IAAA,EAAA,EAAtB,EAEK,KAAA,EAAA,EADA,EAAA,GAAE,iCAAA,CAAA,EAAA,CAAA,IAAA,EAAA,EAEP,EAEK,KAAA,EAAA,EADA,EAAM,MAAM,EAAA,CAAA,EAAA,CAAA,0BAOd,EAAA,oBAAoB,QAAA,EAAA,EAA/B,EAyBM,MAAA,EAAA,CAxBJ,EAAgD,KAAA,KAAA,EAAzC,EAAA,GAAE,6BAAA,CAAA,EAAA,CAAA,EAET,EAAmD,IAAA,KAAA,EAA7C,EAAA,GAAE,kCAAA,CAAA,EAAA,CAAA,EAER,EAmBQ,QAAA,KAAA,CAlBN,EAiBQ,QAAA,KAAA,CAhBN,EAGK,KAAA,KAAA,CAFH,EAA8C,KAAA,KAAA,EAAvC,EAAA,GAAE,2BAAA,CAAA,EAAA,CAAA,EACT,EAA4C,KAAA,KAAA,EAArC,EAAA,GAAE,yBAAA,CAAA,EAAA,CAAA,CAAA,CAAA,GAAA,EAAA,EAAA,EAEX,EAWK,EAAA,KAAA,EAVa,EAAA,oBAAT,QADT,EAWK,KAAA,CATF,IAAK,EAAM,SAAQ,aAAA,EAAA,CAEpB,EAA6B,KAAA,KAAA,EAAtB,EAAM,QAAQ,EAAA,CAAA,EACX,EAAM,SAAM,IAAA,EAAA,EAAtB,EAEK,KAAA,EAAA,EADA,EAAA,GAAE,iCAAA,CAAA,EAAA,CAAA,IAAA,EAAA,EAEP,EAEK,KAAA,EAAA,EADA,EAAM,MAAM,EAAA,CAAA,EAAA,CAAA,0BAOd,EAAA,oBAAoB,QAAA,EAAA,EAA/B,EAyBM,MAAA,EAAA,CAxBJ,EAAiD,KAAA,KAAA,EAA1C,EAAA,GAAE,8BAAA,CAAA,EAAA,CAAA,EAET,EAAoD,IAAA,KAAA,EAA9C,EAAA,GAAE,mCAAA,CAAA,EAAA,CAAA,EAER,EAmBQ,QAAA,KAAA,CAlBN,EAiBQ,QAAA,KAAA,CAhBN,EAGK,KAAA,KAAA,CAFH,EAA8C,KAAA,KAAA,EAAvC,EAAA,GAAE,2BAAA,CAAA,EAAA,CAAA,EACT,EAA4C,KAAA,KAAA,EAArC,EAAA,GAAE,yBAAA,CAAA,EAAA,CAAA,CAAA,CAAA,GAAA,EAAA,EAAA,EAEX,EAWK,EAAA,KAAA,EAVa,EAAA,oBAAT,QADT,EAWK,KAAA,CATF,IAAK,EAAM,SAAQ,cAAA,EAAA,CAEpB,EAA6B,KAAA,KAAA,EAAtB,EAAM,QAAQ,EAAA,CAAA,EACX,EAAM,SAAM,IAAA,EAAA,EAAtB,EAEK,KAAA,EAAA,EADA,EAAA,GAAE,iCAAA,CAAA,EAAA,CAAA,IAAA,EAAA,EAEP,EAEK,KAAA,EAAA,EADA,EAAM,MAAM,EAAA,CAAA,EAAA,CAAA,0BAOd,EAAA,mBAAmB,QAAA,EAAA,EAA9B,EAyBM,MAAA,EAAA,CAxBJ,EAAgD,KAAA,KAAA,EAAzC,EAAA,GAAE,6BAAA,CAAA,EAAA,CAAA,EAET,EAAmD,IAAA,KAAA,EAA7C,EAAA,GAAE,kCAAA,CAAA,EAAA,CAAA,EAER,EAmBQ,QAAA,KAAA,CAlBN,EAiBQ,QAAA,KAAA,CAhBN,EAGK,KAAA,KAAA,CAFH,EAA8C,KAAA,KAAA,EAAvC,EAAA,GAAE,2BAAA,CAAA,EAAA,CAAA,EACT,EAA4C,KAAA,KAAA,EAArC,EAAA,GAAE,yBAAA,CAAA,EAAA,CAAA,CAAA,CAAA,GAAA,EAAA,EAAA,EAEX,EAWK,EAAA,KAAA,EAVa,EAAA,mBAAT,QADT,EAWK,KAAA,CATF,IAAK,EAAM,SAAQ,aAAA,EAAA,CAEpB,EAA6B,KAAA,KAAA,EAAtB,EAAM,QAAQ,EAAA,CAAA,EACX,EAAM,SAAM,IAAA,EAAA,EAAtB,EAEK,KAAA,EAAA,EADA,EAAA,GAAE,iCAAA,CAAA,EAAA,CAAA,IAAA,EAAA,EAEP,EAEK,KAAA,EAAA,EADA,EAAM,MAAM,EAAA,CAAA,EAAA,CAAA,0BAOd,EAAA,sBAAsB,QAAA,EAAA,EAAjC,EAyBM,MAAA,EAAA,CAxBJ,EAAmD,KAAA,KAAA,EAA5C,EAAA,GAAE,gCAAA,CAAA,EAAA,CAAA,EAET,EAAsD,IAAA,KAAA,EAAhD,EAAA,GAAE,qCAAA,CAAA,EAAA,CAAA,EAER,EAmBQ,QAAA,KAAA,CAlBN,EAiBQ,QAAA,KAAA,CAhBN,EAGK,KAAA,KAAA,CAFH,EAA8C,KAAA,KAAA,EAAvC,EAAA,GAAE,2BAAA,CAAA,EAAA,CAAA,EACT,EAA4C,KAAA,KAAA,EAArC,EAAA,GAAE,yBAAA,CAAA,EAAA,CAAA,CAAA,CAAA,GAAA,EAAA,EAAA,EAEX,EAWK,EAAA,KAAA,EAVa,EAAA,sBAAT,QADT,EAWK,KAAA,CATF,IAAK,EAAM,SAAQ,gBAAA,EAAA,CAEpB,EAA6B,KAAA,KAAA,EAAtB,EAAM,QAAQ,EAAA,CAAA,EACX,EAAM,SAAM,IAAA,EAAA,EAAtB,EAEK,KAAA,GAAA,EADA,EAAA,GAAE,iCAAA,CAAA,EAAA,CAAA,IAAA,EAAA,EAEP,EAEK,KAAA,EAAA,EADA,EAAM,MAAM,EAAA,CAAA,EAAA,CAAA,0BAOf,EAAA,oBAAA,EAAA,EAAV,EAEK,KAAA,EAAA,EADA,EAAA,GAAE,oCAAA,CAAA,EAAA,CAAA,GAAA,EAAA,GAAA,EAAA,EAGI,EAAA,mBAAmB,QAAA,EAAA,EAA9B,EAUM,MAAA,EAAA,CATJ,EAAkD,KAAA,KAAA,EAA3C,EAAA,GAAE,+BAAA,CAAA,EAAA,CAAA,EAET,EAMK,KAAA,KAAA,EAAA,EAAA,EAAA,EALH,EAIE,EAAA,KAAA,EAHkB,EAAA,mBAAX,QADT,EAIE,KAAA,CAFC,IAAK,cACN,EAAgB,CAAD,mCAKV,EAAA,eAAe,QAAA,EAAA,EAA1B,EAUM,MAAA,EAAA,CATJ,EAA6C,KAAA,KAAA,EAAtC,EAAA,GAAE,0BAAA,CAAA,EAAA,CAAA,EAET,EAMK,KAAA,KAAA,EAAA,EAAA,EAAA,EALH,EAIE,EAAA,KAAA,EAHkB,EAAA,eAAX,QADT,EAIE,KAAA,CAFC,IAAK,cACN,EAAgB,CAAD,mCAKV,EAAA,gBAAgB,QAAA,EAAA,EAA3B,EAaM,MAAA,GAAA,CAZJ,EAA8C,KAAA,KAAA,EAAvC,EAAA,GAAE,2BAAA,CAAA,EAAA,CAAA,EAET,EASK,KAAA,KAAA,EAAA,EAAA,EAAA,EARH,EAOK,EAAA,KAAA,EANe,EAAA,gBAAX,QADT,EAOK,KAAA,CALF,IAAK,CAAO,EAAA,EAEV,EAAQ,OAAO,EAAG,IACrB,EAAG,EAAA,GAAE,kCAAA,CAAA,EAAuC,IAC5C,EAAG,EAAQ,WAAW,EAAA,CAAA,sEC7NhC,GAAa,CACjB,SAAU,CAER,EADmC,EAAE,cAC3B,QAAS,GACjB,KAAK,OAAO,SAAS,qBAAsB,CAAQ,CACrD,CACF,EACA,WAAY,CACV,cAAA,EACF,EACA,SAAU,CACR,sBAAuB,CAIrB,IAAM,GAAA,EAAA,EAAA,UAAA,EAAA,EAAA,KAHoB,KAAK,cAAe,KAAK,cAAc,EAAE,OAChE,GAAM,CAEwC,EAAG,MAAM,EAE1D,MAAO,CACL,CAAE,KAAM,QAAS,MAAO,EAAqB,KAAM,EACnD,CAAE,KAAM,YAAa,MAAO,EAAqB,SAAU,CAC7D,EAAE,OAAQ,GAAU,EAAM,KAAK,CACjC,EACA,GAAG,EAAW,CAAC,gBAAgB,CAAC,EAChC,GAAGA,EAAS,CACV,cAAgB,GAAU,EAAiB,EAAE,aAC/C,CAAC,CACH,CACF,MCjCO,MAAM,aAAa,MACjB,MAAM,uCAAuC,MAC3C,MAAM,0DAA0D,MAC9D,MAAM,OAAO,MAIf,MAAM,YAAY,iEAP3B,EAsBM,MAtBN,GAsBM,CArBJ,EAoBM,MApBN,GAoBM,CAnBJ,EAIM,MAJN,GAIM,CAHJ,EAEM,MAFN,GAEM,EADD,EAAA,GAAE,aAAA,CAAA,EAAA,CAAA,CAAA,CAAA,EAGT,EAaM,MAbN,GAaM,EAAA,EAAA,EAAA,EAZJ,EAWM,EAAA,KAAA,EAVY,EAAA,qBAAT,QADT,EAWM,MAAA,CATH,IAAK,EAAM,KACZ,MAAM,gBAEN,EAA+C,KAAA,KAAA,EAAxC,EAAA,GAAE,gBAAmB,EAAM,IAAI,CAAA,EAAA,CAAA,GAAA,EAAA,EAAA,EACtC,EAIE,EAAA,KAAA,EAHe,EAAM,MAAd,QADT,EAIE,EAAA,CAFC,IAAK,EAAK,YACJ,8ECPb,GACJ,wDAEI,EAAQ,CACZ,WAAY,CACV,sBAAA,EACA,cAAA,EACA,oBAAA,EACA,WAAA,GACA,qBAAA,EACF,EACA,SAAU,CACR,mBAAoB,CAClB,OAAO,EAAiB,EAAE,iBAAiB,iBAC7C,EACA,qBAAsB,CACpB,OAAO,GAAqB,KAAK,eACnC,EACA,GAAG,EAAS,EAAkB,CAC5B,iBACA,oBACA,iBACF,CAAC,EACD,2BAA4B,CAC1B,OACE,EAAiB,EAAE,iBAAiB,2BACpC,CAAC,EAAqB,EAAE,aAAa,SACrC,EAAiB,EAAE,iBAAiB,4BAExC,CACF,CACF,MCzCO,MAAM,oBAAoB,MAMxB,MAAM,qBAAqB,MACzB,MAAM,eAAe,MACnB,MAAM,OAAO,MAIf,MAAM,YAAY,oMAZ3B,EAmCM,MAnCN,GAmCM,CAlC2B,EAAA,2BAAA,EAAA,EAA/B,EAA4D,EAAA,CAAA,IAAA,CAAA,CAAA,GAAA,EAAA,GAAA,EAAA,EAC5D,EAAe,CAAA,EACf,EAA0B,CAAA,EAC1B,EAAwB,CAAA,EACF,EAAA,mBAAA,EAAA,EAAtB,EAA2C,EAAA,CAAA,IAAA,CAAA,CAAA,GAAA,EAAA,GAAA,EAAA,EAC3C,EA4BM,MA5BN,GA4BM,CA3BJ,EAIM,MAJN,GAIM,CAHJ,EAEM,MAFN,GAEM,EADD,EAAA,GAAE,wBAAA,CAAA,EAAA,CAAA,CAAA,CAAA,EAGT,EAqBM,MArBN,GAqBM,CApBJ,EAmBK,KAAA,KAAA,CAlBH,EAAqD,KAAA,KAAA,EAA9C,EAAA,GAAE,kCAAA,CAAA,EAAA,CAAA,EACT,EAOK,KAAA,KAAA,CANH,EAKI,IAAA,CAJD,KAAM,EAAA,kBACP,OAAO,YAEJ,EAAA,cAAc,EAAA,EAAA,EAAA,CAAA,CAAA,EAGrB,EAAsD,KAAA,KAAA,EAA/C,EAAA,GAAE,mCAAA,CAAA,EAAA,CAAA,EACT,EAOK,KAAA,KAAA,CANH,EAKI,IAAA,CAJD,KAAM,EAAA,oBACP,OAAO,YAEJ,EAAA,eAAe,EAAA,EAAA,EAAA,CAAA,CAAA"} \ No newline at end of file diff --git a/priv/static/static/js/account_actions.iKah8h7y.js b/priv/static/static/js/account_actions.iKah8h7y.js new file mode 100644 index 000000000..93f928e07 --- /dev/null +++ b/priv/static/static/js/account_actions.iKah8h7y.js @@ -0,0 +1,3 @@ +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["static/js/confirm_modal.f5dYyJF2.js","static/js/_plugin-vue_export-helper.By3-ojQa.js","static/js/chunk.aKtaBQYM.js","static/js/free-solid-svg-icons.vn_kySUQ.js","static/js/runtime-dom.esm-bundler.DzcUlaDE.js","static/js/dialog_modal.CxsP9W3P.js","static/js/oauth.C4JLKGAu.js","static/js/lodash.daI9UflD.js","static/js/modal.BLgTTi-o.js","static/css/modal.Dv3V-p-W.css","static/js/helpers.DO6M90bq.js","static/css/dialog_modal.DC5hTqSH.css","static/css/confirm_modal.CxXYsWAn.css","static/js/user_timed_filter_modal.Njj7Gcae.js","static/js/main.CPF6dYCY.js","static/js/emoji.CKB_ooIp.js","static/js/preload-helper.zJ_50EbN.js","static/js/languages.q6q0orhR.js","static/js/free-regular-svg-icons.BGRGA4e_.js","static/js/vuex.esm-bundler.CWMfctgB.js","static/js/locale.service.ol8gNUGd.js","static/js/admin.Dy10Wzep.js","static/js/checkbox.BfKZDdOO.js","static/css/checkbox.DY0WQ--N.css","static/js/user_card.DVbLIASH.js","static/js/attributes_helper.service.Ku0p8pb2.js","static/js/emoji_picker.Ccx2u99H.js","static/js/popover.94nM-RDc.js","static/css/popover.BlbX_Uwp.css","static/css/emoji_picker.CJFAWWOw.css","static/js/unicode_domain_indicator.BnLqD48H.js","static/css/attributes_helper.DsgIU8rn.css","static/js/progress_button.B2rAp_zx.js","static/js/select.BU31Em0g.js","static/css/select.BF2ygbkJ.css","static/js/user_link.CzPI9p_B.js","static/js/user_profile_link_generator.WrdnW5sg.js","static/js/instance_capabilities.B6a7_omF.js","static/js/media_viewer.B2Dx6m_D.js","static/js/post_status.BF7jtp5r.js","static/css/user_card.C2QiPTUj.css","static/js/promise_interval.BgbE32Aa.js","static/css/main.0Iw0byjp.css","static/css/user_timed_filter_modal.ClNyvF9i.css"])))=>i.map(i=>d[i]); +import{I as e,M as t,R as n,U as r,X as i,_ as a,d as o,f as s,g as c,i as l,o as u,p as d,q as f,t as p,u as m,v as h}from"./_plugin-vue_export-helper.By3-ojQa.js";import{a as g}from"./modal.BLgTTi-o.js";import{O as _}from"./oauth.C4JLKGAu.js";import{T as v,kn as y,q as b}from"./free-solid-svg-icons.vn_kySUQ.js";import{t as x}from"./preload-helper.zJ_50EbN.js";import{n as S}from"./instance_capabilities.B6a7_omF.js";import{t as C}from"./popover.94nM-RDc.js";import{t as w}from"./progress_button.B2rAp_zx.js";import{V as T,z as E}from"./main.CPF6dYCY.js";y.add(v);var D={props:[`user`],data(){return{}},components:{Popover:C},created(){this.$store.dispatch(`fetchUserInLists`,this.user.id)},computed:{...g(T,{allLists:e=>e.allLists}),inListsSet(){return new Set(this.user.inLists.map(e=>e.id))},lists(){return this.user.inLists?this.allLists.map(e=>({...e,inList:this.inListsSet.has(e.id)})):[]},triggerAttrs(){return{class:`menu-item dropdown-item -has-submenu`}}},methods:{toggleList(e){this.inListsSet.has(e)?T().removeListAccount({accountId:this.user.id,listId:e}).then(e=>{e.ok&&this.$store.dispatch(`fetchUserInLists`,this.user.id)}):T().addListAccount({accountId:this.user.id,listId:e}).then(e=>{e.ok&&this.$store.dispatch(`fetchUserInLists`,this.user.id)})}}},O={class:`UserListMenu`},k={class:`dropdown-menu`},A=[`onClick`],j={class:`main-button`};function M(o,s,u,p,h,g){let _=n(`FAIcon`),v=n(`Popover`);return t(),d(`div`,O,[a(v,{trigger:`hover`,placement:`right`,"trigger-attrs":o.triggerAttrs,"remove-padding":``},{content:r(()=>[m(`div`,k,[(t(!0),d(l,null,e(o.lists,e=>(t(),d(`div`,{key:e.id,class:`menu-item dropdown-item -icon`},[m(`button`,{class:`main-button`,onClick:t=>o.toggleList(e.id)},[m(`span`,{class:f([`input menu-checkbox`,{"menu-checkbox-checked":e.inList}])},null,2),c(` `+i(e.title),1)],8,A)]))),128))])]),trigger:r(()=>[m(`span`,j,[c(i(o.$t(`lists.manage_lists`))+` `,1),a(_,{class:`chevron-icon`,size:`lg`,icon:`chevron-right`})])]),_:1},8,[`trigger-attrs`])])}var N=p(D,[[`render`,M]]);y.add(b);var P={props:[`user`,`relationship`],data(){return{showingConfirmBlock:!1,showingConfirmRemoveFollower:!1}},components:{ProgressButton:w,Popover:C,UserListMenu:N,ConfirmModal:h(()=>x(()=>import(`./confirm_modal.f5dYyJF2.js`).then(e=>e.n),__vite__mapDeps([0,1,2,3,4,5,6,7,8,9,10,11,12]))),UserTimedFilterModal:h(()=>x(()=>import(`./user_timed_filter_modal.Njj7Gcae.js`).then(e=>e.n),__vite__mapDeps([13,1,2,14,15,16,6,3,7,8,4,9,10,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43])))},methods:{showConfirmRemoveUserFromFollowers(){this.showingConfirmRemoveFollower=!0},hideConfirmRemoveUserFromFollowers(){this.showingConfirmRemoveFollower=!1},hideConfirmBlock(){this.showingConfirmBlock=!1},showRepeats(){this.$store.dispatch(`showReblogs`,this.user.id)},hideRepeats(){this.$store.dispatch(`hideReblogs`,this.user.id)},blockUser(){this.$refs.timedBlockDialog?this.$refs.timedBlockDialog.optionallyPrompt():this.shouldConfirmBlock?this.showingConfirmBlock=!0:this.doBlockUser()},doBlockUser(){this.$store.dispatch(`blockUser`,{id:this.user.id}),this.hideConfirmBlock()},unblockUser(){this.$store.dispatch(`unblockUser`,this.user.id)},removeUserFromFollowers(){this.shouldConfirmRemoveUserFromFollowers?this.showConfirmRemoveUserFromFollowers():this.doRemoveUserFromFollowers()},doRemoveUserFromFollowers(){this.$store.dispatch(`removeUserFromFollowers`,this.user.id),this.hideConfirmRemoveUserFromFollowers()},reportUser(){E().openUserReportingModal({userId:this.user.id})},openChat(){this.$router.push({name:`chat`,params:{username:this.$store.state.users.currentUser.screen_name,recipient_id:this.user.id}})}},computed:{shouldConfirmBlock(){return _().mergedConfig.modalOnBlock},shouldConfirmRemoveUserFromFollowers(){return _().mergedConfig.modalOnRemoveUserFromFollowers},...g(S,[`blockExpiration`,`pleromaChatMessagesAvailable`])}},F={class:`AccountActions`},I={class:`dropdown-menu`},L={key:0,class:`menu-item dropdown-item`},R={key:1,class:`menu-item dropdown-item`},z={key:1,class:`menu-item dropdown-item`},B={class:`menu-item dropdown-item`},V={class:`menu-item dropdown-item`},H={key:2,class:`menu-item dropdown-item`},U={class:`button-unstyled ellipsis-button`},W=[`textContent`],G=[`textContent`];function K(e,c,f,p,h,g){let _=n(`UserListMenu`),v=n(`FAIcon`),y=n(`Popover`),b=n(`i18n-t`),x=n(`ConfirmModal`),S=n(`UserTimedFilterModal`);return t(),d(`div`,F,[a(y,{trigger:`click`,placement:`bottom`,"remove-padding":``},{content:r(()=>[m(`div`,I,[e.relationship.following?(t(),d(l,{key:0},[e.relationship.showing_reblogs?(t(),d(`div`,L,[m(`button`,{class:`main-button`,onClick:c[0]||=(...t)=>e.hideRepeats&&e.hideRepeats(...t)},i(e.$t(`user_card.hide_repeats`)),1)])):s(``,!0),e.relationship.showing_reblogs?s(``,!0):(t(),d(`div`,R,[m(`button`,{class:`main-button`,onClick:c[1]||=(...t)=>e.showRepeats&&e.showRepeats(...t)},i(e.$t(`user_card.show_repeats`)),1)])),c[7]||=m(`div`,{role:`separator`,class:`dropdown-divider`},null,-1)],64)):s(``,!0),a(_,{user:e.user},null,8,[`user`]),e.relationship.followed_by?(t(),d(`div`,z,[m(`button`,{class:`main-button`,onClick:c[2]||=(...t)=>e.removeUserFromFollowers&&e.removeUserFromFollowers(...t)},i(e.$t(`user_card.remove_follower`)),1)])):s(``,!0),m(`div`,B,[e.relationship.blocking?(t(),d(`button`,{key:0,class:`main-button`,onClick:c[3]||=(...t)=>e.unblockUser&&e.unblockUser(...t)},i(e.$t(`user_card.unblock`)),1)):(t(),d(`button`,{key:1,class:`main-button`,onClick:c[4]||=(...t)=>e.blockUser&&e.blockUser(...t)},i(e.$t(`user_card.block`)),1))]),m(`div`,V,[m(`button`,{class:`main-button`,onClick:c[5]||=(...t)=>e.reportUser&&e.reportUser(...t)},i(e.$t(`user_card.report`)),1)]),e.pleromaChatMessagesAvailable?(t(),d(`div`,H,[m(`button`,{class:`main-button`,onClick:c[6]||=(...t)=>e.openChat&&e.openChat(...t)},i(e.$t(`user_card.message`)),1)])):s(``,!0)])]),trigger:r(()=>[m(`button`,U,[a(v,{class:`icon`,icon:`ellipsis-v`})])]),_:1}),(t(),o(u,{to:`#modal`},[e.showingConfirmBlock&&!e.blockExpiration?(t(),o(x,{key:0,ref:`blockDialog`,title:e.$t(`user_card.block_confirm_title`),"confirm-text":e.$t(`user_card.block_confirm_accept_button`),"cancel-text":e.$t(`user_card.block_confirm_cancel_button`),onAccepted:e.doBlockUser,onCancelled:e.hideConfirmBlock},{default:r(()=>[a(b,{keypath:`user_card.block_confirm`,tag:`span`,scope:`global`},{user:r(()=>[m(`span`,{textContent:i(e.user.screen_name_ui)},null,8,W)]),_:1})]),_:1},8,[`title`,`confirm-text`,`cancel-text`,`onAccepted`,`onCancelled`])):s(``,!0)])),(t(),o(u,{to:`#modal`},[e.showingConfirmRemoveFollower?(t(),o(x,{key:0,title:e.$t(`user_card.remove_follower_confirm_title`),"confirm-text":e.$t(`user_card.remove_follower_confirm_accept_button`),"cancel-text":e.$t(`user_card.remove_follower_confirm_cancel_button`),onAccepted:e.doRemoveUserFromFollowers,onCancelled:e.hideConfirmRemoveUserFromFollowers},{default:r(()=>[a(b,{keypath:`user_card.remove_follower_confirm`,tag:`span`,scope:`global`},{user:r(()=>[m(`span`,{textContent:i(e.user.screen_name_ui)},null,8,G)]),_:1})]),_:1},8,[`title`,`confirm-text`,`cancel-text`,`onAccepted`,`onCancelled`])):s(``,!0),e.blockExpiration?(t(),o(S,{key:1,ref:`timedBlockDialog`,"is-mute":!1,user:e.user},null,8,[`user`])):s(``,!0)]))])}var q=p(P,[[`render`,K]]);export{q as default}; +//# sourceMappingURL=account_actions.iKah8h7y.js.map \ No newline at end of file diff --git a/priv/static/static/js/account_actions.iKah8h7y.js.map b/priv/static/static/js/account_actions.iKah8h7y.js.map new file mode 100644 index 000000000..97bf5e559 --- /dev/null +++ b/priv/static/static/js/account_actions.iKah8h7y.js.map @@ -0,0 +1 @@ +{"version":3,"mappings":";8iBASAA,EAAQ,IAAI,CAAc,EAE1B,IAAM,EAAe,CACnB,MAAO,CAAC,MAAM,EACd,MAAO,CACL,MAAO,CAAC,CACV,EACA,WAAY,CACV,SACF,EACA,SAAU,CACR,KAAK,OAAO,SAAS,mBAAoB,KAAK,KAAK,EAAE,CACvD,EACA,SAAU,CACR,GAAG,EAAS,EAAe,CACzB,SAAW,GAAU,EAAM,QAC7B,CAAC,EACD,YAAa,CACX,OAAO,IAAI,IAAI,KAAK,KAAK,QAAQ,IAAK,GAAM,EAAE,EAAE,CAAC,CACnD,EACA,OAAQ,CAEN,OADK,KAAK,KAAK,QACR,KAAK,SAAS,IAAK,IAAU,CAClC,GAAG,EACH,OAAQ,KAAK,WAAW,IAAI,EAAK,EAAE,CACrC,EAAE,EAJ6B,CAAC,CAKlC,EACA,cAAe,CACb,MAAO,CACL,MAAO,sCACT,CACF,CACF,EACA,QAAS,CACP,WAAW,EAAQ,CACb,KAAK,WAAW,IAAI,CAAM,EAC5B,EAAc,EACX,kBAAkB,CAAE,UAAW,KAAK,KAAK,GAAI,QAAO,CAAC,EACrD,KAAM,GAAa,CACb,EAAS,IAGd,KAAK,OAAO,SAAS,mBAAoB,KAAK,KAAK,EAAE,CACvD,CAAC,EAEH,EAAc,EACX,eAAe,CAAE,UAAW,KAAK,KAAK,GAAI,QAAO,CAAC,EAClD,KAAM,GAAa,CACb,EAAS,IAGd,KAAK,OAAO,SAAS,mBAAoB,KAAK,KAAK,EAAE,CACvD,CAAC,CAEP,CACF,CACF,KChEO,MAAM,cAAc,KAQd,MAAM,eAAe,mBAoBpB,MAAM,aAAa,sEA5B/B,EAsCM,MAtCN,EAsCM,CArCJ,EAoCU,GAnCR,QAAQ,QACR,UAAU,QACT,gBAAe,eAChB,sBAEW,QAAO,MAkBV,CAjBN,EAiBM,MAjBN,EAiBM,QAhBJ,EAeM,SAdW,QAAR,QADT,EAeM,OAbH,IAAK,EAAK,GACX,MAAM,kCAEN,EASS,UARP,MAAM,cACL,QAAK,GAAE,aAAW,EAAK,EAAE,IAE1B,EAGE,QAFA,MAAK,GAAC,sBAAqB,yBACQ,EAAK,MAAM,eAC9C,IACF,EAAG,EAAK,KAAK,yBAKV,QAAO,MAQT,CAPP,EAOO,OAPP,EAOO,KANF,KAAE,uBAAyB,IAC9B,KAIE,GAHA,MAAM,eACN,KAAK,KACL,KAAK,4ECpBjBA,EAAQ,IAAI,CAAW,EAEvB,IAAM,EAAiB,CACrB,MAAO,CAAC,OAAQ,cAAc,EAC9B,MAAO,CACL,MAAO,CACL,oBAAqB,GACrB,6BAA8B,EAChC,CACF,EACA,WAAY,CACV,iBACA,UACA,eACA,aAAc,YACN,OAAO,4FACf,EACA,qBAAsB,YAElB,OACE,yLAEN,CACF,EACA,QAAS,CACP,oCAAqC,CACnC,KAAK,6BAA+B,EACtC,EACA,oCAAqC,CACnC,KAAK,6BAA+B,EACtC,EACA,kBAAmB,CACjB,KAAK,oBAAsB,EAC7B,EACA,aAAc,CACZ,KAAK,OAAO,SAAS,cAAe,KAAK,KAAK,EAAE,CAClD,EACA,aAAc,CACZ,KAAK,OAAO,SAAS,cAAe,KAAK,KAAK,EAAE,CAClD,EACA,WAAY,CACN,KAAK,MAAM,iBACb,KAAK,MAAM,iBAAiB,iBAAiB,EAExC,KAAK,mBAGR,KAAK,oBAAsB,GAF3B,KAAK,YAAY,CAKvB,EACA,aAAc,CACZ,KAAK,OAAO,SAAS,YAAa,CAAE,GAAI,KAAK,KAAK,EAAG,CAAC,EACtD,KAAK,iBAAiB,CACxB,EACA,aAAc,CACZ,KAAK,OAAO,SAAS,cAAe,KAAK,KAAK,EAAE,CAClD,EACA,yBAA0B,CACnB,KAAK,qCAGR,KAAK,mCAAmC,EAFxC,KAAK,0BAA0B,CAInC,EACA,2BAA4B,CAC1B,KAAK,OAAO,SAAS,0BAA2B,KAAK,KAAK,EAAE,EAC5D,KAAK,mCAAmC,CAC1C,EACA,YAAa,CACX,EAAgB,EAAE,uBAAuB,CAAE,OAAQ,KAAK,KAAK,EAAG,CAAC,CACnE,EACA,UAAW,CACT,KAAK,QAAQ,KAAK,CAChB,KAAM,OACN,OAAQ,CACN,SAAU,KAAK,OAAO,MAAM,MAAM,YAAY,YAC9C,aAAc,KAAK,KAAK,EAC1B,CACF,CAAC,CACH,CACF,EACA,SAAU,CACR,oBAAqB,CACnB,OAAO,EAAqB,EAAE,aAAa,YAC7C,EACA,sCAAuC,CACrC,OAAO,EAAqB,EAAE,aAAa,8BAC7C,EACA,GAAG,EAAS,EAA8B,CACxC,kBACA,8BACF,CAAC,CACH,CACF,KC3GO,MAAM,gBAAgB,KAOhB,MAAM,eAAe,WAIpB,MAAM,oCAWN,MAAM,oCAiBR,MAAM,8BASH,MAAM,yBAAyB,KAgB/B,MAAM,yBAAyB,WAUlC,MAAM,8BAYF,MAAM,iCAAiC,4LAtFrD,EAiJM,MAjJN,EAiJM,CAhJJ,EA4FU,GA3FR,QAAQ,QACR,UAAU,SACV,sBAEW,QAAO,MA6EV,CA5EN,EA4EM,MA5EN,EA4EM,CA3EY,eAAa,eAA7B,EA2BW,WAzBD,eAAa,qBADrB,EAUM,MAVN,EAUM,CANJ,EAKS,UAJP,MAAM,cACL,QAAK,eAAE,sCAEL,KAAE,0CAIA,eAAa,gBAOb,UAPa,IADtB,EAUM,MAVN,EAUM,CANJ,EAKS,UAJP,MAAM,cACL,QAAK,eAAE,sCAEL,KAAE,wCAGT,EAGE,OAFA,KAAK,YACL,MAAM,4CAGV,EAA6B,GAAd,KAAM,MAAI,mBAEjB,eAAa,iBADrB,EAUM,MAVN,EAUM,CANJ,EAKS,UAJP,MAAM,cACL,QAAK,eAAE,8DAEL,KAAE,6CAGT,EAeM,MAfN,EAeM,CAbI,eAAa,cADrB,EAMS,gBAJP,MAAM,cACL,QAAK,eAAE,sCAEL,KAAE,+BAEP,EAMS,gBAJP,MAAM,cACL,QAAK,eAAE,kCAEL,KAAE,0BAGT,EAOM,MAPN,EAOM,CANJ,EAKS,UAJP,MAAM,cACL,QAAK,eAAE,oCAEL,KAAE,0BAID,oCADR,EAUM,MAVN,EAUM,CANJ,EAKS,UAJP,MAAM,cACL,QAAK,eAAE,gCAEL,KAAE,yCAKF,QAAO,MAMP,CALT,EAKS,SALT,EAKS,CAJP,EAGE,GAFA,MAAM,OACN,KAAK,8BAKb,EAsBW,GAtBD,GAAG,QAAQ,GAEX,uBAAmB,CAAK,uBADhC,EAoBe,SAlBb,IAAI,cACH,MAAO,KAAE,iCACT,eAAc,KAAE,yCAChB,cAAa,KAAE,yCACf,WAAU,cACV,YAAW,mCAYH,CAVT,EAUS,GATP,QAAQ,0BACR,IAAI,OACJ,MAAM,WAEK,KAAI,MAGX,CAFF,EAEE,oBADA,EAA4B,EAApB,KAAK,cAAc,mHAMrC,EA2BW,GA3BD,GAAG,QAAQ,GAEX,oCADR,EAmBe,SAjBZ,MAAO,KAAE,2CACT,eAAc,KAAE,mDAChB,cAAa,KAAE,mDACf,WAAU,4BACV,YAAW,qDAYH,CAVT,EAUS,GATP,QAAQ,oCACR,IAAI,OACJ,MAAM,WAEK,KAAI,MAGX,CAFF,EAEE,oBADA,EAA4B,EAApB,KAAK,cAAc,2GAM3B,uBADR,EAKE,SAHA,IAAI,mBACH,UAAS,GACT,KAAM","names":["library"],"ignoreList":[],"sources":["../../../src/components/user_list_menu/user_list_menu.js?vue&type=script&src=true&lang.js","../../../src/components/user_list_menu/user_list_menu.vue","../../../src/components/account_actions/account_actions.js?vue&type=script&src=true&lang.js","../../../src/components/account_actions/account_actions.vue"],"sourcesContent":["import { mapState } from 'pinia'\n\nimport Popover from 'src/components/popover/popover.vue'\n\nimport { useListsStore } from 'src/stores/lists.js'\n\nimport { library } from '@fortawesome/fontawesome-svg-core'\nimport { faChevronRight } from '@fortawesome/free-solid-svg-icons'\n\nlibrary.add(faChevronRight)\n\nconst UserListMenu = {\n props: ['user'],\n data() {\n return {}\n },\n components: {\n Popover,\n },\n created() {\n this.$store.dispatch('fetchUserInLists', this.user.id)\n },\n computed: {\n ...mapState(useListsStore, {\n allLists: (store) => store.allLists,\n }),\n inListsSet() {\n return new Set(this.user.inLists.map((x) => x.id))\n },\n lists() {\n if (!this.user.inLists) return []\n return this.allLists.map((list) => ({\n ...list,\n inList: this.inListsSet.has(list.id),\n }))\n },\n triggerAttrs() {\n return {\n class: 'menu-item dropdown-item -has-submenu',\n }\n },\n },\n methods: {\n toggleList(listId) {\n if (this.inListsSet.has(listId)) {\n useListsStore()\n .removeListAccount({ accountId: this.user.id, listId })\n .then((response) => {\n if (!response.ok) {\n return\n }\n this.$store.dispatch('fetchUserInLists', this.user.id)\n })\n } else {\n useListsStore()\n .addListAccount({ accountId: this.user.id, listId })\n .then((response) => {\n if (!response.ok) {\n return\n }\n this.$store.dispatch('fetchUserInLists', this.user.id)\n })\n }\n },\n },\n}\n\nexport default UserListMenu\n","\n\n\n","import { mapState } from 'pinia'\nimport { defineAsyncComponent } from 'vue'\n\nimport Popover from 'src/components/popover/popover.vue'\nimport ProgressButton from 'src/components/progress_button/progress_button.vue'\nimport UserListMenu from 'src/components/user_list_menu/user_list_menu.vue'\n\nimport { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'\nimport { useMergedConfigStore } from 'src/stores/merged_config.js'\nimport { useReportsStore } from 'src/stores/reports'\n\nimport { library } from '@fortawesome/fontawesome-svg-core'\nimport { faEllipsisV } from '@fortawesome/free-solid-svg-icons'\n\nlibrary.add(faEllipsisV)\n\nconst AccountActions = {\n props: ['user', 'relationship'],\n data() {\n return {\n showingConfirmBlock: false,\n showingConfirmRemoveFollower: false,\n }\n },\n components: {\n ProgressButton,\n Popover,\n UserListMenu,\n ConfirmModal: defineAsyncComponent(\n () => import('src/components/confirm_modal/confirm_modal.vue'),\n ),\n UserTimedFilterModal: defineAsyncComponent(\n () =>\n import(\n 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'\n ),\n ),\n },\n methods: {\n showConfirmRemoveUserFromFollowers() {\n this.showingConfirmRemoveFollower = true\n },\n hideConfirmRemoveUserFromFollowers() {\n this.showingConfirmRemoveFollower = false\n },\n hideConfirmBlock() {\n this.showingConfirmBlock = false\n },\n showRepeats() {\n this.$store.dispatch('showReblogs', this.user.id)\n },\n hideRepeats() {\n this.$store.dispatch('hideReblogs', this.user.id)\n },\n blockUser() {\n if (this.$refs.timedBlockDialog) {\n this.$refs.timedBlockDialog.optionallyPrompt()\n } else {\n if (!this.shouldConfirmBlock) {\n this.doBlockUser()\n } else {\n this.showingConfirmBlock = true\n }\n }\n },\n doBlockUser() {\n this.$store.dispatch('blockUser', { id: this.user.id })\n this.hideConfirmBlock()\n },\n unblockUser() {\n this.$store.dispatch('unblockUser', this.user.id)\n },\n removeUserFromFollowers() {\n if (!this.shouldConfirmRemoveUserFromFollowers) {\n this.doRemoveUserFromFollowers()\n } else {\n this.showConfirmRemoveUserFromFollowers()\n }\n },\n doRemoveUserFromFollowers() {\n this.$store.dispatch('removeUserFromFollowers', this.user.id)\n this.hideConfirmRemoveUserFromFollowers()\n },\n reportUser() {\n useReportsStore().openUserReportingModal({ userId: this.user.id })\n },\n openChat() {\n this.$router.push({\n name: 'chat',\n params: {\n username: this.$store.state.users.currentUser.screen_name,\n recipient_id: this.user.id,\n },\n })\n },\n },\n computed: {\n shouldConfirmBlock() {\n return useMergedConfigStore().mergedConfig.modalOnBlock\n },\n shouldConfirmRemoveUserFromFollowers() {\n return useMergedConfigStore().mergedConfig.modalOnRemoveUserFromFollowers\n },\n ...mapState(useInstanceCapabilitiesStore, [\n 'blockExpiration',\n 'pleromaChatMessagesAvailable',\n ]),\n },\n}\n\nexport default AccountActions\n","\n\n\n\n\n"],"file":"account_actions.iKah8h7y.js"} \ No newline at end of file diff --git a/priv/static/static/js/admin.Dy10Wzep.js b/priv/static/static/js/admin.Dy10Wzep.js new file mode 100644 index 000000000..811623906 --- /dev/null +++ b/priv/static/static/js/admin.Dy10Wzep.js @@ -0,0 +1,2 @@ +import{n as e}from"./chunk.aKtaBQYM.js";import{n as t}from"./helpers.DO6M90bq.js";var n=e({addNewEmojiFile:()=>Z,announcementToPayload:()=>F,changeStatusScope:()=>P,createEmojiPack:()=>q,deleteAccounts:()=>O,deleteAnnouncement:()=>L,deleteEmojiFile:()=>$,deleteEmojiPack:()=>W,disableMFA:()=>M,downloadRemoteEmojiPack:()=>Y,downloadRemoteEmojiPackZIP:()=>X,editAnnouncement:()=>I,getAnnouncements:()=>ue,getAvailableFrontends:()=>V,getInstanceConfigDescriptions:()=>B,getInstanceDBConfig:()=>z,getUserData:()=>D,importEmojiFromFS:()=>K,installFrontend:()=>U,listRemoteEmojiPacks:()=>J,listStatuses:()=>N,listUsers:()=>k,postAnnouncement:()=>de,pushInstanceDBConfig:()=>H,reloadEmoji:()=>G,requirePasswordChange:()=>j,resendConfirmationEmail:()=>A,saveEmojiPackMetadata:()=>fe,setReportState:()=>R,setUsersActivationStatus:()=>C,setUsersApprovalStatus:()=>w,setUsersConfirmationStatus:()=>T,setUsersRight:()=>S,setUsersSuggestionStatus:()=>E,setUsersTags:()=>x,updateEmojiFile:()=>Q}),r=`/api/v1/pleroma/admin/reports`,i=`/api/v1/pleroma/admin/config`,a=`/api/v1/pleroma/admin/config/descriptions`,o=(e=``)=>`/api/v1/pleroma/admin/announcements/${e}`,s=`/api/v1/pleroma/admin/frontends`,c=`/api/v1/pleroma/admin/frontends/install`,l=(e=``)=>`/api/v1/pleroma/admin/users/${e}`,u=({page:e,pageSize:t,filters:n={},query:r=``,name:i=``,email:a=``})=>{let{local:o=!1,external:s=!1,active:c=!1,needApproval:l=!1,unconfirmed:u=!1,deactivated:d=!1,isAdmin:f=!0,isModerator:p=!0}=n;return`/api/v1/pleroma/admin/users?page=${e}&page_size=${t}&filters=${[o&&`local`,s&&`external`,c&&`active`,l&&`need_approval`,u&&`unconfirmed`,d&&`deactivated`,f&&`is_admin`,p&&`is_moderator`].filter(e=>e).join(`,`)}&query=${r}&name=${i}&email=${a}`},d=`/api/pleroma/admin/users/tag`,f=e=>`/api/pleroma/admin/users/permission_group/${e}`,p=`/api/pleroma/admin/users/activate`,m=`/api/pleroma/admin/users/deactivate`,ee=`/api/pleroma/admin/users/suggest`,te=`/api/pleroma/admin/users/unsuggest`,ne=`/api/v1/pleroma/admin/users/approve`,h=`/api/v1/pleroma/admin/users/confirm_email`,g=`/api/v1/pleroma/admin/users/resend_confirmation_email`,_=({id:e,page:t,pageSize:n,godmode:r,withReblogs:i})=>`/api/v1/pleroma/admin/users/${e}/statuses?page_size=${n}&page=${t}&godmode=${r}&with_reblogs=${i}`,v=e=>`/api/v1/pleroma/admin/statuses/${e}`,re=`/api/v1/pleroma/admin/users/force_password_reset`,ie=`/api/v1/pleroma/admin/users/disable_mfa`,ae=`/api/pleroma/admin/reload_emoji`,oe=`/api/pleroma/emoji/packs/import`,y=e=>`/api/v1/pleroma/emoji/pack?name=${e}`,se=`/api/v1/pleroma/emoji/packs/download`,ce=`/api/v1/pleroma/emoji/packs/download_zip`,le=(e,t,n)=>`/api/v1/pleroma/emoji/packs/remote?url=${e}&page=${t}&page_size=${n}`,b=e=>`/api/v1/pleroma/emoji/packs/files?name=${e}`,x=({tags:e,credentials:n,value:r,screen_names:i})=>t({url:d,method:r?`PUT`:`DELETE`,credentials:n,payload:{nicknames:i,tags:e}}),S=({right:e,credentials:n,value:r,screen_names:i})=>t({url:f(e),method:r?`POST`:`DELETE`,credentials:n,payload:{nicknames:i}}),C=({credentials:e,screen_names:n,value:r})=>t({url:r?p:m,method:`PATCH`,credentials:e,payload:{nicknames:n}}).then(e=>e.users),w=({credentials:e,screen_names:n})=>t({url:ne,method:`PATCH`,credentials:e,payload:{nicknames:n}}).then(e=>e.users),T=({credentials:e,screen_names:n})=>t({url:h,method:`PATCH`,credentials:e,payload:{nicknames:n}}).then(e=>e.users),E=({credentials:e,screen_names:n,value:r})=>t({url:r?ee:te,method:`PATCH`,credentials:e,payload:{nicknames:n}}).then(e=>e.users),D=({credentials:e,screen_name:n})=>t({url:l(n),method:`GET`,credentials:e}),O=({credentials:e,screen_names:n})=>t({url:l(),method:`DELETE`,credentials:e,payload:{nicknames:n}}),ue=({id:e,credentials:n})=>t({url:o(e),credentials:n}),k=({opts:e,credentials:n})=>t({url:u(e),credentials:n,method:`GET`}),A=({screen_names:e,credentials:n})=>t({url:g,credentials:n,method:`PATCH`,payload:{nicknames:e}}),j=({screen_names:e,credentials:n})=>t({url:re,credentials:n,method:`PATCH`,payload:{nicknames:e}}),M=({screen_name:e,credentials:n})=>t({url:ie,credentials:n,method:`PUT`,payload:{nickname:e}}),N=({opts:e,credentials:n})=>t({url:_(e),credentials:n,method:`GET`}),P=({opts:{id:e,sensitive:n,visibility:r},credentials:i})=>{var a={};return n!==void 0&&(a.sensitive=n),r!==void 0&&(a.visibility=r),t({url:v(e),credentials:i,method:`PUT`,payload:a})},F=({content:e,startsAt:t,endsAt:n,allDay:r})=>{let i={content:e};return t!==void 0&&(i.starts_at=t?new Date(t).toISOString():null),n!==void 0&&(i.ends_at=n?new Date(n).toISOString():null),r!==void 0&&(i.all_day=r),i},de=({credentials:e,content:n,startsAt:r,endsAt:i,allDay:a})=>t({url:o(),credentials:e,method:`POST`,payload:F({content:n,startsAt:r,endsAt:i,allDay:a})}),I=({id:e,credentials:n,content:r,startsAt:i,endsAt:a,allDay:s})=>t({url:o(e),credentials:n,method:`PATCH`,payload:F({content:r,startsAt:i,endsAt:a,allDay:s})}),L=({id:e,credentials:n})=>t({url:o(e),credentials:n,method:`DELETE`}),R=({id:e,state:n,credentials:i})=>t({url:r,credentials:i,method:`PATCH`,payload:{reports:[{id:e,state:n}]}}),z=({credentials:e})=>t({url:i,credentials:e}),B=({credentials:e})=>t({url:a,credentials:e}),V=({credentials:e})=>t({url:s,credentials:e}),H=({credentials:e,payload:n})=>t({url:i,method:`POST`,credentials:e,payload:n}),U=({credentials:e,payload:n})=>t({url:c,credentials:e,method:`POST`,payload:n}),W=({name:e})=>t({url:y(e),method:`DELETE`}),G=({credentials:e})=>t({url:ae,method:`POST`,credentials:e}),K=({credentials:e})=>t({url:oe,credentials:e}),q=({name:e,credentials:n})=>t({url:y(e),method:`POST`,credentials:n}),J=({instance:e,page:n,pageSize:r,credentials:i})=>(e.startsWith(`http`)||(e=`https://`+e),t({url:le(e,n,r),credentials:i})),Y=({instance:e,packName:n,as:r,credentials:i})=>t({url:se,credentials:i,method:`POST`,payload:{url:e,name:n,as:r}}),X=({url:e,packName:n,file:r,credentials:i})=>{let a=new FormData;return r&&a.set(`file`,r),e&&a.set(`url`,e),a.set(`name`,n),t({url:ce,method:`POST`,formData:a})},fe=({name:e,newData:n,credentials:r})=>t({url:y(e),credentials:r,method:`PATCH`,payload:{metadata:n}}),Z=({packName:e,file:n,shortcode:r,filename:i})=>{let a=new FormData;return i.trim()!==``&&a.set(`filename`,i),r.trim()!==``&&a.set(`shortcode`,r),a.set(`file`,n),t({url:b(e),method:`POST`,formData:a})},Q=({packName:e,shortcode:n,newShortcode:r,newFilename:i,credentials:a,force:o})=>t({url:b(e),credentials:a,method:`PATCH`,payload:{shortcode:n,new_shortcode:r,new_filename:i,force:o}}),$=({packName:e,shortcode:n})=>t({url:`${b(e)}&shortcode=${n}`,method:`DELETE`});export{x as A,A as C,T as D,w as E,S as O,j as S,C as T,J as _,O as a,H as b,M as c,V as d,B as f,U as g,K as h,q as i,Q as j,E as k,Y as l,D as m,n,$ as o,z as p,P as r,W as s,Z as t,X as u,N as v,R as w,G as x,k as y}; +//# sourceMappingURL=admin.Dy10Wzep.js.map \ No newline at end of file diff --git a/priv/static/static/js/admin.Dy10Wzep.js.map b/priv/static/static/js/admin.Dy10Wzep.js.map new file mode 100644 index 000000000..2b9cf5feb --- /dev/null +++ b/priv/static/static/js/admin.Dy10Wzep.js.map @@ -0,0 +1 @@ +{"version":3,"file":"admin.Dy10Wzep.js","names":[],"sources":["../../../src/api/admin.js"],"sourcesContent":["import { promisedRequest } from './helpers.js'\n\nconst REPORTS = '/api/v1/pleroma/admin/reports'\nconst CONFIG_URL = '/api/v1/pleroma/admin/config'\nconst DESCRIPTIONS_URL = '/api/v1/pleroma/admin/config/descriptions'\n\nconst ANNOUNCEMENTS_URL = (id = '') =>\n `/api/v1/pleroma/admin/announcements/${id}`\n\nconst FRONTENDS_URL = '/api/v1/pleroma/admin/frontends'\nconst FRONTENDS_INSTALL_URL = '/api/v1/pleroma/admin/frontends/install'\n\nconst USERS_URL = (nickname = '') => `/api/v1/pleroma/admin/users/${nickname}`\nconst USERS_URL_LIST = ({\n page,\n pageSize,\n filters = {},\n query = '',\n name = '',\n email = '',\n}) => {\n const {\n local = false,\n external = false,\n active = false,\n needApproval = false,\n unconfirmed = false,\n deactivated = false,\n isAdmin = true,\n isModerator = true,\n } = filters\n const filters_str = [\n local && 'local',\n external && 'external',\n active && 'active',\n needApproval && 'need_approval',\n unconfirmed && 'unconfirmed',\n deactivated && 'deactivated',\n isAdmin && 'is_admin',\n isModerator && 'is_moderator',\n ]\n .filter((x) => x)\n .join(',')\n return `/api/v1/pleroma/admin/users?page=${page}&page_size=${pageSize}&filters=${filters_str}&query=${query}&name=${name}&email=${email}`\n}\n\nconst TAG_USER_URL = '/api/pleroma/admin/users/tag'\n\nconst PERMISSION_GROUP_URL = (right) =>\n `/api/pleroma/admin/users/permission_group/${right}`\nconst ACTIVATE_USERS_URL = '/api/pleroma/admin/users/activate'\nconst DEACTIVATE_USERS_URL = '/api/pleroma/admin/users/deactivate'\nconst SUGGEST_USERS_URL = '/api/pleroma/admin/users/suggest'\nconst UNSUGGEST_USERS_URL = '/api/pleroma/admin/users/unsuggest'\nconst APPROVE_USERS_URL = '/api/v1/pleroma/admin/users/approve'\nconst CONFIRM_USERS_URL = '/api/v1/pleroma/admin/users/confirm_email'\nconst RESEND_CONFIRMATION_EMAIL_URL =\n '/api/v1/pleroma/admin/users/resend_confirmation_email'\nconst LIST_STATUSES_URL = ({ id, page, pageSize, godmode, withReblogs }) =>\n `/api/v1/pleroma/admin/users/${id}/statuses?page_size=${pageSize}&page=${page}&godmode=${godmode}&with_reblogs=${withReblogs}`\nconst CHANGE_STATUS_SCOPE_URL = (id) => `/api/v1/pleroma/admin/statuses/${id}`\nconst REQUIRE_PASSWORD_CHANGE_URL =\n '/api/v1/pleroma/admin/users/force_password_reset'\n\nconst DISABLE_MFA_URL = '/api/v1/pleroma/admin/users/disable_mfa'\nconst EMOJI_RELOAD_URL = '/api/pleroma/admin/reload_emoji'\nconst EMOJI_IMPORT_FS_URL = '/api/pleroma/emoji/packs/import'\nconst EMOJI_PACK_URL = (name) => `/api/v1/pleroma/emoji/pack?name=${name}`\nconst EMOJI_PACKS_DL_REMOTE_URL = '/api/v1/pleroma/emoji/packs/download'\nconst EMOJI_PACKS_DL_REMOTE_ZIP_URL = '/api/v1/pleroma/emoji/packs/download_zip'\nconst EMOJI_PACKS_LS_REMOTE_URL = (url, page, pageSize) =>\n `/api/v1/pleroma/emoji/packs/remote?url=${url}&page=${page}&page_size=${pageSize}`\nconst EMOJI_UPDATE_FILE_URL = (name) =>\n `/api/v1/pleroma/emoji/packs/files?name=${name}`\n\n//\n\nexport const setUsersTags = ({\n tags,\n credentials,\n value,\n screen_names: nicknames,\n}) =>\n promisedRequest({\n url: TAG_USER_URL,\n method: value ? 'PUT' : 'DELETE',\n credentials,\n payload: {\n nicknames,\n tags,\n },\n })\n\nexport const setUsersRight = ({\n right,\n credentials,\n value,\n screen_names: nicknames,\n}) =>\n promisedRequest({\n url: PERMISSION_GROUP_URL(right),\n method: value ? 'POST' : 'DELETE',\n credentials,\n payload: {\n nicknames,\n },\n })\n\nexport const setUsersActivationStatus = ({\n credentials,\n screen_names: nicknames,\n value,\n}) =>\n promisedRequest({\n url: value ? ACTIVATE_USERS_URL : DEACTIVATE_USERS_URL,\n method: 'PATCH',\n credentials,\n payload: {\n nicknames,\n },\n }).then((response) => response.users)\n\nexport const setUsersApprovalStatus = ({\n credentials,\n screen_names: nicknames,\n}) =>\n promisedRequest({\n url: APPROVE_USERS_URL,\n method: 'PATCH',\n credentials,\n payload: {\n nicknames,\n },\n }).then((response) => response.users)\n\nexport const setUsersConfirmationStatus = ({\n credentials,\n screen_names: nicknames,\n}) =>\n promisedRequest({\n url: CONFIRM_USERS_URL,\n method: 'PATCH',\n credentials,\n payload: {\n nicknames,\n },\n }).then((response) => response.users)\n\nexport const setUsersSuggestionStatus = ({\n credentials,\n screen_names: nicknames,\n value,\n}) =>\n promisedRequest({\n url: value ? SUGGEST_USERS_URL : UNSUGGEST_USERS_URL,\n method: 'PATCH',\n credentials,\n payload: {\n nicknames,\n },\n }).then((response) => response.users)\n\nexport const getUserData = ({ credentials, screen_name: nickname }) =>\n promisedRequest({\n url: USERS_URL(nickname),\n method: 'GET',\n credentials,\n })\n\nexport const deleteAccounts = ({ credentials, screen_names: nicknames }) =>\n promisedRequest({\n url: USERS_URL(),\n method: 'DELETE',\n credentials,\n payload: {\n nicknames,\n },\n })\n\nexport const getAnnouncements = ({ id, credentials }) =>\n promisedRequest({ url: ANNOUNCEMENTS_URL(id), credentials })\n\n// the reported list is hardly useful because standards are for dating i guess,\n// so make sure to fetchIfMissing right afterward using this call\nexport const listUsers = ({ opts, credentials }) =>\n promisedRequest({\n url: USERS_URL_LIST(opts),\n credentials,\n method: 'GET',\n })\n\nexport const resendConfirmationEmail = ({\n screen_names: nicknames,\n credentials,\n}) =>\n promisedRequest({\n url: RESEND_CONFIRMATION_EMAIL_URL,\n credentials,\n method: 'PATCH',\n payload: {\n nicknames,\n },\n })\n\nexport const requirePasswordChange = ({\n screen_names: nicknames,\n credentials,\n}) =>\n promisedRequest({\n url: REQUIRE_PASSWORD_CHANGE_URL,\n credentials,\n method: 'PATCH',\n payload: {\n nicknames,\n },\n })\n\nexport const disableMFA = ({ screen_name: nickname, credentials }) =>\n promisedRequest({\n url: DISABLE_MFA_URL,\n credentials,\n method: 'PUT',\n payload: {\n nickname,\n },\n })\n\nexport const listStatuses = ({ opts, credentials }) =>\n promisedRequest({\n url: LIST_STATUSES_URL(opts),\n credentials,\n method: 'GET',\n })\n\nexport const changeStatusScope = ({\n opts: { id, sensitive, visibility },\n credentials,\n}) => {\n var payload = {}\n if (typeof sensitive !== 'undefined') {\n payload['sensitive'] = sensitive\n }\n if (typeof visibility !== 'undefined') {\n payload['visibility'] = visibility\n }\n\n return promisedRequest({\n url: CHANGE_STATUS_SCOPE_URL(id),\n credentials,\n method: 'PUT',\n payload,\n })\n}\n\nexport const announcementToPayload = ({\n content,\n startsAt,\n endsAt,\n allDay,\n}) => {\n const payload = { content }\n\n if (typeof startsAt !== 'undefined') {\n payload.starts_at = startsAt ? new Date(startsAt).toISOString() : null\n }\n\n if (typeof endsAt !== 'undefined') {\n payload.ends_at = endsAt ? new Date(endsAt).toISOString() : null\n }\n\n if (typeof allDay !== 'undefined') {\n payload.all_day = allDay\n }\n\n return payload\n}\n\nexport const postAnnouncement = ({\n credentials,\n content,\n startsAt,\n endsAt,\n allDay,\n}) =>\n promisedRequest({\n url: ANNOUNCEMENTS_URL(),\n credentials,\n method: 'POST',\n payload: announcementToPayload({ content, startsAt, endsAt, allDay }),\n })\n\nexport const editAnnouncement = ({\n id,\n credentials,\n content,\n startsAt,\n endsAt,\n allDay,\n}) =>\n promisedRequest({\n url: ANNOUNCEMENTS_URL(id),\n credentials,\n method: 'PATCH',\n payload: announcementToPayload({ content, startsAt, endsAt, allDay }),\n })\n\nexport const deleteAnnouncement = ({ id, credentials }) =>\n promisedRequest({\n url: ANNOUNCEMENTS_URL(id),\n credentials,\n method: 'DELETE',\n })\n\nexport const setReportState = ({ id, state, credentials }) => {\n return promisedRequest({\n url: REPORTS,\n credentials,\n method: 'PATCH',\n payload: {\n reports: [\n {\n id,\n state,\n },\n ],\n },\n })\n}\n\nexport const getInstanceDBConfig = ({ credentials }) =>\n promisedRequest({\n url: CONFIG_URL,\n credentials,\n })\n\nexport const getInstanceConfigDescriptions = ({ credentials }) =>\n promisedRequest({\n url: DESCRIPTIONS_URL,\n credentials,\n })\n\nexport const getAvailableFrontends = ({ credentials }) =>\n promisedRequest({\n url: FRONTENDS_URL,\n credentials,\n })\n\nexport const pushInstanceDBConfig = ({ credentials, payload }) =>\n promisedRequest({\n url: CONFIG_URL,\n method: 'POST',\n credentials,\n payload,\n })\n\nexport const installFrontend = ({ credentials, payload }) =>\n promisedRequest({\n url: FRONTENDS_INSTALL_URL,\n credentials,\n method: 'POST',\n payload,\n })\n\n// Emoji packs\nexport const deleteEmojiPack = ({ name }) =>\n promisedRequest({\n url: EMOJI_PACK_URL(name),\n method: 'DELETE',\n })\n\nexport const reloadEmoji = ({ credentials }) =>\n promisedRequest({\n url: EMOJI_RELOAD_URL,\n method: 'POST',\n credentials,\n })\n\nexport const importEmojiFromFS = ({ credentials }) =>\n promisedRequest({\n url: EMOJI_IMPORT_FS_URL,\n credentials,\n })\n\nexport const createEmojiPack = ({ name, credentials }) =>\n promisedRequest({\n url: EMOJI_PACK_URL(name),\n method: 'POST',\n credentials,\n })\n\nexport const listRemoteEmojiPacks = ({\n instance,\n page,\n pageSize,\n credentials,\n}) => {\n if (!instance.startsWith('http')) {\n instance = 'https://' + instance\n }\n\n return promisedRequest({\n url: EMOJI_PACKS_LS_REMOTE_URL(instance, page, pageSize),\n credentials,\n })\n}\n\nexport const downloadRemoteEmojiPack = ({\n instance,\n packName,\n as,\n credentials,\n}) =>\n promisedRequest({\n url: EMOJI_PACKS_DL_REMOTE_URL,\n credentials,\n method: 'POST',\n payload: {\n url: instance,\n name: packName,\n as,\n },\n })\n\nexport const downloadRemoteEmojiPackZIP = ({\n url,\n packName,\n file,\n credentials,\n}) => {\n const data = new FormData()\n if (file) data.set('file', file)\n if (url) data.set('url', url)\n data.set('name', packName)\n\n return promisedRequest({\n url: EMOJI_PACKS_DL_REMOTE_ZIP_URL,\n method: 'POST',\n formData: data,\n })\n}\n\nexport const saveEmojiPackMetadata = ({ name, newData, credentials }) =>\n promisedRequest({\n url: EMOJI_PACK_URL(name),\n credentials,\n method: 'PATCH',\n payload: { metadata: newData },\n })\n\nexport const addNewEmojiFile = ({ packName, file, shortcode, filename }) => {\n const data = new FormData()\n if (filename.trim() !== '') {\n data.set('filename', filename)\n }\n if (shortcode.trim() !== '') {\n data.set('shortcode', shortcode)\n }\n data.set('file', file)\n\n return promisedRequest({\n url: EMOJI_UPDATE_FILE_URL(packName),\n method: 'POST',\n formData: data,\n })\n}\n\nexport const updateEmojiFile = ({\n packName,\n shortcode,\n newShortcode,\n newFilename,\n credentials,\n force,\n}) =>\n promisedRequest({\n url: EMOJI_UPDATE_FILE_URL(packName),\n credentials,\n method: 'PATCH',\n payload: {\n shortcode,\n new_shortcode: newShortcode,\n new_filename: newFilename,\n force,\n },\n })\n\nexport const deleteEmojiFile = ({ packName, shortcode }) =>\n promisedRequest({\n url: `${EMOJI_UPDATE_FILE_URL(packName)}&shortcode=${shortcode}`,\n method: 'DELETE',\n })\n"],"mappings":"w9BAEM,EAAU,gCACV,EAAa,+BACb,EAAmB,4CAEnB,GAAqB,EAAK,KAC9B,uCAAuC,IAEnC,EAAgB,kCAChB,EAAwB,0CAExB,GAAa,EAAW,KAAO,+BAA+B,IAC9D,GAAkB,CACtB,OACA,WACA,UAAU,CAAC,EACX,QAAQ,GACR,OAAO,GACP,QAAQ,MACJ,CACJ,GAAM,CACJ,QAAQ,GACR,WAAW,GACX,SAAS,GACT,eAAe,GACf,cAAc,GACd,cAAc,GACd,UAAU,GACV,cAAc,IACZ,EAaJ,MAAO,oCAAoC,EAAK,aAAa,EAAS,WAZlD,CAClB,GAAS,QACT,GAAY,WACZ,GAAU,SACV,GAAgB,gBAChB,GAAe,cACf,GAAe,cACf,GAAW,WACX,GAAe,cACjB,EACG,OAAQ,GAAM,CAAC,EACf,KAAK,GACmF,EAAE,SAAS,EAAM,QAAQ,EAAK,SAAS,GACpI,EAEM,EAAe,+BAEf,EAAwB,GAC5B,6CAA6C,IACzC,EAAqB,oCACrB,EAAuB,sCACvB,GAAoB,mCACpB,GAAsB,qCACtB,GAAoB,sCACpB,EAAoB,4CACpB,EACJ,wDACI,GAAqB,CAAE,KAAI,OAAM,WAAU,UAAS,iBACxD,+BAA+B,EAAG,sBAAsB,EAAS,QAAQ,EAAK,WAAW,EAAQ,gBAAgB,IAC7G,EAA2B,GAAO,kCAAkC,IACpE,GACJ,mDAEI,GAAkB,0CAClB,GAAmB,kCACnB,GAAsB,kCACtB,EAAkB,GAAS,mCAAmC,IAC9D,GAA4B,uCAC5B,GAAgC,2CAChC,IAA6B,EAAK,EAAM,IAC5C,0CAA0C,EAAI,QAAQ,EAAK,aAAa,IACpE,EAAyB,GAC7B,0CAA0C,IAI/B,GAAgB,CAC3B,OACA,cACA,QACA,aAAc,KAEd,EAAgB,CACd,IAAK,EACL,OAAQ,EAAQ,MAAQ,SACxB,cACA,QAAS,CACP,YACA,MACF,CACF,CAAC,EAEU,GAAiB,CAC5B,QACA,cACA,QACA,aAAc,KAEd,EAAgB,CACd,IAAK,EAAqB,CAAK,EAC/B,OAAQ,EAAQ,OAAS,SACzB,cACA,QAAS,CACP,WACF,CACF,CAAC,EAEU,GAA4B,CACvC,cACA,aAAc,EACd,WAEA,EAAgB,CACd,IAAK,EAAQ,EAAqB,EAClC,OAAQ,QACR,cACA,QAAS,CACP,WACF,CACF,CAAC,EAAE,KAAM,GAAa,EAAS,KAAK,EAEzB,GAA0B,CACrC,cACA,aAAc,KAEd,EAAgB,CACd,IAAK,GACL,OAAQ,QACR,cACA,QAAS,CACP,WACF,CACF,CAAC,EAAE,KAAM,GAAa,EAAS,KAAK,EAEzB,GAA8B,CACzC,cACA,aAAc,KAEd,EAAgB,CACd,IAAK,EACL,OAAQ,QACR,cACA,QAAS,CACP,WACF,CACF,CAAC,EAAE,KAAM,GAAa,EAAS,KAAK,EAEzB,GAA4B,CACvC,cACA,aAAc,EACd,WAEA,EAAgB,CACd,IAAK,EAAQ,GAAoB,GACjC,OAAQ,QACR,cACA,QAAS,CACP,WACF,CACF,CAAC,EAAE,KAAM,GAAa,EAAS,KAAK,EAEzB,GAAe,CAAE,cAAa,YAAa,KACtD,EAAgB,CACd,IAAK,EAAU,CAAQ,EACvB,OAAQ,MACR,aACF,CAAC,EAEU,GAAkB,CAAE,cAAa,aAAc,KAC1D,EAAgB,CACd,IAAK,EAAU,EACf,OAAQ,SACR,cACA,QAAS,CACP,WACF,CACF,CAAC,EAEU,IAAoB,CAAE,KAAI,iBACrC,EAAgB,CAAE,IAAK,EAAkB,CAAE,EAAG,aAAY,CAAC,EAIhD,GAAa,CAAE,OAAM,iBAChC,EAAgB,CACd,IAAK,EAAe,CAAI,EACxB,cACA,OAAQ,KACV,CAAC,EAEU,GAA2B,CACtC,aAAc,EACd,iBAEA,EAAgB,CACd,IAAK,EACL,cACA,OAAQ,QACR,QAAS,CACP,WACF,CACF,CAAC,EAEU,GAAyB,CACpC,aAAc,EACd,iBAEA,EAAgB,CACd,IAAK,GACL,cACA,OAAQ,QACR,QAAS,CACP,WACF,CACF,CAAC,EAEU,GAAc,CAAE,YAAa,EAAU,iBAClD,EAAgB,CACd,IAAK,GACL,cACA,OAAQ,MACR,QAAS,CACP,UACF,CACF,CAAC,EAEU,GAAgB,CAAE,OAAM,iBACnC,EAAgB,CACd,IAAK,EAAkB,CAAI,EAC3B,cACA,OAAQ,KACV,CAAC,EAEU,GAAqB,CAChC,KAAM,CAAE,KAAI,YAAW,cACvB,iBACI,CACJ,IAAI,EAAU,CAAC,EAQf,OAPW,IAAc,SACvB,EAAQ,UAAe,GAEd,IAAe,SACxB,EAAQ,WAAgB,GAGnB,EAAgB,CACrB,IAAK,EAAwB,CAAE,EAC/B,cACA,OAAQ,MACR,SACF,CAAC,CACH,EAEa,GAAyB,CACpC,UACA,WACA,SACA,YACI,CACJ,IAAM,EAAU,CAAE,SAAQ,EAc1B,OAZW,IAAa,SACtB,EAAQ,UAAY,EAAW,IAAI,KAAK,CAAQ,EAAE,YAAY,EAAI,MAGzD,IAAW,SACpB,EAAQ,QAAU,EAAS,IAAI,KAAK,CAAM,EAAE,YAAY,EAAI,MAGnD,IAAW,SACpB,EAAQ,QAAU,GAGb,CACT,EAEa,IAAoB,CAC/B,cACA,UACA,WACA,SACA,YAEA,EAAgB,CACd,IAAK,EAAkB,EACvB,cACA,OAAQ,OACR,QAAS,EAAsB,CAAE,UAAS,WAAU,SAAQ,QAAO,CAAC,CACtE,CAAC,EAEU,GAAoB,CAC/B,KACA,cACA,UACA,WACA,SACA,YAEA,EAAgB,CACd,IAAK,EAAkB,CAAE,EACzB,cACA,OAAQ,QACR,QAAS,EAAsB,CAAE,UAAS,WAAU,SAAQ,QAAO,CAAC,CACtE,CAAC,EAEU,GAAsB,CAAE,KAAI,iBACvC,EAAgB,CACd,IAAK,EAAkB,CAAE,EACzB,cACA,OAAQ,QACV,CAAC,EAEU,GAAkB,CAAE,KAAI,QAAO,iBACnC,EAAgB,CACrB,IAAK,EACL,cACA,OAAQ,QACR,QAAS,CACP,QAAS,CACP,CACE,KACA,OACF,CACF,CACF,CACF,CAAC,EAGU,GAAuB,CAAE,iBACpC,EAAgB,CACd,IAAK,EACL,aACF,CAAC,EAEU,GAAiC,CAAE,iBAC9C,EAAgB,CACd,IAAK,EACL,aACF,CAAC,EAEU,GAAyB,CAAE,iBACtC,EAAgB,CACd,IAAK,EACL,aACF,CAAC,EAEU,GAAwB,CAAE,cAAa,aAClD,EAAgB,CACd,IAAK,EACL,OAAQ,OACR,cACA,SACF,CAAC,EAEU,GAAmB,CAAE,cAAa,aAC7C,EAAgB,CACd,IAAK,EACL,cACA,OAAQ,OACR,SACF,CAAC,EAGU,GAAmB,CAAE,UAChC,EAAgB,CACd,IAAK,EAAe,CAAI,EACxB,OAAQ,QACV,CAAC,EAEU,GAAe,CAAE,iBAC5B,EAAgB,CACd,IAAK,GACL,OAAQ,OACR,aACF,CAAC,EAEU,GAAqB,CAAE,iBAClC,EAAgB,CACd,IAAK,GACL,aACF,CAAC,EAEU,GAAmB,CAAE,OAAM,iBACtC,EAAgB,CACd,IAAK,EAAe,CAAI,EACxB,OAAQ,OACR,aACF,CAAC,EAEU,GAAwB,CACnC,WACA,OACA,WACA,kBAEK,EAAS,WAAW,MAAM,IAC7B,EAAW,WAAa,GAGnB,EAAgB,CACrB,IAAK,GAA0B,EAAU,EAAM,CAAQ,EACvD,aACF,CAAC,GAGU,GAA2B,CACtC,WACA,WACA,KACA,iBAEA,EAAgB,CACd,IAAK,GACL,cACA,OAAQ,OACR,QAAS,CACP,IAAK,EACL,KAAM,EACN,IACF,CACF,CAAC,EAEU,GAA8B,CACzC,MACA,WACA,OACA,iBACI,CACJ,IAAM,EAAO,IAAI,SAKjB,OAJI,GAAM,EAAK,IAAI,OAAQ,CAAI,EAC3B,GAAK,EAAK,IAAI,MAAO,CAAG,EAC5B,EAAK,IAAI,OAAQ,CAAQ,EAElB,EAAgB,CACrB,IAAK,GACL,OAAQ,OACR,SAAU,CACZ,CAAC,CACH,EAEa,IAAyB,CAAE,OAAM,UAAS,iBACrD,EAAgB,CACd,IAAK,EAAe,CAAI,EACxB,cACA,OAAQ,QACR,QAAS,CAAE,SAAU,CAAQ,CAC/B,CAAC,EAEU,GAAmB,CAAE,WAAU,OAAM,YAAW,cAAe,CAC1E,IAAM,EAAO,IAAI,SASjB,OARI,EAAS,KAAK,IAAM,IACtB,EAAK,IAAI,WAAY,CAAQ,EAE3B,EAAU,KAAK,IAAM,IACvB,EAAK,IAAI,YAAa,CAAS,EAEjC,EAAK,IAAI,OAAQ,CAAI,EAEd,EAAgB,CACrB,IAAK,EAAsB,CAAQ,EACnC,OAAQ,OACR,SAAU,CACZ,CAAC,CACH,EAEa,GAAmB,CAC9B,WACA,YACA,eACA,cACA,cACA,WAEA,EAAgB,CACd,IAAK,EAAsB,CAAQ,EACnC,cACA,OAAQ,QACR,QAAS,CACP,YACA,cAAe,EACf,aAAc,EACd,OACF,CACF,CAAC,EAEU,GAAmB,CAAE,WAAU,eAC1C,EAAgB,CACd,IAAK,GAAG,EAAsB,CAAQ,EAAE,aAAa,IACrD,OAAQ,QACV,CAAC"} \ No newline at end of file diff --git a/priv/static/static/js/announcements_page.5InQkWqQ.js b/priv/static/static/js/announcements_page.5InQkWqQ.js new file mode 100644 index 000000000..419be5aa2 --- /dev/null +++ b/priv/static/static/js/announcements_page.5InQkWqQ.js @@ -0,0 +1,2 @@ +import{I as e,M as t,R as n,U as r,W as i,X as a,_ as o,d as s,f as c,g as l,i as u,p as d,q as f,t as p,u as m}from"./_plugin-vue_export-helper.By3-ojQa.js";import{i as h,r as g,s as _}from"./runtime-dom.esm-bundler.DzcUlaDE.js";import{i as v}from"./vuex.esm-bundler.CWMfctgB.js";import{t as y}from"./locale.service.ol8gNUGd.js";import{t as b}from"./checkbox.BfKZDdOO.js";import{W as x}from"./main.CPF6dYCY.js";var S={components:{Checkbox:b},props:{announcement:Object,disabled:Boolean}},C={class:`announcement-editor`},w=[`placeholder`,`disabled`],T={class:`announcement-metadata`},E={for:`announcement-start-time`},D=[`type`,`disabled`],O={class:`announcement-metadata`},k={for:`announcement-end-time`},A=[`type`,`disabled`],j={class:`announcement-metadata`};function M(e,s,c,u,f,p){let _=n(`Checkbox`);return t(),d(`div`,C,[i(m(`textarea`,{ref:`textarea`,"onUpdate:modelValue":s[0]||=t=>e.announcement.content=t,class:`input post-textarea`,rows:`1`,cols:`1`,placeholder:e.$t(`announcements.post_placeholder`),disabled:e.disabled},null,8,w),[[h,e.announcement.content]]),m(`span`,T,[m(`label`,E,a(e.$t(`announcements.start_time_prompt`)),1),i(m(`input`,{id:`announcement-start-time`,"onUpdate:modelValue":s[1]||=t=>e.announcement.startsAt=t,class:`input`,type:e.announcement.allDay?`date`:`datetime-local`,disabled:e.disabled},null,8,D),[[g,e.announcement.startsAt]])]),m(`span`,O,[m(`label`,k,a(e.$t(`announcements.end_time_prompt`)),1),i(m(`input`,{id:`announcement-end-time`,"onUpdate:modelValue":s[2]||=t=>e.announcement.endsAt=t,class:`input`,type:e.announcement.allDay?`date`:`datetime-local`,disabled:e.disabled},null,8,A),[[g,e.announcement.endsAt]])]),m(`span`,j,[o(_,{id:`announcement-all-day`,modelValue:e.announcement.allDay,"onUpdate:modelValue":s[3]||=t=>e.announcement.allDay=t,disabled:e.disabled},{default:r(()=>[l(a(e.$t(`announcements.all_day_prompt`)),1)]),_:1},8,[`modelValue`,`disabled`])])])}var N=p(S,[[`render`,M]]),P={components:{AnnouncementEditor:N},data(){return{editing:!1,editedAnnouncement:{content:``,startsAt:void 0,endsAt:void 0,allDay:void 0},editError:``}},props:{announcement:Object},computed:{...v({currentUser:e=>e.users.currentUser}),canEditAnnouncement(){return this.currentUser&&this.currentUser.privileges.has(`announcements_manage_announcements`)},content(){return this.announcement.content},isRead(){return this.announcement.read},publishedAt(){let e=this.announcement.published_at;if(e)return this.formatTimeOrDate(e,y.internalToBrowserLocale(this.$i18n.locale))},startsAt(){let e=this.announcement.starts_at;if(e)return this.formatTimeOrDate(e,y.internalToBrowserLocale(this.$i18n.locale))},endsAt(){let e=this.announcement.ends_at;if(e)return this.formatTimeOrDate(e,y.internalToBrowserLocale(this.$i18n.locale))},inactive(){return this.announcement.inactive}},methods:{markAsRead(){if(!this.isRead)return x().markAnnouncementAsRead(this.announcement.id)},deleteAnnouncement(){return x().deleteAnnouncement(this.announcement.id)},formatTimeOrDate(e,t){let n=new Date(e);return this.announcement.all_day?n.toLocaleDateString(t):n.toLocaleString(t)},enterEditMode(){this.editedAnnouncement.content=this.announcement.pleroma.raw_content,this.editedAnnouncement.startsAt=this.announcement.starts_at,this.editedAnnouncement.endsAt=this.announcement.ends_at,this.editedAnnouncement.allDay=this.announcement.all_day,this.editing=!0},submitEdit(){x().editAnnouncement({id:this.announcement.id,...this.editedAnnouncement}).then(()=>{this.editing=!1}).catch(e=>{this.editError=e.error})},cancelEdit(){this.editing=!1},clearError(){this.editError=void 0}}},F={class:`announcement`},I={class:`heading`},L={class:`body`},R={class:`footer`},z={key:0,class:`times`},ee={key:0},B={key:1},V={key:2},H={key:1,class:`actions`},U=[`disabled`,`title`],W={key:2,class:`actions`},G={key:0,class:`alert error`};function K(e,r,i,u,p,h){let g=n(`rich-content`),_=n(`announcement-editor`),v=n(`FAIcon`);return t(),d(`div`,F,[m(`div`,I,[m(`h4`,null,a(e.$t(`announcements.title`)),1)]),m(`div`,L,[e.editing?(t(),s(_,{key:1,announcement:e.editedAnnouncement},null,8,[`announcement`])):(t(),s(g,{key:0,html:e.content,emoji:e.announcement.emojis,"handle-links":!0},null,8,[`html`,`emoji`]))]),m(`div`,R,[e.editing?c(``,!0):(t(),d(`div`,z,[e.publishedAt?(t(),d(`span`,ee,a(e.$t(`announcements.published_time_display`,{time:e.publishedAt})),1)):c(``,!0),e.startsAt?(t(),d(`span`,B,a(e.$t(`announcements.start_time_display`,{time:e.startsAt})),1)):c(``,!0),e.endsAt?(t(),d(`span`,V,a(e.$t(`announcements.end_time_display`,{time:e.endsAt})),1)):c(``,!0)])),e.editing?(t(),d(`div`,W,[m(`button`,{class:`btn button-default`,onClick:r[3]||=(...t)=>e.submitEdit&&e.submitEdit(...t)},a(e.$t(`announcements.submit_edit_action`)),1),m(`button`,{class:`btn button-default`,onClick:r[4]||=(...t)=>e.cancelEdit&&e.cancelEdit(...t)},a(e.$t(`announcements.cancel_edit_action`)),1),e.editing&&e.editError?(t(),d(`div`,G,[l(a(e.$t(`announcements.edit_error`,{error:e.error}))+` `,1),m(`button`,{class:`button-unstyled`,onClick:r[5]||=(...t)=>e.clearError&&e.clearError(...t)},[o(v,{class:`fa-scale-110 fa-old-padding`,icon:`times`,title:e.$t(`announcements.close_error`)},null,8,[`title`])])])):c(``,!0)])):(t(),d(`div`,H,[e.currentUser?(t(),d(`button`,{key:0,class:f([`btn button-default`,{toggled:e.isRead}]),disabled:e.inactive,title:e.inactive?e.$t(`announcements.inactive_message`):``,onClick:r[0]||=(...t)=>e.markAsRead&&e.markAsRead(...t)},a(e.$t(`announcements.mark_as_read_action`)),11,U)):c(``,!0),e.canEditAnnouncement?(t(),d(`button`,{key:1,class:`btn button-default`,onClick:r[1]||=(...t)=>e.enterEditMode&&e.enterEditMode(...t)},a(e.$t(`announcements.edit_action`)),1)):c(``,!0),e.canEditAnnouncement?(t(),d(`button`,{key:2,class:`btn button-default`,onClick:r[2]||=(...t)=>e.deleteAnnouncement&&e.deleteAnnouncement(...t)},a(e.$t(`announcements.delete_action`)),1)):c(``,!0)]))])])}var q={components:{Announcement:p(P,[[`render`,K]]),AnnouncementEditor:N},data(){return{newAnnouncement:{content:``,startsAt:void 0,endsAt:void 0,allDay:!1},posting:!1,error:void 0}},mounted(){x().fetchAnnouncements()},computed:{...v({currentUser:e=>e.users.currentUser}),announcements(){return x().announcements},canPostAnnouncement(){return this.currentUser&&this.currentUser.privileges.has(`announcements_manage_announcements`)}},methods:{postAnnouncement(){this.posting=!0,x().postAnnouncement(this.newAnnouncement).then(()=>{this.newAnnouncement.content=``,this.startsAt=void 0,this.endsAt=void 0}).catch(e=>{this.error=e.error}).finally(()=>{this.posting=!1})},clearError(){this.error=void 0}}},J={class:`panel panel-default announcements-page`},Y={class:`panel-heading`},X={class:`title`},Z={class:`panel-body`},Q={key:0},$={class:`post-form`},te={class:`heading`},ne={class:`body`},re={class:`footer`},ie=[`disabled`],ae={key:0,class:`alert error`};function oe(r,i,s,f,p,h){let g=n(`announcement-editor`),v=n(`FAIcon`),y=n(`announcement`);return t(),d(`div`,J,[m(`div`,Y,[m(`h1`,X,a(r.$t(`announcements.page_header`)),1)]),m(`div`,Z,[r.canPostAnnouncement?(t(),d(`section`,Q,[m(`div`,$,[m(`div`,te,[m(`h4`,null,a(r.$t(`announcements.post_form_header`)),1)]),m(`div`,ne,[o(g,{announcement:r.newAnnouncement,disabled:r.posting},null,8,[`announcement`,`disabled`])]),m(`div`,re,[m(`button`,{class:`btn button-default post-button`,disabled:r.posting,onClick:i[0]||=_((...e)=>r.postAnnouncement&&r.postAnnouncement(...e),[`prevent`])},a(r.$t(`announcements.post_action`)),9,ie),r.error?(t(),d(`div`,ae,[l(a(r.$t(`announcements.post_error`,{error:r.error}))+` `,1),m(`button`,{class:`button-unstyled`,onClick:i[1]||=(...e)=>r.clearError&&r.clearError(...e)},[o(v,{class:`fa-scale-110 fa-old-padding`,icon:`times`,title:r.$t(`announcements.close_error`)},null,8,[`title`])])])):c(``,!0)])])])):c(``,!0),(t(!0),d(u,null,e(r.announcements,e=>(t(),d(`section`,{key:e.id},[o(y,{announcement:e},null,8,[`announcement`])]))),128))])])}var se=p(q,[[`render`,oe]]);export{se as default}; +//# sourceMappingURL=announcements_page.5InQkWqQ.js.map \ No newline at end of file diff --git a/priv/static/static/js/announcements_page.5InQkWqQ.js.map b/priv/static/static/js/announcements_page.5InQkWqQ.js.map new file mode 100644 index 000000000..43882b4b2 --- /dev/null +++ b/priv/static/static/js/announcements_page.5InQkWqQ.js.map @@ -0,0 +1 @@ +{"version":3,"file":"announcements_page.5InQkWqQ.js","names":[],"sources":["../../../src/components/announcement_editor/announcement_editor.js?vue&type=script&src=true&lang.js","../../../src/components/announcement_editor/announcement_editor.vue","../../../src/components/announcement/announcement.js?vue&type=script&src=true&lang.js","../../../src/components/announcement/announcement.vue","../../../src/components/announcements_page/announcements_page.js?vue&type=script&src=true&lang.js","../../../src/components/announcements_page/announcements_page.vue"],"sourcesContent":["import Checkbox from 'src/components/checkbox/checkbox.vue'\n\nconst AnnouncementEditor = {\n components: {\n Checkbox,\n },\n props: {\n announcement: Object,\n disabled: Boolean,\n },\n}\n\nexport default AnnouncementEditor\n","\n\n\n\n\n","import { mapState } from 'vuex'\n\nimport AnnouncementEditor from 'src/components/announcement_editor/announcement_editor.vue'\nimport localeService from '../../services/locale/locale.service.js'\n\nimport { useAnnouncementsStore } from 'src/stores/announcements.js'\n\nconst Announcement = {\n components: {\n AnnouncementEditor,\n },\n data() {\n return {\n editing: false,\n editedAnnouncement: {\n content: '',\n startsAt: undefined,\n endsAt: undefined,\n allDay: undefined,\n },\n editError: '',\n }\n },\n props: {\n announcement: Object,\n },\n computed: {\n ...mapState({\n currentUser: (state) => state.users.currentUser,\n }),\n canEditAnnouncement() {\n return (\n this.currentUser &&\n this.currentUser.privileges.has('announcements_manage_announcements')\n )\n },\n content() {\n return this.announcement.content\n },\n isRead() {\n return this.announcement.read\n },\n publishedAt() {\n const time = this.announcement.published_at\n if (!time) {\n return\n }\n\n return this.formatTimeOrDate(\n time,\n localeService.internalToBrowserLocale(this.$i18n.locale),\n )\n },\n startsAt() {\n const time = this.announcement.starts_at\n if (!time) {\n return\n }\n\n return this.formatTimeOrDate(\n time,\n localeService.internalToBrowserLocale(this.$i18n.locale),\n )\n },\n endsAt() {\n const time = this.announcement.ends_at\n if (!time) {\n return\n }\n\n return this.formatTimeOrDate(\n time,\n localeService.internalToBrowserLocale(this.$i18n.locale),\n )\n },\n inactive() {\n return this.announcement.inactive\n },\n },\n methods: {\n markAsRead() {\n if (!this.isRead) {\n return useAnnouncementsStore().markAnnouncementAsRead(\n this.announcement.id,\n )\n }\n },\n deleteAnnouncement() {\n return useAnnouncementsStore().deleteAnnouncement(this.announcement.id)\n },\n formatTimeOrDate(time, locale) {\n const d = new Date(time)\n return this.announcement.all_day\n ? d.toLocaleDateString(locale)\n : d.toLocaleString(locale)\n },\n enterEditMode() {\n this.editedAnnouncement.content = this.announcement.pleroma.raw_content\n this.editedAnnouncement.startsAt = this.announcement.starts_at\n this.editedAnnouncement.endsAt = this.announcement.ends_at\n this.editedAnnouncement.allDay = this.announcement.all_day\n this.editing = true\n },\n submitEdit() {\n useAnnouncementsStore()\n .editAnnouncement({\n id: this.announcement.id,\n ...this.editedAnnouncement,\n })\n .then(() => {\n this.editing = false\n })\n .catch((error) => {\n this.editError = error.error\n })\n },\n cancelEdit() {\n this.editing = false\n },\n clearError() {\n this.editError = undefined\n },\n },\n}\n\nexport default Announcement\n","\n\n\n\n\n","import { mapState } from 'vuex'\n\nimport Announcement from 'src/components/announcement/announcement.vue'\nimport AnnouncementEditor from 'src/components/announcement_editor/announcement_editor.vue'\n\nimport { useAnnouncementsStore } from 'src/stores/announcements.js'\n\nconst AnnouncementsPage = {\n components: {\n Announcement,\n AnnouncementEditor,\n },\n data() {\n return {\n newAnnouncement: {\n content: '',\n startsAt: undefined,\n endsAt: undefined,\n allDay: false,\n },\n posting: false,\n error: undefined,\n }\n },\n mounted() {\n useAnnouncementsStore().fetchAnnouncements()\n },\n computed: {\n ...mapState({\n currentUser: (state) => state.users.currentUser,\n }),\n announcements() {\n return useAnnouncementsStore().announcements\n },\n canPostAnnouncement() {\n return (\n this.currentUser &&\n this.currentUser.privileges.has('announcements_manage_announcements')\n )\n },\n },\n methods: {\n postAnnouncement() {\n this.posting = true\n useAnnouncementsStore()\n .postAnnouncement(this.newAnnouncement)\n .then(() => {\n this.newAnnouncement.content = ''\n this.startsAt = undefined\n this.endsAt = undefined\n })\n .catch((error) => {\n this.error = error.error\n })\n .finally(() => {\n this.posting = false\n })\n },\n clearError() {\n this.error = undefined\n },\n },\n}\n\nexport default AnnouncementsPage\n","\n\n\n\n\n"],"mappings":"4ZAEA,IAAM,EAAqB,CACzB,WAAY,CACV,SAAA,CACF,EACA,MAAO,CACL,aAAc,OACd,SAAU,OACZ,CACF,KCTO,MAAM,qBAAqB,kCAUxB,MAAM,uBAAuB,KAC1B,IAAI,yBAAyB,2BAShC,MAAM,uBAAuB,KAC1B,IAAI,uBAAuB,2BAS9B,MAAM,uBAAuB,yDA9BrC,EAuCM,MAvCN,EAuCM,GAtCJ,EAQE,WAAA,CAPA,IAAI,2CACK,EAAA,aAAa,QAAO,EAC7B,MAAM,sBACN,KAAK,IACL,KAAK,IACJ,YAAa,EAAA,GAAE,gCAAA,EACf,SAAU,EAAA,wBALF,EAAA,aAAa,OAAO,CAAA,CAAA,EAO/B,EASO,OATP,EASO,CARL,EAAwF,QAAxF,EAAwF,EAAhD,EAAA,GAAE,iCAAA,CAAA,EAAA,CAAA,EAAA,EAC1C,EAMC,QAAA,CALC,GAAG,0DACM,EAAA,aAAa,SAAQ,EAC9B,MAAM,QACL,KAAM,EAAA,aAAa,OAAM,OAAA,iBACzB,SAAU,EAAA,wBAHF,EAAA,aAAa,QAAQ,CAAA,CAAA,CAAA,CAAA,EAMlC,EASO,OATP,EASO,CARL,EAAoF,QAApF,EAAoF,EAA9C,EAAA,GAAE,+BAAA,CAAA,EAAA,CAAA,EAAA,EACxC,EAMC,QAAA,CALC,GAAG,wDACM,EAAA,aAAa,OAAM,EAC5B,MAAM,QACL,KAAM,EAAA,aAAa,OAAM,OAAA,iBACzB,SAAU,EAAA,wBAHF,EAAA,aAAa,MAAM,CAAA,CAAA,CAAA,CAAA,EAMhC,EAQO,OARP,EAQO,CAPL,EAMW,EAAA,CALT,GAAG,kCACM,EAAA,aAAa,uCAAb,EAAA,aAAa,OAAM,EAC3B,SAAU,EAAA,yBAE6B,CAAA,EAAA,EAArC,EAAA,GAAE,8BAAA,CAAA,EAAA,CAAA,CAAA,CAAA,kEC9BP,EAAe,CACnB,WAAY,CACV,mBAAA,CACF,EACA,MAAO,CACL,MAAO,CACL,QAAS,GACT,mBAAoB,CAClB,QAAS,GACT,SAAU,IAAA,GACV,OAAQ,IAAA,GACR,OAAQ,IAAA,EACV,EACA,UAAW,EACb,CACF,EACA,MAAO,CACL,aAAc,MAChB,EACA,SAAU,CACR,GAAG,EAAS,CACV,YAAc,GAAU,EAAM,MAAM,WACtC,CAAC,EACD,qBAAsB,CACpB,OACE,KAAK,aACL,KAAK,YAAY,WAAW,IAAI,oCAAoC,CAExE,EACA,SAAU,CACR,OAAO,KAAK,aAAa,OAC3B,EACA,QAAS,CACP,OAAO,KAAK,aAAa,IAC3B,EACA,aAAc,CACZ,IAAM,EAAO,KAAK,aAAa,aAC1B,KAIL,OAAO,KAAK,iBACV,EACA,EAAc,wBAAwB,KAAK,MAAM,MAAM,CACzD,CACF,EACA,UAAW,CACT,IAAM,EAAO,KAAK,aAAa,UAC1B,KAIL,OAAO,KAAK,iBACV,EACA,EAAc,wBAAwB,KAAK,MAAM,MAAM,CACzD,CACF,EACA,QAAS,CACP,IAAM,EAAO,KAAK,aAAa,QAC1B,KAIL,OAAO,KAAK,iBACV,EACA,EAAc,wBAAwB,KAAK,MAAM,MAAM,CACzD,CACF,EACA,UAAW,CACT,OAAO,KAAK,aAAa,QAC3B,CACF,EACA,QAAS,CACP,YAAa,CACX,GAAI,CAAC,KAAK,OACR,OAAO,EAAsB,EAAE,uBAC7B,KAAK,aAAa,EACpB,CAEJ,EACA,oBAAqB,CACnB,OAAO,EAAsB,EAAE,mBAAmB,KAAK,aAAa,EAAE,CACxE,EACA,iBAAiB,EAAM,EAAQ,CAC7B,IAAM,EAAI,IAAI,KAAK,CAAI,EACvB,OAAO,KAAK,aAAa,QACrB,EAAE,mBAAmB,CAAM,EAC3B,EAAE,eAAe,CAAM,CAC7B,EACA,eAAgB,CACd,KAAK,mBAAmB,QAAU,KAAK,aAAa,QAAQ,YAC5D,KAAK,mBAAmB,SAAW,KAAK,aAAa,UACrD,KAAK,mBAAmB,OAAS,KAAK,aAAa,QACnD,KAAK,mBAAmB,OAAS,KAAK,aAAa,QACnD,KAAK,QAAU,EACjB,EACA,YAAa,CACX,EAAsB,EACnB,iBAAiB,CAChB,GAAI,KAAK,aAAa,GACtB,GAAG,KAAK,kBACV,CAAC,EACA,SAAW,CACV,KAAK,QAAU,EACjB,CAAC,EACA,MAAO,GAAU,CAChB,KAAK,UAAY,EAAM,KACzB,CAAC,CACL,EACA,YAAa,CACX,KAAK,QAAU,EACjB,EACA,YAAa,CACX,KAAK,UAAY,IAAA,EACnB,CACF,CACF,KC1HO,MAAM,cAAc,KAClB,MAAM,SAAS,KAGf,MAAM,MAAM,KAYZ,MAAM,QAAQ,WAGf,MAAM,iDAcN,MAAM,2CA6BN,MAAM,oBAgBJ,MAAM,mHA9Ed,EA8FM,MA9FN,EA8FM,CA7FJ,EAEM,MAFN,EAEM,CADJ,EAAwC,KAAA,KAAA,EAAjC,EAAA,GAAE,qBAAA,CAAA,EAAA,CAAA,CAAA,CAAA,EAEX,EAWM,MAXN,EAWM,CATK,EAAA,aAKT,EAGE,EAAA,OADC,aAAc,EAAA,+CAPR,EAAA,EADT,EAKE,EAAA,OAHC,KAAM,EAAA,QACN,MAAO,EAAA,aAAa,OACpB,eAAc,gCAOnB,EA6EM,MA7EN,EA6EM,CA3EK,EAAA,kBAAA,EAAA,EADT,EAaM,MAbN,EAaM,CATQ,EAAA,aAAA,EAAA,EAAZ,EAEO,OAAA,GAAA,EADF,EAAA,GAAE,uCAAA,CAAA,KAAiD,EAAA,WAAW,CAAA,CAAA,EAAA,CAAA,GAAA,EAAA,GAAA,EAAA,EAEvD,EAAA,UAAA,EAAA,EAAZ,EAEO,OAAA,EAAA,EADF,EAAA,GAAE,mCAAA,CAAA,KAA6C,EAAA,QAAQ,CAAA,CAAA,EAAA,CAAA,GAAA,EAAA,GAAA,EAAA,EAEhD,EAAA,QAAA,EAAA,EAAZ,EAEO,OAAA,EAAA,EADF,EAAA,GAAE,iCAAA,CAAA,KAA2C,EAAA,MAAM,CAAA,CAAA,EAAA,CAAA,GAAA,EAAA,GAAA,EAAA,KAIjD,EAAA,aA4BT,EAgCM,MAhCN,EAgCM,CA5BJ,EAKS,SAAA,CAJP,MAAM,qBACL,QAAK,AAAA,EAAA,MAAA,GAAA,IAAE,EAAA,YAAA,EAAA,WAAA,GAAA,CAAA,KAEL,EAAA,GAAE,kCAAA,CAAA,EAAA,CAAA,EAEP,EAKS,SAAA,CAJP,MAAM,qBACL,QAAK,AAAA,EAAA,MAAA,GAAA,IAAE,EAAA,YAAA,EAAA,WAAA,GAAA,CAAA,KAEL,EAAA,GAAE,kCAAA,CAAA,EAAA,CAAA,EAGC,EAAA,SAAW,EAAA,WAAA,EAAA,EADnB,EAeM,MAfN,EAeM,CAAA,EAAA,EAXD,EAAA,GAAE,2BAAA,CAAA,MAA+B,EAAA,KAAK,CAAA,CAAA,EAAM,IAC/C,CAAA,EAAA,EASS,SAAA,CARP,MAAM,kBACL,QAAK,AAAA,EAAA,MAAA,GAAA,IAAE,EAAA,YAAA,EAAA,WAAA,GAAA,CAAA,IAER,EAIE,EAAA,CAHA,MAAM,8BACN,KAAK,QACJ,MAAO,EAAA,GAAE,2BAAA,uCAxDT,EAAA,EADT,EA4BM,MA5BN,EA4BM,CAvBI,EAAA,aAAA,EAAA,EADR,EASS,SAAA,OAPP,MAAK,EAAA,CAAC,qBAAoB,CAAA,QACP,EAAA,MAAM,CAAA,CAAA,EACxB,SAAU,EAAA,SACV,MAAO,EAAA,SAAW,EAAA,GAAE,gCAAA,EAAA,GACpB,QAAK,AAAA,EAAA,MAAA,GAAA,IAAE,EAAA,YAAA,EAAA,WAAA,GAAA,CAAA,KAEL,EAAA,GAAE,mCAAA,CAAA,EAAA,GAAA,CAAA,GAAA,EAAA,GAAA,EAAA,EAGC,EAAA,qBAAA,EAAA,EADR,EAMS,SAAA,OAJP,MAAM,qBACL,QAAK,AAAA,EAAA,MAAA,GAAA,IAAE,EAAA,eAAA,EAAA,cAAA,GAAA,CAAA,KAEL,EAAA,GAAE,2BAAA,CAAA,EAAA,CAAA,GAAA,EAAA,GAAA,EAAA,EAGC,EAAA,qBAAA,EAAA,EADR,EAMS,SAAA,OAJP,MAAM,qBACL,QAAK,AAAA,EAAA,MAAA,GAAA,IAAE,EAAA,oBAAA,EAAA,mBAAA,GAAA,CAAA,KAEL,EAAA,GAAE,6BAAA,CAAA,EAAA,CAAA,GAAA,EAAA,GAAA,EAAA,SCnDf,IAAM,EAAoB,CACxB,WAAY,CACV,+BAAA,EACA,mBAAA,CACF,EACA,MAAO,CACL,MAAO,CACL,gBAAiB,CACf,QAAS,GACT,SAAU,IAAA,GACV,OAAQ,IAAA,GACR,OAAQ,EACV,EACA,QAAS,GACT,MAAO,IAAA,EACT,CACF,EACA,SAAU,CACR,EAAsB,EAAE,mBAAmB,CAC7C,EACA,SAAU,CACR,GAAG,EAAS,CACV,YAAc,GAAU,EAAM,MAAM,WACtC,CAAC,EACD,eAAgB,CACd,OAAO,EAAsB,EAAE,aACjC,EACA,qBAAsB,CACpB,OACE,KAAK,aACL,KAAK,YAAY,WAAW,IAAI,oCAAoC,CAExE,CACF,EACA,QAAS,CACP,kBAAmB,CACjB,KAAK,QAAU,GACf,EAAsB,EACnB,iBAAiB,KAAK,eAAe,EACrC,SAAW,CACV,KAAK,gBAAgB,QAAU,GAC/B,KAAK,SAAW,IAAA,GAChB,KAAK,OAAS,IAAA,EAChB,CAAC,EACA,MAAO,GAAU,CAChB,KAAK,MAAQ,EAAM,KACrB,CAAC,EACA,YAAc,CACb,KAAK,QAAU,EACjB,CAAC,CACL,EACA,YAAa,CACX,KAAK,MAAQ,IAAA,EACf,CACF,CACF,KC7DO,MAAM,wCAAwC,KAC5C,MAAM,eAAe,KACpB,MAAM,OAAO,KAId,MAAM,YAAY,eAId,MAAM,WAAW,MACf,MAAM,SAAS,MAGf,MAAM,MAAM,MAMZ,MAAM,QAAQ,4BAUf,MAAM,oHA9BlB,EAwDM,MAxDN,EAwDM,CAvDJ,EAIM,MAJN,EAIM,CAHJ,EAEK,KAFL,EAEK,EADA,EAAA,GAAE,2BAAA,CAAA,EAAA,CAAA,CAAA,CAAA,EAGT,EAiDM,MAjDN,EAiDM,CA/CI,EAAA,qBAAA,EAAA,EADR,EAuCU,UAAA,EAAA,CApCR,EAmCM,MAnCN,EAmCM,CAlCJ,EAEM,MAFN,GAEM,CADJ,EAAmD,KAAA,KAAA,EAA5C,EAAA,GAAE,gCAAA,CAAA,EAAA,CAAA,CAAA,CAAA,EAEX,EAKM,MALN,GAKM,CAJJ,EAGE,EAAA,CAFC,aAAc,EAAA,gBACd,SAAU,EAAA,+CAGf,EAwBM,MAxBN,GAwBM,CAvBJ,EAMS,SAAA,CALP,MAAM,iCACL,SAAU,EAAA,QACV,QAAK,AAAA,EAAA,KAAA,GAAA,GAAA,IAAU,EAAA,kBAAA,EAAA,iBAAA,GAAA,CAAA,EAAgB,CAAA,SAAA,CAAA,KAE7B,EAAA,GAAE,2BAAA,CAAA,EAAA,EAAA,EAAA,EAGC,EAAA,OAAA,EAAA,EADR,EAeM,MAfN,GAeM,CAAA,EAAA,EAXD,EAAA,GAAE,2BAAA,CAAA,MAA+B,EAAA,KAAK,CAAA,CAAA,EAAM,IAC/C,CAAA,EAAA,EASS,SAAA,CARP,MAAM,kBACL,QAAK,AAAA,EAAA,MAAA,GAAA,IAAE,EAAA,YAAA,EAAA,WAAA,GAAA,CAAA,IAER,EAIE,EAAA,CAHA,MAAM,8BACN,KAAK,QACJ,MAAO,EAAA,GAAE,2BAAA,0DAOtB,EAOU,EAAA,KAAA,EANe,EAAA,cAAhB,QADT,EAOU,UAAA,CALP,IAAK,EAAa,EAAA,EAAA,CAEnB,EAEE,EAAA,CADe,cAAY,EAAA,KAAA,EAAA,CAAA,cAAA,CAAA,CAAA,CAAA"} \ No newline at end of file diff --git a/priv/static/static/js/attributes_helper.service.Ku0p8pb2.js b/priv/static/static/js/attributes_helper.service.Ku0p8pb2.js new file mode 100644 index 000000000..c65d57b12 --- /dev/null +++ b/priv/static/static/js/attributes_helper.service.Ku0p8pb2.js @@ -0,0 +1,3 @@ +import{I as e,L as t,M as n,R as r,U as i,X as a,Y as o,_ as s,d as c,f as l,g as u,i as d,p as f,q as p,t as m,u as h}from"./_plugin-vue_export-helper.By3-ojQa.js";import{s as g}from"./runtime-dom.esm-bundler.DzcUlaDE.js";import{t as _}from"./lodash.daI9UflD.js";import{O as v}from"./oauth.C4JLKGAu.js";import{kn as y}from"./free-solid-svg-icons.vn_kySUQ.js";import{t as b}from"./languages.q6q0orhR.js";import{t as x}from"./unicode_domain_indicator.BnLqD48H.js";import{t as S}from"./popover.94nM-RDc.js";import{t as C}from"./emoji_picker.Ccx2u99H.js";import{r as w}from"./free-regular-svg-icons.BGRGA4e_.js";var T=_(),E={props:{ariaLive:{type:String,default:`assertive`}},data(){return{currentText:``}},methods:{announce(e){this.currentText=e,setTimeout(()=>{this.currentText=``},1e3)}}},D=[`aria-live`];function O(e,t,r,i,o,s){return n(),f(`div`,{class:`visible-for-screenreader-only`,"aria-live":e.ariaLive},a(e.currentText),9,D)}var k=m(E,[[`render`,O]]),A=(e,t,n)=>e.slice(0,t.start)+n+e.slice(t.end),j=(e,t)=>(0,T.find)(M(N(e)),({start:e,end:n})=>e<=t&&n>t),M=e=>(0,T.reduce)(e,(e,t)=>{let n={word:t,start:0,end:t.length};if(e.length>0){let t=e.pop();n.start+=t.end,n.end+=t.end,e.push(t)}return e.push(n),e},[]),N=e=>{let t=[],n=``;for(let r=0;r{let a={top:n+e.offsetTop,left:r+e.offsetLeft};if(!i&&e!==window){let{topPadding:t,leftPadding:n}=I(e);a.top+=i?0:t,a.left+=i?0:n}if(e.offsetParent&&window.getComputedStyle(e.offsetParent).position!==`sticky`&&(t===window||t.contains(e.offsetParent)||t===e.offsetParent))return F(e.offsetParent,t,a,!1);if(t!==window){let{topPadding:e,leftPadding:n}=I(t);a.top+=e,a.left+=n}return a},I=e=>{let t=window.getComputedStyle(e)[`padding-top`],n=Number(t.substring(0,t.length-2)),r=window.getComputedStyle(e)[`padding-left`];return{topPadding:n,leftPadding:Number(r.substring(0,r.length-2))}},L=()=>`${Math.random()}`.replace(`.`,`-`);y.add(w);var R={emits:[`update:modelValue`,`shown`],props:{suggest:{required:!0,type:Function},modelValue:{required:!0,type:String},enableEmojiPicker:{required:!1,type:Boolean,default:!1},hideEmojiButton:{required:!1,type:Boolean,default:!1},enableStickerPicker:{required:!1,type:Boolean,default:!1},placement:{required:!1,type:String,default:`auto`},newlineOnCtrlEnter:{required:!1,type:Boolean,default:!1}},data(){return{randomSeed:L(),input:void 0,caretEl:void 0,highlighted:-1,caret:0,focused:!1,blurTimeout:null,temporarilyHideSuggestions:!1,disableClickOutside:!1,suggestions:[],overlayStyle:{},pickerShown:!1}},components:{Popover:S,EmojiPicker:C,UnicodeDomainIndicator:x,ScreenReaderNotice:k},computed:{padEmoji(){return v().mergedConfig.padEmoji},defaultCandidateIndex(){return v().mergedConfig.autocompleteSelect?0:-1},preText(){return this.modelValue.slice(0,this.caret)},postText(){return this.modelValue.slice(this.caret)},showSuggestions(){return this.focused&&this.suggestions&&this.suggestions.length>0&&!this.pickerShown&&!this.temporarilyHideSuggestions},textAtCaret(){return this.wordAtCaret?.word},wordAtCaret(){if(this.modelValue&&this.caret)return P.wordAtPosition(this.modelValue,this.caret-1)||{}},languages(){return b(v().mergedConfig.interfaceLanguage)},maybeLocalizedEmojiNamesAndKeywords(){return e=>{let t=[e.displayText],n=[];return e.displayTextI18n&&t.push(this.$t(e.displayTextI18n.key,e.displayTextI18n.args)),e.annotations&&this.languages.forEach(r=>{t.push(e.annotations[r]?.name),n.push(...e.annotations[r]?.keywords||[])}),{names:t.filter(e=>e),keywords:n.filter(e=>e)}}},maybeLocalizedEmojiName(){return e=>{if(!e.annotations)return e.displayText;if(e.displayTextI18n)return this.$t(e.displayTextI18n.key,e.displayTextI18n.args);for(let t of this.languages)if(e.annotations[t]?.name)return e.annotations[t].name;return e.displayText}},suggestionListId(){return`suggestions-${this.randomSeed}`},suggestionItemId(){return e=>`suggestion-item-${e}-${this.randomSeed}`}},mounted(){let{root:e,hiddenOverlayCaret:t,suggestorPopover:n}=this.$refs,r=e.querySelector(`.emoji-input > input`)||e.querySelector(`.emoji-input > textarea`);if(!r)return;this.input=r,this.caretEl=t,n.setAnchorEl?(n.setAnchorEl(this.caretEl),this.$refs.picker.setAnchorEl(this.caretEl)):console.warn(`setAnchorEl not found, are we in a unit test?`);let i=getComputedStyle(this.input);this.overlayStyle.padding=i.padding,this.overlayStyle.border=i.border,this.overlayStyle.margin=i.margin,this.overlayStyle.lineHeight=i.lineHeight,this.overlayStyle.fontFamily=i.fontFamily,this.overlayStyle.fontSize=i.fontSize,this.overlayStyle.wordWrap=i.wordWrap,this.overlayStyle.whiteSpace=i.whiteSpace,r.addEventListener(`blur`,this.onBlur),r.addEventListener(`focus`,this.onFocus),r.addEventListener(`paste`,this.onPaste),r.addEventListener(`keyup`,this.onKeyUp),r.addEventListener(`keydown`,this.onKeyDown),r.addEventListener(`click`,this.onClickInput),r.addEventListener(`transitionend`,this.onTransition),r.addEventListener(`input`,this.onInput),r.addEventListener(`scroll`,this.onInputScroll)},unmounted(){let{input:e}=this;e&&(e.removeEventListener(`blur`,this.onBlur),e.removeEventListener(`focus`,this.onFocus),e.removeEventListener(`paste`,this.onPaste),e.removeEventListener(`keyup`,this.onKeyUp),e.removeEventListener(`keydown`,this.onKeyDown),e.removeEventListener(`click`,this.onClickInput),e.removeEventListener(`transitionend`,this.onTransition),e.removeEventListener(`input`,this.onInput),e.removeEventListener(`scroll`,this.onInputScroll))},watch:{showSuggestions:function(e){this.$emit(`shown`,e),e?this.$refs.suggestorPopover.showPopover():this.$refs.suggestorPopover.hidePopover()},textAtCaret:async function(e){if(e===void 0)return;if(e===e.charAt(0)){this.suggestions=[];return}let t=await this.suggest(e,this.maybeLocalizedEmojiNamesAndKeywords);if(this.textAtCaret!==e||t.length<=0){this.suggestions=[];return}this.suggestions=(0,T.take)(t,5).map(({imageUrl:e,...t})=>({...t,img:e||``})),this.highlighted=this.defaultCandidateIndex,this.$refs.screenReaderNotice.announce(this.$t(`tool_tip.autocomplete_available`,{number:this.suggestions.length},this.suggestions.length))}},methods:{onInputScroll(e){this.$refs.hiddenOverlay.scrollTo({top:this.input.scrollTop,left:this.input.scrollLeft}),this.setCaret(e)},triggerShowPicker(){this.$nextTick(()=>{this.$refs.picker.showPicker(),this.scrollIntoView()}),this.disableClickOutside=!0,setTimeout(()=>{this.disableClickOutside=!1},0)},togglePicker(){this.input.focus(),this.pickerShown?this.$refs.picker.hidePicker():(this.scrollIntoView(),this.$refs.picker.showPicker())},replace(e){let t=P.replaceWord(this.modelValue,this.wordAtCaret,e);this.$emit(`update:modelValue`,t),this.caret=0},insert({insertion:e,keepOpen:t,surroundingSpace:n=!0}){let r=this.modelValue.substring(0,this.caret)||``,i=this.modelValue.substring(this.caret)||``,a=/\s/,o=n&&!a.exec(r.slice(-1))&&r.length&&this.padEmoji>0?` `:``,s=n&&!a.exec(i[0])&&this.padEmoji?` `:``,c=[r,o,e,s,i].join(``);this.$emit(`update:modelValue`,c);let l=this.caret+(e+s+o).length;t||this.input.focus(),this.$nextTick(function(){this.input.setSelectionRange(l,l),this.caret=l})},replaceText(e,t){let n=this.suggestions.length||0;if(this.textAtCaret.length!==1&&(n>0||t)){let n=(t||this.suggestions[this.highlighted]).replacement,r=P.replaceWord(this.modelValue,this.wordAtCaret,n);this.$emit(`update:modelValue`,r),this.highlighted=0;let i=this.wordAtCaret.start+n.length;this.$nextTick(function(){this.input.focus(),this.input.setSelectionRange(i,i),this.caret=i}),e.preventDefault()}},cycleBackward(e){let t=this.suggestions.length||0;--this.highlighted,this.highlighted===-1?this.input.focus():this.highlighted<-1&&(this.highlighted=t-1),t>0&&e.preventDefault()},cycleForward(e){let t=this.suggestions.length||0;this.highlighted+=1,this.highlighted>=t&&(this.highlighted=-1,this.input.focus()),t>0&&e.preventDefault()},scrollIntoView(){let e=this.$refs.picker.$el,t=this.$el.closest(`.sidebar-scroller`)||this.$el.closest(`.post-form-modal-view`)||window,n=t===window?t.scrollY:t.scrollTop,r=n+(t===window?t.innerHeight:t.offsetHeight),i=e.offsetHeight+F(e,t).top,a=n+Math.max(0,i-r);t===window?t.scroll(0,a):t.scrollTop=a,this.$nextTick(()=>{let{offsetHeight:e}=this.input,{picker:t}=this.$refs;t.$el.getBoundingClientRect().bottom>window.innerHeight&&(t.$el.style.top=`auto`,t.$el.style.bottom=e+`px`)})},onPickerShown(){this.pickerShown=!0},onPickerClosed(){this.pickerShown=!1},onBlur(e){this.blurTimeout=setTimeout(()=>{this.focused=!1,this.setCaret(e)},200)},onClick(e,t){this.replaceText(e,t)},onFocus(e){this.blurTimeout&&=(clearTimeout(this.blurTimeout),null),this.focused=!0,this.setCaret(e),this.temporarilyHideSuggestions=!1},onKeyUp(e){let{key:t}=e;this.setCaret(e),t===`Escape`?this.temporarilyHideSuggestions=!0:this.temporarilyHideSuggestions=!1},onPaste(e){this.setCaret(e)},onKeyDown(e){let{ctrlKey:t,shiftKey:n,key:r}=e;this.newlineOnCtrlEnter&&t&&r===`Enter`&&(this.insert({insertion:` +`,surroundingSpace:!1}),e.stopPropagation(),e.preventDefault(),this.$nextTick(()=>{this.input.blur(),this.input.focus()})),this.temporarilyHideSuggestions||(r===`Tab`&&(n?this.cycleBackward(e):this.cycleForward(e)),r===`ArrowUp`?this.cycleBackward(e):r===`ArrowDown`&&this.cycleForward(e),r===`Enter`&&(t||this.replaceText(e))),r===`Escape`&&(this.temporarilyHideSuggestions||this.input.focus())},onInput(e){this.setCaret(e),this.$emit(`update:modelValue`,e.target.value)},onStickerUploaded(e){this.$emit(`sticker-uploaded`,e)},onStickerUploadFailed(e){this.$emit(`sticker-upload-Failed`,e)},setCaret({target:{selectionStart:e}}){this.caret=e,this.$nextTick(()=>{this.$refs.suggestorPopover?.updateStyles()})},autoCompleteItemLabel(e){return e.user?e.displayText+` `+e.detailText:this.maybeLocalizedEmojiName(e)}}},z={ref:`hiddenOverlayCaret`,class:`caret`},B=[`title`],V=[`id`],H=[`id`,`aria-label`,`aria-selected`,`onClick`],U={class:`image`},W=[`src`],G={key:1},K={class:`label`},q={key:0,class:`displayText`},J={key:1,class:`displayText`},Y={class:`detailText`};function X(m,_,v,y,b,x){let S=r(`screen-reader-notice`),C=r(`FAIcon`),w=r(`EmojiPicker`),T=r(`UnicodeDomainIndicator`),E=r(`Popover`);return n(),f(`div`,{ref:`root`,class:p([`input emoji-input`,{"-with-picker":!m.hideEmojiButton,"-textarea":m.input?.tagName===`TEXTAREA`}])},[t(m.$slots,`default`,{id:`textbox-`+m.randomSeed,ariaOwns:m.suggestionListId,ariaAutocomplete:`both`,ariaExpanded:m.showSuggestions,ariaActivedescendant:!m.showSuggestions||m.highlighted===-1?``:m.suggestionItemId(m.highlighted)}),h(`div`,{ref:`hiddenOverlay`,class:`hidden-overlay`,style:o(m.overlayStyle),"aria-hidden":!0},[h(`span`,null,a(m.preText),1),h(`span`,z,`x`,512),h(`span`,null,a(m.postText),1)],4),s(S,{ref:`screenReaderNotice`,"aria-live":`assertive`},null,512),m.enableEmojiPicker?(n(),f(d,{key:0},[m.hideEmojiButton?l(``,!0):(n(),f(`button`,{key:0,class:`button-unstyled emoji-picker-icon`,type:`button`,title:m.$t(`emoji.add_emoji`),onClick:_[0]||=g((...e)=>m.togglePicker&&m.togglePicker(...e),[`prevent`])},[s(C,{icon:[`far`,`face-smile-beam`]})],8,B)),m.enableEmojiPicker?(n(),c(w,{key:1,ref:`picker`,"enable-sticker-picker":m.enableStickerPicker,class:`emoji-picker-panel`,onEmoji:m.insert,onStickerUploaded:m.onStickerUploaded,onStickerUploadFailed:m.onStickerUploadFailed,onShow:m.onPickerShown,onClose:m.onPickerClosed},null,8,[`enable-sticker-picker`,`onEmoji`,`onStickerUploaded`,`onStickerUploadFailed`,`onShow`,`onClose`])):l(``,!0)],64)):l(``,!0),s(E,{ref:`suggestorPopover`,class:`autocomplete-panel`,placement:`bottom`,"hide-trigger":!0},{content:i(()=>[h(`div`,{id:m.suggestionListId,ref:`panel-body`,class:`autocomplete-panel-body`,role:`listbox`},[(n(!0),f(d,null,e(m.suggestions,(e,t)=>(n(),f(`div`,{id:m.suggestionItemId(t),key:t,class:p([`menu-item autocomplete-item`,{"-active":t===m.highlighted}]),role:`option`,"aria-label":m.autoCompleteItemLabel(e),"aria-selected":t===m.highlighted,onClick:g(t=>m.onClick(t,e),[`stop`,`prevent`])},[h(`span`,U,[e.img?(n(),f(`img`,{key:0,src:e.img},null,8,W)):(n(),f(`span`,G,a(e.replacement),1))]),h(`div`,K,[e.user?(n(),f(`span`,q,[u(a(e.displayText),1),s(T,{user:e.user,at:!1},null,8,[`user`])])):l(``,!0),e.user?l(``,!0):(n(),f(`span`,J,a(m.maybeLocalizedEmojiName(e)),1)),h(`span`,Y,a(e.detailText),1)])],10,H))),128))],8,V)]),_:1},512)],2)}var Z=m(R,[[`render`,X]]),Q=e=>{let t=$(e.emoji),n=e.store&&ee(e.store);return(r,i)=>{let a=r[0];return a===`:`&&e.emoji?t(r,i):a===`@`&&n?n(r):[]}},$=e=>(t,n)=>{let r=t.toLowerCase().substring(1);return e.map(e=>({...e,...n(e)})).filter(e=>e.names.concat(e.keywords).filter(e=>e.toLowerCase().includes(r)).length).map(e=>{let t=0;return t+=Math.max(...e.names.map(e=>e.toLowerCase()===r?200:0),0),t+=e.imageUrl?100:0,t+=Math.max(...e.names.map(e=>e.toLowerCase().startsWith(r)?10:0),0),t-=e.displayText.length,e.score=t,e}).sort((e,t)=>{let n=e.displayText>t.displayText?.5:-.5;return t.score-e.score+n})},ee=({dispatch:e,state:t})=>{let n=[],r=``,i=null,a=null,o=t=>e(`searchUsers`,{query:t}),s=e=>(a&&a(),new Promise((t,n)=>{i=setTimeout(()=>{o(e).then(t).catch(n)},300),a=()=>{clearTimeout(i),t([])}}));return async e=>{let i=e.toLowerCase().substr(1);return r===i?n:(n=[],r=i,i.includes(`@`)||await s(i),n=t.users.users.filter(e=>e.screen_name&&e.name&&(e.screen_name.toLowerCase().startsWith(i)||e.name.toLowerCase().startsWith(i))).slice(0,20).sort((e,t)=>{let n=0,r=0;n+=e.screen_name.toLowerCase().startsWith(i)?2:0,r+=t.screen_name.toLowerCase().startsWith(i)?2:0,n+=+!!e.name.toLowerCase().startsWith(i),r+=+!!t.name.toLowerCase().startsWith(i);let a=(r-n)*10,o=e.last_status_att.name?1:-1,c=e.screen_name>t.screen_name?1:-1;return a+s+c+o}).map(e=>({user:e,displayText:e.screen_name_ui,detailText:e.name,imageUrl:e.profile_image_url_original,replacement:`@`+e.screen_name+` `}))||[],n)}},te=e=>Object.keys(e).reduce((t,n)=>(t[(0,T.kebabCase)(n)]=e[n],t),{});export{F as a,L as i,Q as n,Z as r,te as t}; +//# sourceMappingURL=attributes_helper.service.Ku0p8pb2.js.map \ No newline at end of file diff --git a/priv/static/static/js/attributes_helper.service.Ku0p8pb2.js.map b/priv/static/static/js/attributes_helper.service.Ku0p8pb2.js.map new file mode 100644 index 000000000..f68865d91 --- /dev/null +++ b/priv/static/static/js/attributes_helper.service.Ku0p8pb2.js.map @@ -0,0 +1 @@ +{"version":3,"file":"attributes_helper.service.Ku0p8pb2.js","names":["library","Completion"],"sources":["../../../src/components/screen_reader_notice/screen_reader_notice.js?vue&type=script&src=true&lang.js","../../../src/components/screen_reader_notice/screen_reader_notice.vue","../../../src/services/completion/completion.js","../../../src/services/offset_finder/offset_finder.service.js","../../../src/services/random_seed/random_seed.service.js","../../../src/components/emoji_input/emoji_input.js?vue&type=script&src=true&lang.js","../../../src/components/emoji_input/emoji_input.vue","../../../src/components/emoji_input/suggestor.js","../../../src/services/attributes_helper/attributes_helper.service.js"],"sourcesContent":["const ScreenReaderNotice = {\n props: {\n ariaLive: {\n type: String,\n default: 'assertive',\n },\n },\n data() {\n return {\n currentText: '',\n }\n },\n methods: {\n announce(text) {\n this.currentText = text\n setTimeout(() => {\n this.currentText = ''\n }, 1000)\n },\n },\n}\n\nexport default ScreenReaderNotice\n","\n\n\n","import { find, reduce } from 'lodash'\n\nexport const replaceWord = (str, toReplace, replacement) => {\n return str.slice(0, toReplace.start) + replacement + str.slice(toReplace.end)\n}\n\nexport const wordAtPosition = (str, pos) => {\n const words = splitByWhitespaceBoundary(str)\n const wordsWithPosition = addPositionToWords(words)\n\n return find(wordsWithPosition, ({ start, end }) => start <= pos && end > pos)\n}\n\nexport const addPositionToWords = (words) => {\n return reduce(\n words,\n (result, word) => {\n const data = {\n word,\n start: 0,\n end: word.length,\n }\n\n if (result.length > 0) {\n const previous = result.pop()\n\n data.start += previous.end\n data.end += previous.end\n\n result.push(previous)\n }\n\n result.push(data)\n\n return result\n },\n [],\n )\n}\n\nexport const splitByWhitespaceBoundary = (str) => {\n const result = []\n let currentWord = ''\n for (let i = 0; i < str.length; i++) {\n const currentChar = str[i]\n // Starting a new word\n if (!currentWord) {\n currentWord = currentChar\n continue\n }\n // current character is whitespace while word isn't, or vice versa:\n // add our current word to results, start over the current word.\n if (!!currentChar.trim() !== !!currentWord.trim()) {\n result.push(currentWord)\n currentWord = currentChar\n continue\n }\n currentWord += currentChar\n }\n // Add the last word we were working on\n if (currentWord) {\n result.push(currentWord)\n }\n return result\n}\n\nconst completion = {\n wordAtPosition,\n addPositionToWords,\n splitByWhitespaceBoundary,\n replaceWord,\n}\n\nexport default completion\n","export const findOffset = (\n child,\n parent,\n { top = 0, left = 0 } = {},\n ignorePadding = true,\n) => {\n const result = {\n top: top + child.offsetTop,\n left: left + child.offsetLeft,\n }\n if (!ignorePadding && child !== window) {\n const { topPadding, leftPadding } = findPadding(child)\n result.top += ignorePadding ? 0 : topPadding\n result.left += ignorePadding ? 0 : leftPadding\n }\n\n if (\n child.offsetParent &&\n window.getComputedStyle(child.offsetParent).position !== 'sticky' &&\n (parent === window ||\n parent.contains(child.offsetParent) ||\n parent === child.offsetParent)\n ) {\n return findOffset(child.offsetParent, parent, result, false)\n } else {\n if (parent !== window) {\n const { topPadding, leftPadding } = findPadding(parent)\n result.top += topPadding\n result.left += leftPadding\n }\n return result\n }\n}\n\nconst findPadding = (el) => {\n const topPaddingStr = window.getComputedStyle(el)['padding-top']\n const topPadding = Number(\n topPaddingStr.substring(0, topPaddingStr.length - 2),\n )\n const leftPaddingStr = window.getComputedStyle(el)['padding-left']\n const leftPadding = Number(\n leftPaddingStr.substring(0, leftPaddingStr.length - 2),\n )\n\n return { topPadding, leftPadding }\n}\n","const genRandomSeed = () => `${Math.random()}`.replace('.', '-')\n\nexport default genRandomSeed\n","import { take } from 'lodash'\n\nimport Popover from 'src/components/popover/popover.vue'\nimport ScreenReaderNotice from 'src/components/screen_reader_notice/screen_reader_notice.vue'\nimport { ensureFinalFallback } from '../../i18n/languages.js'\nimport Completion from '../../services/completion/completion.js'\nimport { findOffset } from '../../services/offset_finder/offset_finder.service.js'\nimport genRandomSeed from '../../services/random_seed/random_seed.service.js'\nimport EmojiPicker from '../emoji_picker/emoji_picker.vue'\nimport UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'\n\nimport { useMergedConfigStore } from 'src/stores/merged_config.js'\n\nimport { library } from '@fortawesome/fontawesome-svg-core'\nimport { faSmileBeam } from '@fortawesome/free-regular-svg-icons'\n\nlibrary.add(faSmileBeam)\n\n/**\n * EmojiInput - augmented inputs for emoji and autocomplete support in inputs\n * without having to give up the comfort of and