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/326] 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/326] 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/326] 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/326] 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/326] =?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/326] =?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/326] */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/326] 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/326] 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/326] 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 0feb23a3082b65915e0abbe09c2df83f6014d6c3 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Fri, 16 Jan 2026 23:16:51 +0400 Subject: [PATCH 011/326] 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 012/326] 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 013/326] 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 014/326] 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 015/326] 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 016/326] 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 017/326] 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 018/326] 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 10ab71d1577dd54719cdf81172707f4080a95d49 Mon Sep 17 00:00:00 2001 From: Dryusdan Date: Wed, 21 Jan 2026 17:55:31 +0100 Subject: [PATCH 019/326] 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 020/326] 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 021/326] 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 022/326] 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 023/326] 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 024/326] 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 025/326] 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 026/326] 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 027/326] 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 028/326] 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 029/326] 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 030/326] 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 031/326] 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 032/326] 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 033/326] 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 034/326] 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 035/326] 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 036/326] 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 037/326] 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 038/326] 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 039/326] 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 040/326] 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 041/326] 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 042/326] 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 043/326] 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 044/326] 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 045/326] 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 046/326] 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 047/326] 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 048/326] 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 049/326] 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 050/326] 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 051/326] 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 052/326] 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 053/326] 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 054/326] 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 055/326] 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 056/326] 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 057/326] 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 058/326] 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 059/326] 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 060/326] 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 061/326] 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 062/326] 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 063/326] 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 064/326] 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 065/326] 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 066/326] 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 067/326] 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 068/326] 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 069/326] 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 070/326] 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 071/326] 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 072/326] 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 073/326] 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 074/326] 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 075/326] 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 076/326] 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 077/326] 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 078/326] 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 079/326] 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 080/326] 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 081/326] 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 082/326] 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 083/326] 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 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 084/326] 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 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 085/326] 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 784a319f2eb9b98e6e034c38b462dd1e11390359 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 14 May 2026 00:22:24 +0200 Subject: [PATCH 086/326] 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 1e65c7a69e3cbd84009563a8d1974f772188a4b7 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Fri, 22 May 2026 20:36:52 +0400 Subject: [PATCH 087/326] 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 add3844c6d8f6e540d139f55e44047d946d9559a Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 23 May 2026 11:38:33 +0400 Subject: [PATCH 088/326] 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 089/326] 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 090/326] 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 a84d16831f788d82ec77eceb51debf359cf397d2 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 25 May 2026 18:09:18 +0400 Subject: [PATCH 091/326] 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 092/326] 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 093/326] 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 094/326] 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 095/326] 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 096/326] 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 097/326] 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 098/326] 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 099/326] 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 100/326] 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 07c65adb0c5ad94610a998730cd81439bf498f8b Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 10 Jun 2026 22:26:16 +0200 Subject: [PATCH 101/326] 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 102/326] 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 103/326] 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 104/326] 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 105/326] 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 106/326] 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 107/326] 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 108/326] 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 109/326] 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 110/326] 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 111/326] 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 112/326] 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 113/326] 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 114/326] 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 115/326] 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 116/326] 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 117/326] 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 118/326] 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 119/326] 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 120/326] 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 121/326] 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 122/326] 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 123/326] 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 124/326] 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 125/326] 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 126/326] 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 127/326] 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 128/326] 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 129/326] 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 130/326] 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 131/326] 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 132/326] 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 133/326] 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 134/326] 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 135/326] 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 285/326] 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 286/326] 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 287/326] 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 288/326] 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 289/326] 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 290/326] 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 291/326] 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 292/326] 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 293/326] 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 294/326] 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 295/326] 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 296/326] 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 297/326] 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 298/326] 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 299/326] 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 300/326] 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 301/326] 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 302/326] 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 303/326] 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 304/326] 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 305/326] 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 306/326] 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