From e4ac2a7cd69aa5e87d9dc277c0271e15466e3215 Mon Sep 17 00:00:00 2001 From: tusooa Date: Fri, 16 Dec 2022 02:56:32 -0500 Subject: [PATCH 001/199] Detail backup states --- lib/pleroma/ecto_enums.ex | 8 ++ lib/pleroma/user/backup.ex | 116 ++++++++++++++---- lib/pleroma/workers/backup_worker.ex | 2 +- .../20221216052127_add_state_to_backups.exs | 21 ++++ test/pleroma/user/backup_test.exs | 48 +++++++- 5 files changed, 166 insertions(+), 29 deletions(-) create mode 100644 priv/repo/migrations/20221216052127_add_state_to_backups.exs diff --git a/lib/pleroma/ecto_enums.ex b/lib/pleroma/ecto_enums.ex index a4890b489..b346b39d6 100644 --- a/lib/pleroma/ecto_enums.ex +++ b/lib/pleroma/ecto_enums.ex @@ -27,3 +27,11 @@ failed: 4, manual: 5 ) + +defenum(Pleroma.User.Backup.State, + pending: 1, + running: 2, + complete: 3, + failed: 4, + invalid: 5 +) diff --git a/lib/pleroma/user/backup.ex b/lib/pleroma/user/backup.ex index 9df010605..74001f9c3 100644 --- a/lib/pleroma/user/backup.ex +++ b/lib/pleroma/user/backup.ex @@ -15,6 +15,7 @@ defmodule Pleroma.User.Backup do alias Pleroma.Bookmark alias Pleroma.Repo alias Pleroma.User + alias Pleroma.User.Backup.State alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.UserView @@ -25,12 +26,16 @@ defmodule Pleroma.User.Backup do field(:file_name, :string) field(:file_size, :integer, default: 0) field(:processed, :boolean, default: false) + field(:state, State, default: :invalid) + field(:processed_number, :integer, default: 0) belongs_to(:user, User, type: FlakeId.Ecto.CompatType) timestamps() end + @report_every 100 + def create(user, admin_id \\ nil) do with :ok <- validate_limit(user, admin_id), {:ok, backup} <- user |> new() |> Repo.insert() do @@ -46,7 +51,8 @@ def new(user) do %__MODULE__{ user_id: user.id, content_type: "application/zip", - file_name: name + file_name: name, + state: :pending } end @@ -109,27 +115,75 @@ def remove_outdated(%__MODULE__{id: latest_id, user_id: user_id}) do def get(id), do: Repo.get(__MODULE__, id) + defp set_state(backup, state, processed_number \\ nil) do + struct = + %{state: state} + |> Pleroma.Maps.put_if_present(:processed_number, processed_number) + + backup + |> cast(struct, [:state, :processed_number]) + |> Repo.update() + end + def process(%__MODULE__{} = backup) do - with {:ok, zip_file} <- export(backup), - {:ok, %{size: size}} <- File.stat(zip_file), - {:ok, _upload} <- upload(backup, zip_file) do - backup - |> cast(%{file_size: size, processed: true}, [:file_size, :processed]) - |> Repo.update() + set_state(backup, :running, 0) + + current_pid = self() + + Task.Supervisor.async_nolink( + Pleroma.TaskSupervisor, + fn -> + with {:ok, zip_file} <- export(backup, current_pid), + {:ok, %{size: size}} <- File.stat(zip_file), + {:ok, _upload} <- upload(backup, zip_file) do + backup + |> cast( + %{ + file_size: size, + processed: true, + state: :complete + }, + [:file_size, :processed, :state] + ) + |> Repo.update() + + send(current_pid, :completed) + end + end + ) + + wait_backup(backup, backup.processed_number) + end + + defp wait_backup(backup, current_processed) do + receive do + {:progress, new_processed} -> + total_processed = current_processed + new_processed + + with {:ok, updated_backup} <- set_state(backup, :running, total_processed) do + wait_backup(updated_backup, total_processed) + else + _ -> wait_backup(backup, total_processed) + end + + :completed -> + {:ok, get(backup.id)} + after + 30_000 -> set_state(backup, :failed) end end @files ['actor.json', 'outbox.json', 'likes.json', 'bookmarks.json'] - def export(%__MODULE__{} = backup) do + def export(%__MODULE__{} = backup, caller_pid \\ nil) do backup = Repo.preload(backup, :user) name = String.trim_trailing(backup.file_name, ".zip") dir = dir(name) with :ok <- File.mkdir(dir), - :ok <- actor(dir, backup.user), - :ok <- statuses(dir, backup.user), - :ok <- likes(dir, backup.user), - :ok <- bookmarks(dir, backup.user), + :ok <- actor(dir, backup.user, caller_pid), + :ok <- statuses(dir, backup.user, caller_pid), + :ok <- likes(dir, backup.user, caller_pid), + :ok <- bookmarks(dir, backup.user, caller_pid), {:ok, zip_path} <- :zip.create(String.to_charlist(dir <> ".zip"), @files, cwd: dir), {:ok, _} <- File.rm_rf(dir) do {:ok, to_string(zip_path)} @@ -157,11 +211,12 @@ def upload(%__MODULE__{} = backup, zip_path) do end end - defp actor(dir, user) do + defp actor(dir, user, caller_pid) do with {:ok, json} <- UserView.render("user.json", %{user: user}) |> Map.merge(%{"likes" => "likes.json", "bookmarks" => "bookmarks.json"}) |> Jason.encode() do + send(caller_pid, {:progress, 1}) File.write(Path.join(dir, "actor.json"), json) end end @@ -180,7 +235,9 @@ defp write_header(file, name) do ) end - defp write(query, dir, name, fun) do + defp should_report?(num), do: rem(num, @report_every) == 0 + + defp write(query, dir, name, fun, caller_pid) do path = Path.join(dir, "#{name}.json") with {:ok, file} <- File.open(path, [:write, :utf8]), @@ -192,35 +249,41 @@ defp write(query, dir, name, fun) do with {:ok, data} <- fun.(i), {:ok, str} <- Jason.encode(data), :ok <- IO.write(file, str <> ",\n") do + if should_report?(acc + 1) do + send(caller_pid, {:progress, @report_every}) + end + acc + 1 else _ -> acc end end) + send(caller_pid, {:progress, rem(total, @report_every)}) + with :ok <- :file.pwrite(file, {:eof, -2}, "\n],\n \"totalItems\": #{total}}") do File.close(file) end end end - defp bookmarks(dir, %{id: user_id} = _user) do + defp bookmarks(dir, %{id: user_id} = _user, caller_pid) do Bookmark |> where(user_id: ^user_id) |> join(:inner, [b], activity in assoc(b, :activity)) |> select([b, a], %{id: b.id, object: fragment("(?)->>'object'", a.data)}) - |> write(dir, "bookmarks", fn a -> {:ok, a.object} end) + |> write(dir, "bookmarks", fn a -> {:ok, a.object} end, caller_pid) end - defp likes(dir, user) do + defp likes(dir, user, caller_pid) do user.ap_id |> Activity.Queries.by_actor() |> Activity.Queries.by_type("Like") |> select([like], %{id: like.id, object: fragment("(?)->>'object'", like.data)}) - |> write(dir, "likes", fn a -> {:ok, a.object} end) + |> write(dir, "likes", fn a -> {:ok, a.object} end, caller_pid) end - defp statuses(dir, user) do + defp statuses(dir, user, caller_pid) do opts = %{} |> Map.put(:type, ["Create", "Announce"]) @@ -233,10 +296,15 @@ defp statuses(dir, user) do ] |> Enum.concat() |> ActivityPub.fetch_activities_query(opts) - |> write(dir, "outbox", fn a -> - with {:ok, activity} <- Transmogrifier.prepare_outgoing(a.data) do - {:ok, Map.delete(activity, "@context")} - end - end) + |> write( + dir, + "outbox", + fn a -> + with {:ok, activity} <- Transmogrifier.prepare_outgoing(a.data) do + {:ok, Map.delete(activity, "@context")} + end + end, + caller_pid + ) end end diff --git a/lib/pleroma/workers/backup_worker.ex b/lib/pleroma/workers/backup_worker.ex index 12ee70f00..a485ddb4b 100644 --- a/lib/pleroma/workers/backup_worker.ex +++ b/lib/pleroma/workers/backup_worker.ex @@ -51,7 +51,7 @@ def perform(%Job{args: %{"op" => "delete", "backup_id" => backup_id}}) do end @impl Oban.Worker - def timeout(_job), do: :timer.seconds(900) + def timeout(_job), do: :infinity defp has_email?(user) do not is_nil(user.email) and user.email != "" diff --git a/priv/repo/migrations/20221216052127_add_state_to_backups.exs b/priv/repo/migrations/20221216052127_add_state_to_backups.exs new file mode 100644 index 000000000..73b30fc35 --- /dev/null +++ b/priv/repo/migrations/20221216052127_add_state_to_backups.exs @@ -0,0 +1,21 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Repo.Migrations.AddStateToBackups do + use Ecto.Migration + + def up do + alter table(:backups) do + add(:state, :integer, default: 5) + add(:processed_number, :integer, default: 0) + end + end + + def down do + alter table(:backups) do + remove(:state) + remove(:processed_number) + end + end +end diff --git a/test/pleroma/user/backup_test.exs b/test/pleroma/user/backup_test.exs index 5c9b94000..a536b4a4a 100644 --- a/test/pleroma/user/backup_test.exs +++ b/test/pleroma/user/backup_test.exs @@ -39,7 +39,7 @@ test "it creates a backup record and an Oban job" do assert_enqueued(worker: BackupWorker, args: args) backup = Backup.get(args["backup_id"]) - assert %Backup{user_id: ^user_id, processed: false, file_size: 0} = backup + assert %Backup{user_id: ^user_id, processed: false, file_size: 0, state: :pending} = backup end test "it return an error if the export limit is over" do @@ -59,7 +59,30 @@ test "it process a backup record" do assert {:ok, %Oban.Job{args: %{"backup_id" => backup_id} = args}} = Backup.create(user) assert {:ok, backup} = perform_job(BackupWorker, args) assert backup.file_size > 0 - assert %Backup{id: ^backup_id, processed: true, user_id: ^user_id} = backup + assert %Backup{id: ^backup_id, processed: true, user_id: ^user_id, state: :complete} = backup + + delete_job_args = %{"op" => "delete", "backup_id" => backup_id} + + assert_enqueued(worker: BackupWorker, args: delete_job_args) + assert {:ok, backup} = perform_job(BackupWorker, delete_job_args) + refute Backup.get(backup_id) + + email = Pleroma.Emails.UserEmail.backup_is_ready_email(backup) + + assert_email_sent( + to: {user.name, user.email}, + html_body: email.html_body + ) + end + + test "it updates states of the backup" do + clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) + %{id: user_id} = user = insert(:user) + + assert {:ok, %Oban.Job{args: %{"backup_id" => backup_id} = args}} = Backup.create(user) + assert {:ok, backup} = perform_job(BackupWorker, args) + assert backup.file_size > 0 + assert %Backup{id: ^backup_id, processed: true, user_id: ^user_id, state: :complete} = backup delete_job_args = %{"op" => "delete", "backup_id" => backup_id} @@ -148,7 +171,7 @@ test "it creates a zip archive with user data" do Bookmark.create(user.id, status3.id) assert {:ok, backup} = user |> Backup.new() |> Repo.insert() - assert {:ok, path} = Backup.export(backup) + assert {:ok, path} = Backup.export(backup, self()) assert {:ok, zipfile} = :zip.zip_open(String.to_charlist(path), [:memory]) assert {:ok, {'actor.json', json}} = :zip.zip_get('actor.json', zipfile) @@ -230,6 +253,23 @@ test "it creates a zip archive with user data" do File.rm!(path) end + test "it counts the correct number processed" do + user = insert(:user, %{nickname: "cofe", name: "Cofe", ap_id: "http://cofe.io/users/cofe"}) + + Enum.map(1..120, fn i -> + {:ok, status} = CommonAPI.post(user, %{status: "status #{i}"}) + CommonAPI.favorite(user, status.id) + Bookmark.create(user.id, status.id) + end) + + assert {:ok, backup} = user |> Backup.new() |> Repo.insert() + {:ok, backup} = Backup.process(backup) + + assert backup.processed_number == 1 + 120 + 120 + 120 + + Backup.delete(backup) + end + describe "it uploads and deletes a backup archive" do setup do clear_config([Pleroma.Upload, :base_url], "https://s3.amazonaws.com") @@ -246,7 +286,7 @@ test "it creates a zip archive with user data" do Bookmark.create(user.id, status3.id) assert {:ok, backup} = user |> Backup.new() |> Repo.insert() - assert {:ok, path} = Backup.export(backup) + assert {:ok, path} = Backup.export(backup, self()) [path: path, backup: backup] end From bdd63d2a3a5fe76f8a8ac1f71cb8698be85bfef4 Mon Sep 17 00:00:00 2001 From: tusooa Date: Fri, 16 Dec 2022 14:10:48 -0500 Subject: [PATCH 002/199] Expose backup status via Pleroma API --- .../operations/pleroma_backup_operation.ex | 8 +++- .../web/pleroma_api/views/backup_view.ex | 10 +++++ .../pleroma_api/views/backup_view_test.exs | 39 +++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex index 45fa2b058..5655527e0 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex @@ -64,7 +64,9 @@ defp backup do content_type: %Schema{type: :string}, file_name: %Schema{type: :string}, file_size: %Schema{type: :integer}, - processed: %Schema{type: :boolean} + processed: %Schema{type: :boolean, description: "whether this backup has succeeded"}, + state: %Schema{type: :string, description: "the state of the backup", enum: ["pending", "running", "complete", "failed"]}, + processed_number: %Schema{type: :integer, description: "the number of records processed"} }, example: %{ "content_type" => "application/zip", @@ -72,7 +74,9 @@ defp backup do "https://cofe.fe:4000/media/backups/archive-foobar-20200908T164207-Yr7vuT5Wycv-sN3kSN2iJ0k-9pMo60j9qmvRCdDqIew.zip", "file_size" => 4105, "inserted_at" => "2020-09-08T16:42:07.000Z", - "processed" => true + "processed" => true, + "state" => "complete", + "processed_number": 20 } } end diff --git a/lib/pleroma/web/pleroma_api/views/backup_view.ex b/lib/pleroma/web/pleroma_api/views/backup_view.ex index d778590f0..02d891791 100644 --- a/lib/pleroma/web/pleroma_api/views/backup_view.ex +++ b/lib/pleroma/web/pleroma_api/views/backup_view.ex @@ -9,12 +9,22 @@ defmodule Pleroma.Web.PleromaAPI.BackupView do alias Pleroma.Web.CommonAPI.Utils def render("show.json", %{backup: %Backup{} = backup}) do + # To deal with records before the migration + state = + if backup.state == :invalid do + if backup.processed, do: :complete, else: :failed + else + backup.state + end + %{ id: backup.id, content_type: backup.content_type, url: download_url(backup), file_size: backup.file_size, processed: backup.processed, + state: to_string(state), + processed_number: backup.processed_number, inserted_at: Utils.to_masto_date(backup.inserted_at) } end diff --git a/test/pleroma/web/pleroma_api/views/backup_view_test.exs b/test/pleroma/web/pleroma_api/views/backup_view_test.exs index a86688bc4..6908463d6 100644 --- a/test/pleroma/web/pleroma_api/views/backup_view_test.exs +++ b/test/pleroma/web/pleroma_api/views/backup_view_test.exs @@ -15,4 +15,43 @@ test "it renders the ID" do result = BackupView.render("show.json", backup: backup) assert result.id == backup.id end + + test "it renders the state and processed_number" do + user = insert(:user) + backup = Backup.new(user) + + result = BackupView.render("show.json", backup: backup) + assert result.state == to_string(backup.state) + assert result.processed_number == backup.processed_number + end + + test "it renders failed state with legacy records" do + backup = %Backup{ + id: 0, + content_type: "application/zip", + file_name: "dummy", + file_size: 1, + state: :invalid, + processed: true, + processed_number: 1, + inserted_at: NaiveDateTime.utc_now() + } + + result = BackupView.render("show.json", backup: backup) + assert result.state == "complete" + + backup = %Backup{ + id: 0, + content_type: "application/zip", + file_name: "dummy", + file_size: 1, + state: :invalid, + processed: false, + processed_number: 1, + inserted_at: NaiveDateTime.utc_now() + } + + result = BackupView.render("show.json", backup: backup) + assert result.state == "failed" + end end From 46ab97d72145a122cc0e0ada3cd9f0af5449bdf3 Mon Sep 17 00:00:00 2001 From: tusooa Date: Fri, 16 Dec 2022 14:13:46 -0500 Subject: [PATCH 003/199] Simplify backup update clause --- lib/pleroma/user/backup.ex | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/user/backup.ex b/lib/pleroma/user/backup.ex index 74001f9c3..97b8718c1 100644 --- a/lib/pleroma/user/backup.ex +++ b/lib/pleroma/user/backup.ex @@ -160,11 +160,8 @@ defp wait_backup(backup, current_processed) do {:progress, new_processed} -> total_processed = current_processed + new_processed - with {:ok, updated_backup} <- set_state(backup, :running, total_processed) do - wait_backup(updated_backup, total_processed) - else - _ -> wait_backup(backup, total_processed) - end + set_state(backup, :running, total_processed) + wait_backup(backup, total_processed) :completed -> {:ok, get(backup.id)} From a1b95922c5c4a4351b52c0f7a484a96efed08be9 Mon Sep 17 00:00:00 2001 From: tusooa Date: Fri, 16 Dec 2022 14:16:07 -0500 Subject: [PATCH 004/199] Fix compile error --- lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex index 5655527e0..ea3fc230a 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex @@ -76,7 +76,7 @@ defp backup do "inserted_at" => "2020-09-08T16:42:07.000Z", "processed" => true, "state" => "complete", - "processed_number": 20 + "processed_number" => 20 } } end From 070fbb89e1bc51669b03555b72c6852a769f1e15 Mon Sep 17 00:00:00 2001 From: tusooa Date: Fri, 16 Dec 2022 14:29:21 -0500 Subject: [PATCH 005/199] Lint --- .../api_spec/operations/pleroma_backup_operation.ex | 6 +++++- lib/pleroma/web/pleroma_api/views/backup_view.ex | 10 +++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex index ea3fc230a..400f3825d 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex @@ -65,7 +65,11 @@ defp backup do file_name: %Schema{type: :string}, file_size: %Schema{type: :integer}, processed: %Schema{type: :boolean, description: "whether this backup has succeeded"}, - state: %Schema{type: :string, description: "the state of the backup", enum: ["pending", "running", "complete", "failed"]}, + state: %Schema{ + type: :string, + description: "the state of the backup", + enum: ["pending", "running", "complete", "failed"] + }, processed_number: %Schema{type: :integer, description: "the number of records processed"} }, example: %{ diff --git a/lib/pleroma/web/pleroma_api/views/backup_view.ex b/lib/pleroma/web/pleroma_api/views/backup_view.ex index 02d891791..20403aeee 100644 --- a/lib/pleroma/web/pleroma_api/views/backup_view.ex +++ b/lib/pleroma/web/pleroma_api/views/backup_view.ex @@ -11,11 +11,11 @@ defmodule Pleroma.Web.PleromaAPI.BackupView do def render("show.json", %{backup: %Backup{} = backup}) do # To deal with records before the migration state = - if backup.state == :invalid do - if backup.processed, do: :complete, else: :failed - else - backup.state - end + if backup.state == :invalid do + if backup.processed, do: :complete, else: :failed + else + backup.state + end %{ id: backup.id, From 7d3e4eaeb94a5381d56a3281e5813b7e0c63c8dd Mon Sep 17 00:00:00 2001 From: tusooa Date: Sun, 18 Dec 2022 15:55:52 -0500 Subject: [PATCH 006/199] Log errors more extensively --- lib/pleroma/user/backup.ex | 105 +++++++++++++++++++++--------- test/pleroma/user/backup_test.exs | 50 ++++++++++++++ 2 files changed, 125 insertions(+), 30 deletions(-) diff --git a/lib/pleroma/user/backup.ex b/lib/pleroma/user/backup.ex index 97b8718c1..cb9a40ba1 100644 --- a/lib/pleroma/user/backup.ex +++ b/lib/pleroma/user/backup.ex @@ -9,6 +9,7 @@ defmodule Pleroma.User.Backup do import Ecto.Query import Pleroma.Web.Gettext + require Logger require Pleroma.Constants alias Pleroma.Activity @@ -130,48 +131,79 @@ def process(%__MODULE__{} = backup) do current_pid = self() - Task.Supervisor.async_nolink( - Pleroma.TaskSupervisor, - fn -> - with {:ok, zip_file} <- export(backup, current_pid), - {:ok, %{size: size}} <- File.stat(zip_file), - {:ok, _upload} <- upload(backup, zip_file) do - backup - |> cast( - %{ - file_size: size, - processed: true, - state: :complete - }, - [:file_size, :processed, :state] - ) - |> Repo.update() + task = + Task.Supervisor.async_nolink( + Pleroma.TaskSupervisor, + __MODULE__, + :do_process, + [backup, current_pid] + ) - send(current_pid, :completed) - end - end - ) - - wait_backup(backup, backup.processed_number) + wait_backup(backup, backup.processed_number, task) end - defp wait_backup(backup, current_processed) do + def do_process(backup, current_pid) do + with {:ok, zip_file} <- export(backup, current_pid), + {:ok, %{size: size}} <- File.stat(zip_file), + {:ok, _upload} <- upload(backup, zip_file) do + backup + |> cast( + %{ + file_size: size, + processed: true, + state: :complete + }, + [:file_size, :processed, :state] + ) + |> Repo.update() + end + end + + defp wait_backup(backup, current_processed, task) do receive do {:progress, new_processed} -> total_processed = current_processed + new_processed set_state(backup, :running, total_processed) - wait_backup(backup, total_processed) + wait_backup(backup, total_processed, task) - :completed -> - {:ok, get(backup.id)} + {:DOWN, _ref, _proc, _pid, reason} -> + backup = get(backup.id) + + if reason != :normal do + Logger.error("Backup #{backup.id} process ended abnormally: #{inspect(reason)}") + + {:ok, backup} = set_state(backup, :failed) + + {:error, + %{ + backup: backup, + reason: :exit, + details: reason + }} + else + {:ok, backup} + end after - 30_000 -> set_state(backup, :failed) + 30_000 -> + Logger.error( + "Backup #{backup.id} timed out after no response for 30 seconds, terminating" + ) + + Task.Supervisor.terminate_child(Pleroma.TaskSupervisor, task.pid) + + {:ok, backup} = set_state(backup, :failed) + + {:error, + %{ + backup: backup, + reason: :timeout + }} end end @files ['actor.json', 'outbox.json', 'likes.json', 'bookmarks.json'] - def export(%__MODULE__{} = backup, caller_pid \\ nil) do + def export(%__MODULE__{} = backup, caller_pid) do backup = Repo.preload(backup, :user) name = String.trim_trailing(backup.file_name, ".zip") dir = dir(name) @@ -243,7 +275,12 @@ defp write(query, dir, name, fun, caller_pid) do query |> Pleroma.Repo.chunk_stream(100) |> Enum.reduce(0, fn i, acc -> - with {:ok, data} <- fun.(i), + with {:ok, data} <- + (try do + fun.(i) + rescue + e -> {:error, e} + end), {:ok, str} <- Jason.encode(data), :ok <- IO.write(file, str <> ",\n") do if should_report?(acc + 1) do @@ -252,7 +289,15 @@ defp write(query, dir, name, fun, caller_pid) do acc + 1 else - _ -> acc + {:error, e} -> + Logger.warn( + "Error processing backup item: #{inspect(e)}\n The item is: #{inspect(i)}" + ) + + acc + + _ -> + acc end end) diff --git a/test/pleroma/user/backup_test.exs b/test/pleroma/user/backup_test.exs index a536b4a4a..066bf6ba8 100644 --- a/test/pleroma/user/backup_test.exs +++ b/test/pleroma/user/backup_test.exs @@ -270,6 +270,56 @@ test "it counts the correct number processed" do Backup.delete(backup) end + test "it handles errors" do + user = insert(:user, %{nickname: "cofe", name: "Cofe", ap_id: "http://cofe.io/users/cofe"}) + + Enum.map(1..120, fn i -> + {:ok, _status} = CommonAPI.post(user, %{status: "status #{i}"}) + end) + + assert {:ok, backup} = user |> Backup.new() |> Repo.insert() + + with_mock Pleroma.Web.ActivityPub.Transmogrifier, + [:passthrough], + prepare_outgoing: fn data -> + object = + data["object"] + |> Pleroma.Object.normalize(fetch: false) + |> Map.get(:data) + + data = data |> Map.put("object", object) + + if String.contains?(data["object"]["content"], "119"), + do: raise(%Postgrex.Error{}), + else: {:ok, data} + end do + {:ok, backup} = Backup.process(backup) + assert backup.processed + assert backup.state == :complete + assert backup.processed_number == 1 + 119 + + Backup.delete(backup) + end + end + + test "it handles unrecoverable exceptions" do + user = insert(:user, %{nickname: "cofe", name: "Cofe", ap_id: "http://cofe.io/users/cofe"}) + + assert {:ok, backup} = user |> Backup.new() |> Repo.insert() + + with_mock Backup, [:passthrough], do_process: fn _, _ -> raise "mock exception" end do + {:error, %{backup: backup, reason: :exit}} = Backup.process(backup) + + assert backup.state == :failed + end + + with_mock Backup, [:passthrough], do_process: fn _, _ -> Process.sleep(:timer.seconds(32)) end do + {:error, %{backup: backup, reason: :timeout}} = Backup.process(backup) + + assert backup.state == :failed + end + end + describe "it uploads and deletes a backup archive" do setup do clear_config([Pleroma.Upload, :base_url], "https://s3.amazonaws.com") From 179efd94677d1d30bdbbbbaafc899c8c908181d2 Mon Sep 17 00:00:00 2001 From: tusooa Date: Sat, 24 Dec 2022 00:17:17 -0500 Subject: [PATCH 007/199] Make backup parameters configurable --- config/config.exs | 4 +++- config/description.exs | 15 +++++++++++++++ lib/pleroma/user/backup.ex | 37 ++++++++++++++++++++++++++----------- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/config/config.exs b/config/config.exs index e41ec2f91..ecb592b9c 100644 --- a/config/config.exs +++ b/config/config.exs @@ -874,7 +874,9 @@ config :pleroma, Pleroma.User.Backup, purge_after_days: 30, limit_days: 7, - dir: nil + dir: nil, + process_wait_time: 30_000, + process_chunk_size: 100 config :pleroma, ConcurrentLimiter, [ {Pleroma.Web.RichMedia.Helpers, [max_running: 5, max_waiting: 5]}, diff --git a/config/description.exs b/config/description.exs index bf4734426..996267558 100644 --- a/config/description.exs +++ b/config/description.exs @@ -3394,6 +3394,21 @@ type: :integer, description: "Limit user to export not more often than once per N days", suggestions: [7] + }, + %{ + key: :process_wait_time, + type: :integer, + label: "Process Wait Time", + description: + "The amount of time to wait for backup to report progress, in milliseconds. If no progress is received from the backup job for that much time, terminate it and deem it failed.", + suggestions: [30_000] + }, + %{ + key: :process_chunk_size, + type: :integer, + label: "Process Chunk Size", + description: "The number of activities to fetch in the backup job for each chunk.", + suggestions: [100] } ] }, diff --git a/lib/pleroma/user/backup.ex b/lib/pleroma/user/backup.ex index cb9a40ba1..447fca2a1 100644 --- a/lib/pleroma/user/backup.ex +++ b/lib/pleroma/user/backup.ex @@ -35,8 +35,6 @@ defmodule Pleroma.User.Backup do timestamps() end - @report_every 100 - def create(user, admin_id \\ nil) do with :ok <- validate_limit(user, admin_id), {:ok, backup} <- user |> new() |> Repo.insert() do @@ -160,6 +158,8 @@ def do_process(backup, current_pid) do end defp wait_backup(backup, current_processed, task) do + wait_time = Pleroma.Config.get([__MODULE__, :process_wait_time]) + receive do {:progress, new_processed} -> total_processed = current_processed + new_processed @@ -175,6 +175,8 @@ defp wait_backup(backup, current_processed, task) do {:ok, backup} = set_state(backup, :failed) + cleanup(backup) + {:error, %{ backup: backup, @@ -185,15 +187,17 @@ defp wait_backup(backup, current_processed, task) do {:ok, backup} end after - 30_000 -> + wait_time -> Logger.error( - "Backup #{backup.id} timed out after no response for 30 seconds, terminating" + "Backup #{backup.id} timed out after no response for #{wait_time}ms, terminating" ) Task.Supervisor.terminate_child(Pleroma.TaskSupervisor, task.pid) {:ok, backup} = set_state(backup, :failed) + cleanup(backup) + {:error, %{ backup: backup, @@ -205,8 +209,7 @@ defp wait_backup(backup, current_processed, task) do @files ['actor.json', 'outbox.json', 'likes.json', 'bookmarks.json'] def export(%__MODULE__{} = backup, caller_pid) do backup = Repo.preload(backup, :user) - name = String.trim_trailing(backup.file_name, ".zip") - dir = dir(name) + dir = backup_tempdir(backup) with :ok <- File.mkdir(dir), :ok <- actor(dir, backup.user, caller_pid), @@ -264,16 +267,28 @@ defp write_header(file, name) do ) end - defp should_report?(num), do: rem(num, @report_every) == 0 + defp should_report?(num, chunk_size), do: rem(num, chunk_size) == 0 + + defp backup_tempdir(backup) do + name = String.trim_trailing(backup.file_name, ".zip") + dir(name) + end + + defp cleanup(backup) do + dir = backup_tempdir(backup) + File.rm_rf(dir) + end defp write(query, dir, name, fun, caller_pid) do path = Path.join(dir, "#{name}.json") + chunk_size = Pleroma.Config.get([__MODULE__, :process_chunk_size]) + with {:ok, file} <- File.open(path, [:write, :utf8]), :ok <- write_header(file, name) do total = query - |> Pleroma.Repo.chunk_stream(100) + |> Pleroma.Repo.chunk_stream(chunk_size, _returns_as = :one, timeout: :infinity) |> Enum.reduce(0, fn i, acc -> with {:ok, data} <- (try do @@ -283,8 +298,8 @@ defp write(query, dir, name, fun, caller_pid) do end), {:ok, str} <- Jason.encode(data), :ok <- IO.write(file, str <> ",\n") do - if should_report?(acc + 1) do - send(caller_pid, {:progress, @report_every}) + if should_report?(acc + 1, chunk_size) do + send(caller_pid, {:progress, chunk_size}) end acc + 1 @@ -301,7 +316,7 @@ defp write(query, dir, name, fun, caller_pid) do end end) - send(caller_pid, {:progress, rem(total, @report_every)}) + send(caller_pid, {:progress, rem(total, chunk_size)}) with :ok <- :file.pwrite(file, {:eof, -2}, "\n],\n \"totalItems\": #{total}}") do File.close(file) From 9a2523a09a2d9ccf5db12ef648444c51a98ab113 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Mon, 6 Mar 2023 21:16:34 +0100 Subject: [PATCH 008/199] instances: Store some metadata based on NodeInfo --- lib/pleroma/instances/instance.ex | 100 +++++++++++++++++- .../20230306112859_instances_add_metadata.exs | 14 +++ test/fixtures/mastodon-nodeinfo20.json | 1 + .../mastodon-well-known-nodeinfo.json | 1 + test/fixtures/wildebeest-nodeinfo21.json | 1 + .../wildebeest-well-known-nodeinfo.json | 1 + test/pleroma/instances/instance_test.exs | 60 +++++++++++ 7 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 priv/repo/migrations/20230306112859_instances_add_metadata.exs create mode 100644 test/fixtures/mastodon-nodeinfo20.json create mode 100644 test/fixtures/mastodon-well-known-nodeinfo.json create mode 100644 test/fixtures/wildebeest-nodeinfo21.json create mode 100644 test/fixtures/wildebeest-well-known-nodeinfo.json diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex index a5529ad44..9756c66dc 100644 --- a/lib/pleroma/instances/instance.ex +++ b/lib/pleroma/instances/instance.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Instances.Instance do alias Pleroma.Instances alias Pleroma.Instances.Instance + alias Pleroma.Maps alias Pleroma.Repo alias Pleroma.User alias Pleroma.Workers.BackgroundWorker @@ -24,6 +25,14 @@ defmodule Pleroma.Instances.Instance do field(:favicon, :string) field(:favicon_updated_at, :naive_datetime) + embeds_one :metadata, Pleroma.Instances.Metadata, primary_key: false do + field(:software_name, :string) + field(:software_version, :string) + field(:software_repository, :string) + end + + field(:metadata_updated_at, :utc_datetime) + timestamps() end @@ -31,11 +40,17 @@ defmodule Pleroma.Instances.Instance do def changeset(struct, params \\ %{}) do struct - |> cast(params, [:host, :unreachable_since, :favicon, :favicon_updated_at]) + |> cast(params, __schema__(:fields) -- [:metadata]) + |> cast_embed(:metadata, with: &metadata_changeset/2) |> validate_required([:host]) |> unique_constraint(:host) end + def metadata_changeset(struct, params \\ %{}) do + struct + |> cast(params, [:software_name, :software_version, :software_repository]) + end + def filter_reachable([]), do: %{} def filter_reachable(urls_or_hosts) when is_list(urls_or_hosts) do @@ -198,6 +213,89 @@ defp scrape_favicon(%URI{} = instance_uri) do end end + def get_or_update_metadata(%URI{host: host} = instance_uri) do + existing_record = Repo.get_by(Instance, %{host: host}) + now = NaiveDateTime.utc_now() + + if existing_record && existing_record.metadata_updated_at && + NaiveDateTime.diff(now, existing_record.metadata_updated_at) < 86_400 do + existing_record.metadata + else + metadata = scrape_metadata(instance_uri) + + if existing_record do + existing_record + |> changeset(%{metadata: metadata, metadata_updated_at: now}) + |> Repo.update() + else + %Instance{} + |> changeset(%{host: host, metadata: metadata, metadata_updated_at: now}) + |> Repo.insert() + end + + metadata + end + end + + defp get_nodeinfo_uri(well_known) do + links = Map.get(well_known, "links", []) + + nodeinfo21 = + Enum.find(links, &(&1["rel"] == "http://nodeinfo.diaspora.software/ns/schema/2.1"))["href"] + + nodeinfo20 = + Enum.find(links, &(&1["rel"] == "http://nodeinfo.diaspora.software/ns/schema/2.0"))["href"] + + cond do + is_binary(nodeinfo21) -> {:ok, nodeinfo21} + is_binary(nodeinfo20) -> {:ok, nodeinfo20} + true -> {:error, :no_links} + end + end + + defp scrape_metadata(%URI{} = instance_uri) do + try do + with {_, true} <- {:reachable, reachable?(instance_uri.host)}, + {:ok, %Tesla.Env{body: well_known_body}} <- + instance_uri + |> URI.merge("/.well-known/nodeinfo") + |> to_string() + |> Pleroma.HTTP.get([{"accept", "application/json"}]), + {:ok, well_known_json} <- Jason.decode(well_known_body), + {:ok, nodeinfo_uri} <- get_nodeinfo_uri(well_known_json), + {:ok, %Tesla.Env{body: nodeinfo_body}} <- + Pleroma.HTTP.get(nodeinfo_uri, [{"accept", "application/json"}]), + {:ok, nodeinfo} <- Jason.decode(nodeinfo_body) do + # Can extract more metadata from NodeInfo but need to be careful about it's size, + # can't just dump the entire thing + software = Map.get(nodeinfo, "software", %{}) + + %{ + software_name: software["name"], + software_version: software["version"] + } + |> Maps.put_if_present(:software_repository, software["repository"]) + else + {:reachable, false} -> + Logger.debug( + "Instance.scrape_metadata(\"#{to_string(instance_uri)}\") ignored unreachable host" + ) + + nil + + _ -> + nil + end + rescue + e -> + Logger.warn( + "Instance.scrape_metadata(\"#{to_string(instance_uri)}\") error: #{inspect(e)}" + ) + + nil + end + end + @doc """ Deletes all users from an instance in a background task, thus also deleting all of those users' activities and notifications. diff --git a/priv/repo/migrations/20230306112859_instances_add_metadata.exs b/priv/repo/migrations/20230306112859_instances_add_metadata.exs new file mode 100644 index 000000000..898f5220e --- /dev/null +++ b/priv/repo/migrations/20230306112859_instances_add_metadata.exs @@ -0,0 +1,14 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Repo.Migrations.InstancesAddMetadata do + use Ecto.Migration + + def change do + alter table(:instances) do + add(:metadata, :map) + add(:metadata_updated_at, :utc_datetime) + end + end +end diff --git a/test/fixtures/mastodon-nodeinfo20.json b/test/fixtures/mastodon-nodeinfo20.json new file mode 100644 index 000000000..35010fdf0 --- /dev/null +++ b/test/fixtures/mastodon-nodeinfo20.json @@ -0,0 +1 @@ +{"version":"2.0","software":{"name":"mastodon","version":"4.1.0"},"protocols":["activitypub"],"services":{"outbound":[],"inbound":[]},"usage":{"users":{"total":971090,"activeMonth":167218,"activeHalfyear":384808},"localPosts":52071541},"openRegistrations":true,"metadata":{}} \ No newline at end of file diff --git a/test/fixtures/mastodon-well-known-nodeinfo.json b/test/fixtures/mastodon-well-known-nodeinfo.json new file mode 100644 index 000000000..237d5462a --- /dev/null +++ b/test/fixtures/mastodon-well-known-nodeinfo.json @@ -0,0 +1 @@ +{"links":[{"rel":"http://nodeinfo.diaspora.software/ns/schema/2.0","href":"https://mastodon.example.org/nodeinfo/2.0"}]} \ No newline at end of file diff --git a/test/fixtures/wildebeest-nodeinfo21.json b/test/fixtures/wildebeest-nodeinfo21.json new file mode 100644 index 000000000..c6af474bf --- /dev/null +++ b/test/fixtures/wildebeest-nodeinfo21.json @@ -0,0 +1 @@ +{"version":"2.1","software":{"name":"wildebeest","version":"0.0.1","repository":"https://github.com/cloudflare/wildebeest"},"protocols":["activitypub"],"usage":{"users":{"total":1,"activeMonth":1,"activeHalfyear":1}},"openRegistrations":false,"metadata":{"upstream":{"name":"mastodon","version":"3.5.1"}}} \ No newline at end of file diff --git a/test/fixtures/wildebeest-well-known-nodeinfo.json b/test/fixtures/wildebeest-well-known-nodeinfo.json new file mode 100644 index 000000000..c7ddb43af --- /dev/null +++ b/test/fixtures/wildebeest-well-known-nodeinfo.json @@ -0,0 +1 @@ +{"links":[{"rel":"http://nodeinfo.diaspora.software/ns/schema/2.0","href":"https://wildebeest.example.org/nodeinfo/2.0"},{"rel":"http://nodeinfo.diaspora.software/ns/schema/2.1","href":"https://wildebeest.example.org/nodeinfo/2.1"}]} \ No newline at end of file diff --git a/test/pleroma/instances/instance_test.exs b/test/pleroma/instances/instance_test.exs index 861519bce..a769f9362 100644 --- a/test/pleroma/instances/instance_test.exs +++ b/test/pleroma/instances/instance_test.exs @@ -161,6 +161,66 @@ test "Doesn't scrapes unreachable instances" do end end + describe "get_or_update_metadata/1" do + test "Scrapes Wildebeest NodeInfo" do + Tesla.Mock.mock(fn + %{url: "https://wildebeest.example.org/.well-known/nodeinfo"} -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/wildebeest-well-known-nodeinfo.json") + } + + %{url: "https://wildebeest.example.org/nodeinfo/2.1"} -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/wildebeest-nodeinfo21.json") + } + end) + + expected = %{ + software_name: "wildebeest", + software_repository: "https://github.com/cloudflare/wildebeest", + software_version: "0.0.1" + } + + assert expected == + Instance.get_or_update_metadata(URI.parse("https://wildebeest.example.org/")) + + expected = %Pleroma.Instances.Instance.Pleroma.Instances.Metadata{ + software_name: "wildebeest", + software_repository: "https://github.com/cloudflare/wildebeest", + software_version: "0.0.1" + } + + assert expected == + Repo.get_by(Pleroma.Instances.Instance, %{host: "wildebeest.example.org"}).metadata + end + + test "Scrapes Mastodon NodeInfo" do + Tesla.Mock.mock(fn + %{url: "https://mastodon.example.org/.well-known/nodeinfo"} -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/mastodon-well-known-nodeinfo.json") + } + + %{url: "https://mastodon.example.org/nodeinfo/2.0"} -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/mastodon-nodeinfo20.json") + } + end) + + expected = %{ + software_name: "mastodon", + software_version: "4.1.0" + } + + assert expected == + Instance.get_or_update_metadata(URI.parse("https://mastodon.example.org/")) + end + end + test "delete_users_and_activities/1 deletes remote instance users and activities" do [mario, luigi, _peach, wario] = users = [ From e444832bb5d0a02ca1a0988c3490d72b99758899 Mon Sep 17 00:00:00 2001 From: Duponin Date: Mon, 10 Apr 2023 20:17:30 +0000 Subject: [PATCH 009/199] fix OTP install documentation 'su pleroma' will never work if 'pleroma' user has no password, which is better for security purpose. If admin has no 'sudo' binary, I'm expecting them to be skilled enough to make their way on their own. --- docs/installation/otp_en.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/installation/otp_en.md b/docs/installation/otp_en.md index 13f9636f3..e7dec9039 100644 --- a/docs/installation/otp_en.md +++ b/docs/installation/otp_en.md @@ -111,13 +111,13 @@ adduser --system --shell /bin/false --home /opt/pleroma pleroma export FLAVOUR="amd64-musl" # Clone the release build into a temporary directory and unpack it -su pleroma -s $SHELL -lc " +sudo -u pleroma -s $SHELL -lc " curl 'https://git.pleroma.social/api/v4/projects/2/jobs/artifacts/stable/download?job=$FLAVOUR' -o /tmp/pleroma.zip unzip /tmp/pleroma.zip -d /tmp/ " # Move the release to the home directory and delete temporary files -su pleroma -s $SHELL -lc " +sudo -u pleroma -s $SHELL -lc " mv /tmp/release/* /opt/pleroma rmdir /tmp/release rm /tmp/pleroma.zip @@ -138,25 +138,25 @@ mkdir -p /etc/pleroma chown -R pleroma /etc/pleroma # Run the config generator -su pleroma -s $SHELL -lc "./bin/pleroma_ctl instance gen --output /etc/pleroma/config.exs --output-psql /tmp/setup_db.psql" +sudo -u pleroma -s $SHELL -lc "./bin/pleroma_ctl instance gen --output /etc/pleroma/config.exs --output-psql /tmp/setup_db.psql" # Create the postgres database -su postgres -s $SHELL -lc "psql -f /tmp/setup_db.psql" +sudo -u postgres -s $SHELL -lc "psql -f /tmp/setup_db.psql" # Create the database schema -su pleroma -s $SHELL -lc "./bin/pleroma_ctl migrate" +sudo -u pleroma -s $SHELL -lc "./bin/pleroma_ctl migrate" # If you have installed RUM indexes uncommend and run -# su pleroma -s $SHELL -lc "./bin/pleroma_ctl migrate --migrations-path priv/repo/optional_migrations/rum_indexing/" +# sudo -u pleroma -s $SHELL -lc "./bin/pleroma_ctl migrate --migrations-path priv/repo/optional_migrations/rum_indexing/" # Start the instance to verify that everything is working as expected -su pleroma -s $SHELL -lc "./bin/pleroma daemon" +sudo -u pleroma -s $SHELL -lc "./bin/pleroma daemon" # Wait for about 20 seconds and query the instance endpoint, if it shows your uri, name and email correctly, you are configured correctly sleep 20 && curl http://localhost:4000/api/v1/instance # Stop the instance -su pleroma -s $SHELL -lc "./bin/pleroma stop" +sudo -u pleroma -s $SHELL -lc "./bin/pleroma stop" ``` ### Setting up nginx and getting Let's Encrypt SSL certificaties From 7944271c1bdf4a4240b682249c068deb1b2acab0 Mon Sep 17 00:00:00 2001 From: Duponin Date: Sat, 15 Apr 2023 19:03:51 +0000 Subject: [PATCH 010/199] Unify install guides using sudo to use `sudo -Hu` --- docs/installation/migrating_from_source_otp_en.md | 8 ++++---- docs/installation/otp_en.md | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/installation/migrating_from_source_otp_en.md b/docs/installation/migrating_from_source_otp_en.md index d303a6daf..64d335331 100644 --- a/docs/installation/migrating_from_source_otp_en.md +++ b/docs/installation/migrating_from_source_otp_en.md @@ -85,26 +85,26 @@ export FLAVOUR="amd64-musl" # Clone the release build into a temporary directory and unpack it # Replace `stable` with `unstable` if you want to run the unstable branch -su pleroma -s $SHELL -lc " +sudo -Hu pleroma " curl 'https://git.pleroma.social/api/v4/projects/2/jobs/artifacts/stable/download?job=$FLAVOUR' -o /tmp/pleroma.zip unzip /tmp/pleroma.zip -d /tmp/ " # Move the release to the home directory and delete temporary files -su pleroma -s $SHELL -lc " +sudo -Hu pleroma " mv /tmp/release/* ~pleroma/ rmdir /tmp/release rm /tmp/pleroma.zip " # Start the instance to verify that everything is working as expected -su pleroma -s $SHELL -lc "./bin/pleroma daemon" +sudo -Hu pleroma "./bin/pleroma daemon" # Wait for about 20 seconds and query the instance endpoint, if it shows your uri, name and email correctly, you are configured correctly sleep 20 && curl http://localhost:4000/api/v1/instance # Stop the instance -su pleroma -s $SHELL -lc "./bin/pleroma stop" +sudo -Hu pleroma "./bin/pleroma stop" ``` ## Setting up a system service diff --git a/docs/installation/otp_en.md b/docs/installation/otp_en.md index e7dec9039..12078cb20 100644 --- a/docs/installation/otp_en.md +++ b/docs/installation/otp_en.md @@ -111,13 +111,13 @@ adduser --system --shell /bin/false --home /opt/pleroma pleroma export FLAVOUR="amd64-musl" # Clone the release build into a temporary directory and unpack it -sudo -u pleroma -s $SHELL -lc " +sudo -Hu pleroma " curl 'https://git.pleroma.social/api/v4/projects/2/jobs/artifacts/stable/download?job=$FLAVOUR' -o /tmp/pleroma.zip unzip /tmp/pleroma.zip -d /tmp/ " # Move the release to the home directory and delete temporary files -sudo -u pleroma -s $SHELL -lc " +sudo -Hu pleroma " mv /tmp/release/* /opt/pleroma rmdir /tmp/release rm /tmp/pleroma.zip @@ -138,25 +138,25 @@ mkdir -p /etc/pleroma chown -R pleroma /etc/pleroma # Run the config generator -sudo -u pleroma -s $SHELL -lc "./bin/pleroma_ctl instance gen --output /etc/pleroma/config.exs --output-psql /tmp/setup_db.psql" +sudo -Hu pleroma "./bin/pleroma_ctl instance gen --output /etc/pleroma/config.exs --output-psql /tmp/setup_db.psql" # Create the postgres database sudo -u postgres -s $SHELL -lc "psql -f /tmp/setup_db.psql" # Create the database schema -sudo -u pleroma -s $SHELL -lc "./bin/pleroma_ctl migrate" +sudo -Hu pleroma "./bin/pleroma_ctl migrate" # If you have installed RUM indexes uncommend and run -# sudo -u pleroma -s $SHELL -lc "./bin/pleroma_ctl migrate --migrations-path priv/repo/optional_migrations/rum_indexing/" +# sudo -Hu pleroma "./bin/pleroma_ctl migrate --migrations-path priv/repo/optional_migrations/rum_indexing/" # Start the instance to verify that everything is working as expected -sudo -u pleroma -s $SHELL -lc "./bin/pleroma daemon" +sudo -Hu pleroma "./bin/pleroma daemon" # Wait for about 20 seconds and query the instance endpoint, if it shows your uri, name and email correctly, you are configured correctly sleep 20 && curl http://localhost:4000/api/v1/instance # Stop the instance -sudo -u pleroma -s $SHELL -lc "./bin/pleroma stop" +sudo -Hu pleroma "./bin/pleroma stop" ``` ### Setting up nginx and getting Let's Encrypt SSL certificaties From d91a823836f416dc0bdf0fe8527695d372dcb347 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Sat, 22 Apr 2023 16:14:56 +0000 Subject: [PATCH 011/199] Remove unused indexes These indexes were always listed as unused and several grow quite large. The most significant impact is the activities_visibility_index which takes many hours to rebuild when restoring the server from backup even on fast hardware. --- .../20230422154018_drop_unused_indexes.exs | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 priv/repo/migrations/20230422154018_drop_unused_indexes.exs diff --git a/priv/repo/migrations/20230422154018_drop_unused_indexes.exs b/priv/repo/migrations/20230422154018_drop_unused_indexes.exs new file mode 100644 index 000000000..f2ba6b8df --- /dev/null +++ b/priv/repo/migrations/20230422154018_drop_unused_indexes.exs @@ -0,0 +1,88 @@ +defmodule Pleroma.Repo.Migrations.DropUnusedIndexes do + use Ecto.Migration + + @disable_ddl_transaction true + @disable_migration_lock true + + def up do + drop_if_exists( + index(:activities, ["(data->>'actor')", "inserted_at desc"], name: :activities_actor_index) + ) + + drop_if_exists(index(:activities, ["(data->'to')"], name: :activities_to_index)) + + drop_if_exists(index(:activities, ["(data->'cc')"], name: :activities_cc_index)) + + drop_if_exists(index(:activities, ["(split_part(actor, '/', 3))"], name: :activities_hosts)) + + drop_if_exists( + index(:activities, ["(data->'object'->>'inReplyTo')"], name: :activities_in_reply_to) + ) + + drop_if_exists( + index(:activities, ["((data #> '{\"object\",\"likes\"}'))"], name: :activities_likes) + ) + + drop_if_exists( + index(:activities, ["activity_visibility(actor, recipients, data)", "id DESC NULLS LAST"], + name: :activities_visibility_index, + where: "data->>'type' = 'Create'" + ) + ) + end + + def down do + create_if_not_exists( + index(:activities, ["(data->>'actor')", "inserted_at desc"], + name: :activities_actor_index, + concurrently: true + ) + ) + + create_if_not_exists( + index(:activities, ["(data->'to')"], + name: :activities_to_index, + using: :gin, + concurrently: true + ) + ) + + create_if_not_exists( + index(:activities, ["(data->'cc')"], + name: :activities_cc_index, + using: :gin, + concurrently: true + ) + ) + + create_if_not_exists( + index(:activities, ["(split_part(actor, '/', 3))"], + name: :activities_hosts, + concurrently: true + ) + ) + + create_if_not_exists( + index(:activities, ["(data->'object'->>'inReplyTo')"], + name: :activities_in_reply_to, + concurrently: true + ) + ) + + create_if_not_exists( + index(:activities, ["((data #> '{\"object\",\"likes\"}'))"], + name: :activities_likes, + using: :gin, + concurrently: true + ) + ) + + create_if_not_exists( + index(:activities, ["activity_visibility(actor, recipients, data)", "id DESC NULLS LAST"], + name: :activities_visibility_index, + where: "data->>'type' = 'Create'", + concurrently: true + ) + ) + end +end From 675639225a905f5b0b2650cd3f20a4758fc3f868 Mon Sep 17 00:00:00 2001 From: HJ <30-hj@users.noreply.git.pleroma.social> Date: Fri, 28 Apr 2023 11:13:42 +0000 Subject: [PATCH 012/199] allow https: so that flash works across instances without need for media proxy --- lib/pleroma/web/plugs/http_security_plug.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/plugs/http_security_plug.ex b/lib/pleroma/web/plugs/http_security_plug.ex index 34895c8d5..045384e08 100644 --- a/lib/pleroma/web/plugs/http_security_plug.ex +++ b/lib/pleroma/web/plugs/http_security_plug.ex @@ -104,7 +104,7 @@ defp csp_string do {[img_src, " https:"], [media_src, " https:"]} end - connect_src = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url] + connect_src = ["connect-src 'self' blob: https: ", static_url, ?\s, websocket_url] connect_src = if Config.get(:env) == :dev do From cd20d15bb8d2f97f8dd0850993041f15865cdda9 Mon Sep 17 00:00:00 2001 From: HJ <30-hj@users.noreply.git.pleroma.social> Date: Fri, 28 Apr 2023 11:19:14 +0000 Subject: [PATCH 013/199] changelog --- changelog.d/3879.fix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/3879.fix diff --git a/changelog.d/3879.fix b/changelog.d/3879.fix new file mode 100644 index 000000000..7c58cc3c2 --- /dev/null +++ b/changelog.d/3879.fix @@ -0,0 +1 @@ +fix not being able to fetch flash file from remote instance \ No newline at end of file From 4fd96b24aef831095ac8b3494fe4c59d8c9de333 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Fri, 5 May 2023 10:53:09 +0200 Subject: [PATCH 014/199] AddRemoveValidator: Use User.fetch_by_ap_id instead of upgrade_user_from_ap_id --- .../web/activity_pub/object_validators/add_remove_validator.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/object_validators/add_remove_validator.ex b/lib/pleroma/web/activity_pub/object_validators/add_remove_validator.ex index 5202db7f1..db3259550 100644 --- a/lib/pleroma/web/activity_pub/object_validators/add_remove_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/add_remove_validator.ex @@ -73,6 +73,7 @@ defp maybe_refetch_user(%User{featured_address: address} = user) when is_binary( end defp maybe_refetch_user(%User{ap_id: ap_id}) do - Pleroma.Web.ActivityPub.Transmogrifier.upgrade_user_from_ap_id(ap_id) + # Maybe it could use User.get_or_fetch_by_ap_id to avoid refreshing too often + User.fetch_by_ap_id(ap_id) end end From 0903c416456d137fa40bef231c31f038c8c20e9f Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Fri, 5 May 2023 10:54:16 +0200 Subject: [PATCH 015/199] User: Stop relying on ap_enabled --- lib/pleroma/user.ex | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index f6e30555c..2a0faed77 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1061,11 +1061,7 @@ def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do end def maybe_direct_follow(%User{} = follower, %User{} = followed) do - if not ap_enabled?(followed) do - follow(follower, followed) - else - {:ok, follower, followed} - end + {:ok, follower, followed} end @doc "A mass follow for local users. Respects blocks in both directions but does not create activities." From 606f78f5e5781a59cb9d083e8085144cca326cc6 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Fri, 5 May 2023 10:55:08 +0200 Subject: [PATCH 016/199] ActivityPub: Stop relying on ap_enabled and upgrade_user_from_ap_id --- lib/pleroma/web/activity_pub/activity_pub.ex | 28 +++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index f22756015..2fae98fad 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1751,24 +1751,20 @@ def pinned_fetch_task(%{pinned_objects: pins}) do def make_user_from_ap_id(ap_id, additional \\ []) do user = User.get_cached_by_ap_id(ap_id) - if user && !User.ap_enabled?(user) do - Transmogrifier.upgrade_user_from_ap_id(ap_id) - else - with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id, additional) do - {:ok, _pid} = Task.start(fn -> pinned_fetch_task(data) end) + with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id, additional) do + {:ok, _pid} = Task.start(fn -> pinned_fetch_task(data) end) - if user do - user - |> User.remote_user_changeset(data) - |> User.update_and_set_cache() - else - maybe_handle_clashing_nickname(data) + if user do + user + |> User.remote_user_changeset(data) + |> User.update_and_set_cache() + else + maybe_handle_clashing_nickname(data) - data - |> User.remote_user_changeset() - |> Repo.insert() - |> User.set_cache() - end + data + |> User.remote_user_changeset() + |> Repo.insert() + |> User.set_cache() end end end From 3962253cf16d19237b00b5e01573aed10fb596bd Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Fri, 5 May 2023 10:55:46 +0200 Subject: [PATCH 017/199] Publisher: Stop filtering via ap_enabled?/1 --- lib/pleroma/web/activity_pub/publisher.ex | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index 6c1ba76a3..af6aa0781 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -199,7 +199,6 @@ def publish(%User{} = actor, %{data: %{"bcc" => bcc}} = activity) inboxes = recipients - |> Enum.filter(&User.ap_enabled?/1) |> Enum.map(fn actor -> actor.inbox end) |> Enum.filter(fn inbox -> should_federate?(inbox, public) end) |> Instances.filter_reachable() @@ -241,7 +240,6 @@ def publish(%User{} = actor, %Activity{} = activity) do json = Jason.encode!(data) recipients(actor, activity) - |> Enum.filter(fn user -> User.ap_enabled?(user) end) |> Enum.map(fn %User{} = user -> determine_inbox(activity, user) end) From 2ee483ba41eb4f01c3ca763c30dcaa07d3b258f9 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Fri, 5 May 2023 11:00:40 +0200 Subject: [PATCH 018/199] Transmogrifier: Remove upgrade_user_from_ap_id --- .../web/activity_pub/transmogrifier.ex | 20 ------ .../web/activity_pub/transmogrifier_test.exs | 64 ------------------- 2 files changed, 84 deletions(-) diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 3141f8437..ea7a6a810 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -20,7 +20,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.Federator - alias Pleroma.Workers.TransmogrifierWorker import Ecto.Query @@ -968,25 +967,6 @@ def perform(:user_upgrade, user) do |> Repo.update_all([]) end - def upgrade_user_from_ap_id(ap_id) do - with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id), - {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id), - {:ok, user} <- update_user(user, data) do - {:ok, _pid} = Task.start(fn -> ActivityPub.pinned_fetch_task(user) end) - TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id}) - {:ok, user} - else - %User{} = user -> {:ok, user} - e -> e - end - end - - defp update_user(user, data) do - user - |> User.remote_user_changeset(data) - |> User.update_and_set_cache() - end - def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do Map.put(data, "url", url["href"]) end diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs index 6b4636d22..588f46296 100644 --- a/test/pleroma/web/activity_pub/transmogrifier_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs @@ -8,7 +8,6 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do alias Pleroma.Activity alias Pleroma.Object - alias Pleroma.Tests.ObanHelpers alias Pleroma.User alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Utils @@ -339,69 +338,6 @@ test "Updates of Notes are handled" do end end - describe "user upgrade" do - test "it upgrades a user to activitypub" do - user = - insert(:user, %{ - nickname: "rye@niu.moe", - local: false, - ap_id: "https://niu.moe/users/rye", - follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"}) - }) - - user_two = insert(:user) - Pleroma.FollowingRelationship.follow(user_two, user, :follow_accept) - - {:ok, activity} = CommonAPI.post(user, %{status: "test"}) - {:ok, unrelated_activity} = CommonAPI.post(user_two, %{status: "test"}) - assert "http://localhost:4001/users/rye@niu.moe/followers" in activity.recipients - - user = User.get_cached_by_id(user.id) - assert user.note_count == 1 - - {:ok, user} = Transmogrifier.upgrade_user_from_ap_id("https://niu.moe/users/rye") - ObanHelpers.perform_all() - - assert user.ap_enabled - assert user.note_count == 1 - assert user.follower_address == "https://niu.moe/users/rye/followers" - assert user.following_address == "https://niu.moe/users/rye/following" - - user = User.get_cached_by_id(user.id) - assert user.note_count == 1 - - activity = Activity.get_by_id(activity.id) - assert user.follower_address in activity.recipients - - assert %{ - "url" => [ - %{ - "href" => - "https://cdn.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg" - } - ] - } = user.avatar - - assert %{ - "url" => [ - %{ - "href" => - "https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png" - } - ] - } = user.banner - - refute "..." in activity.recipients - - unrelated_activity = Activity.get_by_id(unrelated_activity.id) - refute user.follower_address in unrelated_activity.recipients - - user_two = User.get_cached_by_id(user_two.id) - assert User.following?(user_two, user) - refute "..." in User.following(user_two) - end - end - describe "actor rewriting" do test "it fixes the actor URL property to be a proper URI" do data = %{ From e17265a7a248db7514b091d47ae79d1a2de5b440 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Fri, 5 May 2023 11:01:29 +0200 Subject: [PATCH 019/199] TransmogrifierWorker: Remove obsolete worker --- .../web/activity_pub/transmogrifier.ex | 22 ------------------- lib/pleroma/workers/transmogrifier_worker.ex | 18 --------------- 2 files changed, 40 deletions(-) delete mode 100644 lib/pleroma/workers/transmogrifier_worker.ex diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index ea7a6a810..0e6c429f9 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -945,28 +945,6 @@ defp strip_internal_tags(%{"tag" => tags} = object) do defp strip_internal_tags(object), do: object - def perform(:user_upgrade, user) do - # we pass a fake user so that the followers collection is stripped away - old_follower_address = User.ap_followers(%User{nickname: user.nickname}) - - from( - a in Activity, - where: ^old_follower_address in a.recipients, - update: [ - set: [ - recipients: - fragment( - "array_replace(?,?,?)", - a.recipients, - ^old_follower_address, - ^user.follower_address - ) - ] - ] - ) - |> Repo.update_all([]) - end - def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do Map.put(data, "url", url["href"]) end diff --git a/lib/pleroma/workers/transmogrifier_worker.ex b/lib/pleroma/workers/transmogrifier_worker.ex deleted file mode 100644 index 1f3f5385e..000000000 --- a/lib/pleroma/workers/transmogrifier_worker.ex +++ /dev/null @@ -1,18 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2022 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Workers.TransmogrifierWorker do - alias Pleroma.User - - use Pleroma.Workers.WorkerHelper, queue: "transmogrifier" - - @impl Oban.Worker - def perform(%Job{args: %{"op" => "user_upgrade", "user_id" => user_id}}) do - user = User.get_cached_by_id(user_id) - Pleroma.Web.ActivityPub.Transmogrifier.perform(:user_upgrade, user) - end - - @impl Oban.Worker - def timeout(_job), do: :timer.seconds(5) -end From 8181be89a28ec293cb36fccc2324bc78cc118d3a Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Fri, 5 May 2023 11:02:58 +0200 Subject: [PATCH 020/199] Federator: Stop using ap_enabled?/1 --- lib/pleroma/web/federator.ex | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/lib/pleroma/web/federator.ex b/lib/pleroma/web/federator.ex index 318b6cb11..84b77cda1 100644 --- a/lib/pleroma/web/federator.ex +++ b/lib/pleroma/web/federator.ex @@ -6,7 +6,6 @@ defmodule Pleroma.Web.Federator do alias Pleroma.Activity alias Pleroma.Object.Containment alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.Federator.Publisher @@ -80,7 +79,7 @@ def perform(:incoming_ap_doc, params) do # NOTE: we use the actor ID to do the containment, this is fine because an # actor shouldn't be acting on objects outside their own AP server. - with {_, {:ok, _user}} <- {:actor, ap_enabled_actor(actor)}, + with {_, {:ok, _user}} <- {:actor, User.get_or_fetch_by_ap_id(actor)}, nil <- Activity.normalize(params["id"]), {_, :ok} <- {:correct_origin?, Containment.contain_origin_from_id(actor, params)}, @@ -110,14 +109,4 @@ def perform(:incoming_ap_doc, params) do {:error, e} end end - - def ap_enabled_actor(id) do - user = User.get_cached_by_ap_id(id) - - if User.ap_enabled?(user) do - {:ok, user} - else - ActivityPub.make_user_from_ap_id(id) - end - end end From 9dfa1c4be0d9c37222a299c212607407f93d6596 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Fri, 5 May 2023 11:07:18 +0200 Subject: [PATCH 021/199] ActivityPub: Mark fetch_and_prepare_user_from_ap_id/1 as private --- lib/pleroma/web/activity_pub/activity_pub.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 2fae98fad..9175e9e13 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1668,7 +1668,7 @@ def user_data_from_user_object(data, additional \\ []) do end end - def fetch_and_prepare_user_from_ap_id(ap_id, additional \\ []) do + defp fetch_and_prepare_user_from_ap_id(ap_id, additional) do with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id), {:ok, data} <- user_data_from_user_object(data, additional) do {:ok, maybe_update_follow_information(data)} From 238edc30dec2cb48b3a6eca48397d5202060726e Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Fri, 5 May 2023 11:08:26 +0200 Subject: [PATCH 022/199] User: Remove ap_enabled?/1 --- lib/pleroma/user.ex | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 2a0faed77..eff82afd9 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -2147,10 +2147,6 @@ def get_public_key_for_ap_id(ap_id) do end end - def ap_enabled?(%User{local: true}), do: true - def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled - def ap_enabled?(_), do: false - @doc "Gets or fetch a user by uri or nickname." @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()} def get_or_fetch("http://" <> _host = uri), do: get_or_fetch_by_ap_id(uri) From fcd49e3985d34bd3b783791e12e638f66cab0a9b Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Fri, 5 May 2023 11:09:15 +0200 Subject: [PATCH 023/199] User: Remove ap_enabled field --- lib/pleroma/user.ex | 3 --- lib/pleroma/web/activity_pub/activity_pub.ex | 1 - .../20230504173400_remove_user_ap_enabled.exs | 13 +++++++++++++ test/pleroma/user_test.exs | 11 +++-------- .../activity_pub/activity_pub_controller_test.exs | 1 - .../web/activity_pub/activity_pub_test.exs | 1 - test/pleroma/web/activity_pub/publisher_test.exs | 15 +++++---------- test/pleroma/web/common_api_test.exs | 2 +- test/pleroma/web/federator_test.exs | 6 ++---- test/support/factory.ex | 1 - 10 files changed, 24 insertions(+), 30 deletions(-) create mode 100644 priv/repo/migrations/20230504173400_remove_user_ap_enabled.exs diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index eff82afd9..ce125d608 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -124,7 +124,6 @@ defmodule Pleroma.User do field(:domain_blocks, {:array, :string}, default: []) field(:is_active, :boolean, default: true) field(:no_rich_text, :boolean, default: false) - field(:ap_enabled, :boolean, default: false) field(:is_moderator, :boolean, default: false) field(:is_admin, :boolean, default: false) field(:show_role, :boolean, default: true) @@ -488,7 +487,6 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do :nickname, :public_key, :avatar, - :ap_enabled, :banner, :is_locked, :last_refreshed_at, @@ -1894,7 +1892,6 @@ def purge_user_changeset(user) do confirmation_token: nil, domain_blocks: [], is_active: false, - ap_enabled: false, is_moderator: false, is_admin: false, mascot: nil, diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 9175e9e13..1a10aef1d 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1547,7 +1547,6 @@ defp object_to_user_data(data, additional) do %{ ap_id: data["id"], uri: get_actor_url(data["url"]), - ap_enabled: true, banner: normalize_image(data["image"]), fields: fields, emoji: emojis, diff --git a/priv/repo/migrations/20230504173400_remove_user_ap_enabled.exs b/priv/repo/migrations/20230504173400_remove_user_ap_enabled.exs new file mode 100644 index 000000000..0aea41324 --- /dev/null +++ b/priv/repo/migrations/20230504173400_remove_user_ap_enabled.exs @@ -0,0 +1,13 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Repo.Migrations.RemoveUserApEnabled do + use Ecto.Migration + + def change do + alter table(:users) do + remove(:ap_enabled, :boolean, default: false, null: false) + end + end +end diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs index c16312a65..7f60b959a 100644 --- a/test/pleroma/user_test.exs +++ b/test/pleroma/user_test.exs @@ -1844,7 +1844,6 @@ test "delete/1 purges a user when they wouldn't be fully deleted" do confirmation_token: "qqqq", domain_blocks: ["lain.com"], is_active: false, - ap_enabled: true, is_moderator: true, is_admin: true, mascot: %{"a" => "b"}, @@ -1885,7 +1884,6 @@ test "delete/1 purges a user when they wouldn't be fully deleted" do confirmation_token: nil, domain_blocks: [], is_active: false, - ap_enabled: false, is_moderator: false, is_admin: false, mascot: nil, @@ -2473,8 +2471,7 @@ test "updates the counters normally on following/getting a follow when disabled" insert(:user, local: false, follower_address: "http://localhost:4001/users/masto_closed/followers", - following_address: "http://localhost:4001/users/masto_closed/following", - ap_enabled: true + following_address: "http://localhost:4001/users/masto_closed/following" ) assert other_user.following_count == 0 @@ -2495,8 +2492,7 @@ test "synchronizes the counters with the remote instance for the followed when e insert(:user, local: false, follower_address: "http://localhost:4001/users/masto_closed/followers", - following_address: "http://localhost:4001/users/masto_closed/following", - ap_enabled: true + following_address: "http://localhost:4001/users/masto_closed/following" ) assert other_user.following_count == 0 @@ -2517,8 +2513,7 @@ test "synchronizes the counters with the remote instance for the follower when e insert(:user, local: false, follower_address: "http://localhost:4001/users/masto_closed/followers", - following_address: "http://localhost:4001/users/masto_closed/following", - ap_enabled: true + following_address: "http://localhost:4001/users/masto_closed/following" ) assert other_user.following_count == 0 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 ef91066c1..62eb9b5a3 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -575,7 +575,6 @@ test "it inserts an incoming activity into the database" <> user = insert(:user, ap_id: "https://mastodon.example.org/users/raymoo", - ap_enabled: true, local: false, last_refreshed_at: nil ) diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs index a6f8b6152..54fc6ef0a 100644 --- a/test/pleroma/web/activity_pub/activity_pub_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_test.exs @@ -174,7 +174,6 @@ test "it returns a user" do {:ok, user} = ActivityPub.make_user_from_ap_id(user_id) assert user.ap_id == user_id assert user.nickname == "admin@mastodon.example.org" - assert user.ap_enabled assert user.follower_address == "http://mastodon.example.org/users/admin/followers" end diff --git a/test/pleroma/web/activity_pub/publisher_test.exs b/test/pleroma/web/activity_pub/publisher_test.exs index e2db3d575..c5137cbb7 100644 --- a/test/pleroma/web/activity_pub/publisher_test.exs +++ b/test/pleroma/web/activity_pub/publisher_test.exs @@ -276,8 +276,7 @@ test "publish to url with with different ports" do follower = insert(:user, %{ local: false, - inbox: "https://domain.com/users/nick1/inbox", - ap_enabled: true + inbox: "https://domain.com/users/nick1/inbox" }) actor = insert(:user, follower_address: follower.ap_id) @@ -313,8 +312,7 @@ test "publish to url with with different ports" do follower = insert(:user, %{ local: false, - inbox: "https://domain.com/users/nick1/inbox", - ap_enabled: true + inbox: "https://domain.com/users/nick1/inbox" }) actor = insert(:user, follower_address: follower.ap_id) @@ -348,8 +346,7 @@ test "publish to url with with different ports" do follower = insert(:user, %{ local: false, - inbox: "https://domain.com/users/nick1/inbox", - ap_enabled: true + inbox: "https://domain.com/users/nick1/inbox" }) actor = insert(:user, follower_address: follower.ap_id) @@ -382,15 +379,13 @@ test "publish to url with with different ports" do fetcher = insert(:user, local: false, - inbox: "https://domain.com/users/nick1/inbox", - ap_enabled: true + inbox: "https://domain.com/users/nick1/inbox" ) another_fetcher = insert(:user, local: false, - inbox: "https://domain2.com/users/nick1/inbox", - ap_enabled: true + inbox: "https://domain2.com/users/nick1/inbox" ) actor = insert(:user) diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index 5c9103e9f..5bd0df0b2 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -1328,7 +1328,7 @@ test "cancels a pending follow for a local user" do test "cancels a pending follow for a remote user" do follower = insert(:user) - followed = insert(:user, is_locked: true, local: false, ap_enabled: true) + followed = insert(:user, is_locked: true, local: false) assert {:ok, follower, followed, %{id: activity_id, data: %{"state" => "pending"}}} = CommonAPI.follow(follower, followed) diff --git a/test/pleroma/web/federator_test.exs b/test/pleroma/web/federator_test.exs index 1ffe6aae1..6826e6c2f 100644 --- a/test/pleroma/web/federator_test.exs +++ b/test/pleroma/web/federator_test.exs @@ -78,16 +78,14 @@ test "it federates only to reachable instances via AP" do local: false, nickname: "nick1@domain.com", ap_id: "https://domain.com/users/nick1", - inbox: inbox1, - ap_enabled: true + inbox: inbox1 }) insert(:user, %{ local: false, nickname: "nick2@domain2.com", ap_id: "https://domain2.com/users/nick2", - inbox: inbox2, - ap_enabled: true + inbox: inbox2 }) dt = NaiveDateTime.utc_now() diff --git a/test/support/factory.ex b/test/support/factory.ex index 09f02458c..d94544717 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -50,7 +50,6 @@ def user_factory(attrs \\ %{}) do last_refreshed_at: NaiveDateTime.utc_now(), notification_settings: %Pleroma.User.NotificationSetting{}, multi_factor_authentication_settings: %Pleroma.MFA.Settings{}, - ap_enabled: true, keys: pem } From c63bf6a040ffd982da43e980209a7d96e87e4d7f Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Fri, 5 May 2023 11:13:50 +0200 Subject: [PATCH 024/199] Add changelog for !3880 --- changelog.d/3880.remove | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/3880.remove diff --git a/changelog.d/3880.remove b/changelog.d/3880.remove new file mode 100644 index 000000000..113c76c85 --- /dev/null +++ b/changelog.d/3880.remove @@ -0,0 +1 @@ +Cleanup OStatus-era user upgrades and ap_enabled indicator \ No newline at end of file From c0d11da2d8edc57ef88163c06a19aad3e28d14db Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 7 May 2023 15:16:30 +0300 Subject: [PATCH 025/199] conditionally set csp depnding on media-proxy state --- lib/pleroma/web/plugs/http_security_plug.ex | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/plugs/http_security_plug.ex b/lib/pleroma/web/plugs/http_security_plug.ex index 045384e08..df46cfa0c 100644 --- a/lib/pleroma/web/plugs/http_security_plug.ex +++ b/lib/pleroma/web/plugs/http_security_plug.ex @@ -93,18 +93,26 @@ defp csp_string do img_src = "img-src 'self' data: blob:" media_src = "media-src 'self'" + connect_src = ["connect-src 'self' blob:", static_url, ?\s, websocket_url] # Strict multimedia CSP enforcement only when MediaProxy is enabled - {img_src, media_src} = + {img_src, media_src, connect_src} = if Config.get([:media_proxy, :enabled]) && !Config.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do sources = build_csp_multimedia_source_list() - {[img_src, sources], [media_src, sources]} + { + [img_src, sources], + [media_src, sources], + [connect_src, sources] + } else - {[img_src, " https:"], [media_src, " https:"]} + { + [img_src, " https:"], + [media_src, " https:"], + [connect_src, " https:"] + } end - connect_src = ["connect-src 'self' blob: https: ", static_url, ?\s, websocket_url] connect_src = if Config.get(:env) == :dev do From f8ef4924ecab5ba6851eee82845624bc15f868de Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 7 May 2023 15:24:09 +0300 Subject: [PATCH 026/199] fix whitespace --- lib/pleroma/web/plugs/http_security_plug.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/plugs/http_security_plug.ex b/lib/pleroma/web/plugs/http_security_plug.ex index df46cfa0c..a3166bc96 100644 --- a/lib/pleroma/web/plugs/http_security_plug.ex +++ b/lib/pleroma/web/plugs/http_security_plug.ex @@ -93,7 +93,7 @@ defp csp_string do img_src = "img-src 'self' data: blob:" media_src = "media-src 'self'" - connect_src = ["connect-src 'self' blob:", static_url, ?\s, websocket_url] + connect_src = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url] # Strict multimedia CSP enforcement only when MediaProxy is enabled {img_src, media_src, connect_src} = From f50fd9278fd36e6bd3ae36bb7f5033d9fd8a84ac Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 7 May 2023 15:29:19 +0300 Subject: [PATCH 027/199] reduce redundant reduntancy reduction --- lib/pleroma/web/plugs/http_security_plug.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/plugs/http_security_plug.ex b/lib/pleroma/web/plugs/http_security_plug.ex index a3166bc96..b189d5bfd 100644 --- a/lib/pleroma/web/plugs/http_security_plug.ex +++ b/lib/pleroma/web/plugs/http_security_plug.ex @@ -93,7 +93,7 @@ defp csp_string do img_src = "img-src 'self' data: blob:" media_src = "media-src 'self'" - connect_src = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url] + connect_src = "connect-src 'self' blob:" # Strict multimedia CSP enforcement only when MediaProxy is enabled {img_src, media_src, connect_src} = @@ -103,7 +103,7 @@ defp csp_string do { [img_src, sources], [media_src, sources], - [connect_src, sources] + [connect_src, sources, ?\s, websocket_url] } else { From 2a07411b0cb14ea26966659605d95074b02a8538 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 7 May 2023 15:34:17 +0300 Subject: [PATCH 028/199] keep the websocket url for all modes --- lib/pleroma/web/plugs/http_security_plug.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/plugs/http_security_plug.ex b/lib/pleroma/web/plugs/http_security_plug.ex index b189d5bfd..b3dc8a3a6 100644 --- a/lib/pleroma/web/plugs/http_security_plug.ex +++ b/lib/pleroma/web/plugs/http_security_plug.ex @@ -93,7 +93,7 @@ defp csp_string do img_src = "img-src 'self' data: blob:" media_src = "media-src 'self'" - connect_src = "connect-src 'self' blob:" + connect_src = ["connect-src 'self' blob: ", ?\s, websocket_url] # Strict multimedia CSP enforcement only when MediaProxy is enabled {img_src, media_src, connect_src} = @@ -103,7 +103,7 @@ defp csp_string do { [img_src, sources], [media_src, sources], - [connect_src, sources, ?\s, websocket_url] + [connect_src, sources] } else { From fb3335ffe287205274167d73a3124d2f811f2b6b Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Wed, 17 May 2023 17:12:21 +0200 Subject: [PATCH 029/199] EctoType: Add BareUri --- .../object_validators/bare_uri.ex | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 lib/pleroma/ecto_type/activity_pub/object_validators/bare_uri.ex diff --git a/lib/pleroma/ecto_type/activity_pub/object_validators/bare_uri.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/bare_uri.ex new file mode 100644 index 000000000..1038296e7 --- /dev/null +++ b/lib/pleroma/ecto_type/activity_pub/object_validators/bare_uri.ex @@ -0,0 +1,23 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.BareUri do + use Ecto.Type + + def type, do: :string + + def cast(uri) when is_binary(uri) do + case URI.parse(uri) do + %URI{scheme: nil} -> :error + %URI{} -> {:ok, uri} + _ -> :error + end + end + + def cast(_), do: :error + + def dump(data), do: {:ok, data} + + def load(data), do: {:ok, data} +end From a5066bb0789e15d808e99e8676c16ad74290419c Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Wed, 17 May 2023 17:13:26 +0200 Subject: [PATCH 030/199] CommonFields: Use BareUri for :url Closes: https://git.pleroma.social/pleroma/pleroma/-/issues/3121 --- changelog.d/3884.fix | 1 + .../object_validators/common_fields.ex | 2 +- .../object_validators/bare_uri_test.ex | 25 +++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 changelog.d/3884.fix create mode 100644 test/pleroma/ecto_type/activity_pub/object_validators/bare_uri_test.ex diff --git a/changelog.d/3884.fix b/changelog.d/3884.fix new file mode 100644 index 000000000..f8dbb2bbf --- /dev/null +++ b/changelog.d/3884.fix @@ -0,0 +1 @@ +Allow non-HTTP(s) URIs in "url" fields for compatibility with "FEP-fffd: Proxy Objects" \ No newline at end of file diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex index 7b60c139a..d580208df 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex @@ -58,7 +58,7 @@ defmacro status_object_fields do field(:like_count, :integer, default: 0) field(:announcement_count, :integer, default: 0) field(:inReplyTo, ObjectValidators.ObjectID) - field(:url, ObjectValidators.Uri) + field(:url, ObjectValidators.BareUri) field(:likes, {:array, ObjectValidators.ObjectID}, default: []) field(:announcements, {:array, ObjectValidators.ObjectID}, default: []) diff --git a/test/pleroma/ecto_type/activity_pub/object_validators/bare_uri_test.ex b/test/pleroma/ecto_type/activity_pub/object_validators/bare_uri_test.ex new file mode 100644 index 000000000..226383c3c --- /dev/null +++ b/test/pleroma/ecto_type/activity_pub/object_validators/bare_uri_test.ex @@ -0,0 +1,25 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.BareUriTest do + use Pleroma.DataCase, async: true + + alias Pleroma.EctoType.ActivityPub.ObjectValidators.BareUri + + test "diaspora://" do + text = "diaspora://alice@fediverse.example/post/deadbeefdeadbeefdeadbeefdeadbeef" + assert {:ok, text} = BareUri.cast(text) + end + + test "nostr:" do + text = "nostr:note1gwdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + assert {:ok, text} = BareUri.cast(text) + end + + test "errors for non-URIs" do + assert :error == SafeText.cast(1) + assert :error == SafeText.cast("foo") + assert :error == SafeText.cast("foo bar") + end +end From 2c66f584b53efe834e359b6829f5a73ad067dce2 Mon Sep 17 00:00:00 2001 From: tusooa Date: Sun, 21 May 2023 09:11:43 -0400 Subject: [PATCH 031/199] Show more informative errors when profile exceeds char limits --- .../update-credentials-limit-error.fix | 1 + .../controllers/account_controller.ex | 12 +++++ .../mastodon_api/update_credentials_test.exs | 48 ++++++++++++++++--- 3 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 changelog.d/update-credentials-limit-error.fix diff --git a/changelog.d/update-credentials-limit-error.fix b/changelog.d/update-credentials-limit-error.fix new file mode 100644 index 000000000..7682f958e --- /dev/null +++ b/changelog.d/update-credentials-limit-error.fix @@ -0,0 +1 @@ +Show more informative errors when profile exceeds char limits diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index c313a0e97..9a4b56301 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -263,6 +263,18 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p {:error, %Ecto.Changeset{errors: [background: {"file is too large", _}]}} -> render_error(conn, :request_entity_too_large, "File is too large") + {:error, %Ecto.Changeset{errors: [{:bio, {_, _}} | _]}} -> + render_error(conn, :request_entity_too_large, "Bio is too long") + + {:error, %Ecto.Changeset{errors: [{:name, {_, _}} | _]}} -> + render_error(conn, :request_entity_too_large, "Name is too long") + + {:error, %Ecto.Changeset{errors: [{:fields, {"invalid", _}} | _]}} -> + render_error(conn, :request_entity_too_large, "One or more field entries are too long") + + {:error, %Ecto.Changeset{errors: [{:fields, {_, _}} | _]}} -> + render_error(conn, :request_entity_too_large, "Too many field entries") + _e -> render_error(conn, :forbidden, "Invalid request") end diff --git a/test/pleroma/web/mastodon_api/update_credentials_test.exs b/test/pleroma/web/mastodon_api/update_credentials_test.exs index 6c63d53c2..45412bb34 100644 --- a/test/pleroma/web/mastodon_api/update_credentials_test.exs +++ b/test/pleroma/web/mastodon_api/update_credentials_test.exs @@ -97,6 +97,42 @@ test "updates the user's bio", %{conn: conn} do assert user.raw_bio == raw_bio end + test "updating bio honours bio limit", %{conn: conn} do + bio_limit = Config.get([:instance, :user_bio_length], 5000) + + raw_bio = String.duplicate(".", bio_limit + 1) + + conn = patch(conn, "/api/v1/accounts/update_credentials", %{"note" => raw_bio}) + + assert %{"error" => "Bio is too long"} = json_response_and_validate_schema(conn, 413) + end + + test "updating name honours name limit", %{conn: conn} do + name_limit = Config.get([:instance, :user_name_length], 100) + + name = String.duplicate(".", name_limit + 1) + + conn = patch(conn, "/api/v1/accounts/update_credentials", %{"display_name" => name}) + + assert %{"error" => "Name is too long"} = json_response_and_validate_schema(conn, 413) + end + + test "when both name and bio exceeds the limit, display name error", %{conn: conn} do + name_limit = Config.get([:instance, :user_name_length], 100) + bio_limit = Config.get([:instance, :user_bio_length], 5000) + + name = String.duplicate(".", name_limit + 1) + raw_bio = String.duplicate(".", bio_limit + 1) + + conn = + patch(conn, "/api/v1/accounts/update_credentials", %{ + "display_name" => name, + "note" => raw_bio + }) + + assert %{"error" => "Name is too long"} = json_response_and_validate_schema(conn, 413) + end + test "updates the user's locking status", %{conn: conn} do conn = patch(conn, "/api/v1/accounts/update_credentials", %{locked: "true"}) @@ -595,17 +631,17 @@ test "update fields when invalid request", %{conn: conn} do fields = [%{"name" => "foo", "value" => long_value}] - assert %{"error" => "Invalid request"} == + assert %{"error" => "One or more field entries are too long"} == conn |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) - |> json_response_and_validate_schema(403) + |> json_response_and_validate_schema(413) fields = [%{"name" => long_name, "value" => "bar"}] - assert %{"error" => "Invalid request"} == + assert %{"error" => "One or more field entries are too long"} == conn |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) - |> json_response_and_validate_schema(403) + |> json_response_and_validate_schema(413) clear_config([:instance, :max_account_fields], 1) @@ -614,10 +650,10 @@ test "update fields when invalid request", %{conn: conn} do %{"name" => "link", "value" => "cofe.io"} ] - assert %{"error" => "Invalid request"} == + assert %{"error" => "Too many field entries"} == conn |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) - |> json_response_and_validate_schema(403) + |> json_response_and_validate_schema(413) end end From 1fa196d8f7abfeccfcd911c74190866ad0950ca0 Mon Sep 17 00:00:00 2001 From: tusooa Date: Thu, 25 May 2023 18:40:38 -0400 Subject: [PATCH 032/199] Fix deleting banned users' statuses --- changelog.d/delete-status-of-banned-user.fix | 1 + lib/pleroma/web/common_api.ex | 2 +- test/pleroma/web/common_api_test.exs | 14 +++++++++++++ .../controllers/status_controller_test.exs | 21 +++++++++++++++++++ 4 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 changelog.d/delete-status-of-banned-user.fix diff --git a/changelog.d/delete-status-of-banned-user.fix b/changelog.d/delete-status-of-banned-user.fix new file mode 100644 index 000000000..1fa6a29d8 --- /dev/null +++ b/changelog.d/delete-status-of-banned-user.fix @@ -0,0 +1 @@ +Fix error 404 when deleting status of a banned user diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex index 89cc0d6fe..65d08de49 100644 --- a/lib/pleroma/web/common_api.ex +++ b/lib/pleroma/web/common_api.ex @@ -142,7 +142,7 @@ def reject_follow_request(follower, followed) do def delete(activity_id, user) do with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <- - {:find_activity, Activity.get_by_id(activity_id)}, + {:find_activity, Activity.get_by_id(activity_id, filter: [])}, {_, %Object{} = object, _} <- {:find_object, Object.normalize(activity, fetch: false), activity}, true <- User.privileged?(user, :messages_delete) || user.ap_id == object.data["actor"], diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index 968d826a2..9f4739a1f 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -393,6 +393,20 @@ test "privileged users deleting non-local posts won't federate the delete" do refute Activity.get_by_id(post.id) end + + test "it allows privileged users to delete banned user's posts" do + clear_config([:instance, :moderator_privileges], [:messages_delete]) + user = insert(:user) + moderator = insert(:user, is_moderator: true) + + {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"}) + User.set_activation(user, false) + + assert {:ok, delete} = CommonAPI.delete(post.id, moderator) + assert delete.local + + refute Activity.get_by_id(post.id) + end end test "favoriting race condition" do 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 1e8979127..4f434cb69 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -1018,6 +1018,27 @@ test "when you're privileged to", %{conn: conn} do refute Activity.get_by_id(activity.id) end + + test "when you're privileged and the user is banned", %{conn: conn} do + clear_config([:instance, :moderator_privileges], [:messages_delete]) + posting_user = insert(:user, is_active: false) + refute posting_user.is_active + activity = insert(:note_activity, user: posting_user) + user = insert(:user, is_moderator: true) + + res_conn = + conn + |> assign(:user, user) + |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:statuses"])) + |> delete("/api/v1/statuses/#{activity.id}") + + assert %{} = json_response_and_validate_schema(res_conn, 200) + + assert ModerationLog |> Repo.one() |> ModerationLog.get_log_entry_message() == + "@#{user.nickname} deleted status ##{activity.id}" + + refute Activity.get_by_id(activity.id) + end end describe "reblogging" do From 279fd47b486ccfda4537d7a64d553ac261a6fdd8 Mon Sep 17 00:00:00 2001 From: Zero Date: Thu, 25 May 2023 12:36:05 -0400 Subject: [PATCH 033/199] ForceMentionsInContent: fix double mentions for Mastodon/Misskey posts The code checked for duplicates using "ap_id", but in Mastodon and Misskey the look like that: Mastodon: https://mastodon.example.com/users/roger Misskey: https:///misskey.example.com/users/104ab42f11 The fix is to also check for "uri", which is what will be in the "explicitly_mentioned_uris" list: Mastodon: https://mastodon.example.com/@roger Misskey: https://misskey.example.com/@roger --- changelog.d/3888.fix | 1 + .../mrf/force_mentions_in_content.ex | 6 +-- .../mrf/force_mentions_in_content_test.exs | 51 +++++++++++++++++++ 3 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 changelog.d/3888.fix diff --git a/changelog.d/3888.fix b/changelog.d/3888.fix new file mode 100644 index 000000000..886aa7b39 --- /dev/null +++ b/changelog.d/3888.fix @@ -0,0 +1 @@ +ForceMentionsInContent: fix double mentions for Mastodon/Misskey posts \ No newline at end of file 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 70224561c..b9b175cc3 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 @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2022 Pleroma Authors +# Copyright © 2017-2023 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent do @@ -98,8 +98,8 @@ def filter( explicitly_mentioned_uris = extract_mention_uris_from_content(content) added_mentions = - Enum.reduce(mention_users, "", fn %User{ap_id: uri} = user, acc -> - unless uri in explicitly_mentioned_uris do + Enum.reduce(mention_users, "", fn %User{ap_id: api_id, uri: uri} = user, acc -> + unless Enum.any?([api_id, uri], fn u -> u in explicitly_mentioned_uris end) do acc <> Formatter.mention_from_user(user, %{mentions_format: :compact}) <> " " else acc diff --git a/test/pleroma/web/activity_pub/mrf/force_mentions_in_content_test.exs b/test/pleroma/web/activity_pub/mrf/force_mentions_in_content_test.exs index b349a4bb7..5ebd972a6 100644 --- a/test/pleroma/web/activity_pub/mrf/force_mentions_in_content_test.exs +++ b/test/pleroma/web/activity_pub/mrf/force_mentions_in_content_test.exs @@ -256,4 +256,55 @@ test "works with Updates" do } }} = MRF.filter_one(ForceMentionsInContent, activity) end + + test "don't add duplicate mentions for mastodon or misskey posts" do + [zero, rogerick, greg] = [ + insert(:user, + ap_id: "https://pleroma.example.com/users/zero", + uri: "https://pleroma.example.com/users/zero", + nickname: "zero@pleroma.example.com", + local: false + ), + insert(:user, + ap_id: "https://misskey.example.com/users/104ab42f11", + uri: "https://misskey.example.com/@rogerick", + nickname: "rogerick@misskey.example.com", + local: false + ), + insert(:user, + ap_id: "https://mastodon.example.com/users/greg", + uri: "https://mastodon.example.com/@greg", + nickname: "greg@mastodon.example.com", + local: false + ) + ] + + {:ok, post} = CommonAPI.post(rogerick, %{status: "eugh"}) + + inline_mentions = [ + "@rogerick", + "@greg" + ] + + activity = %{ + "type" => "Create", + "actor" => zero.ap_id, + "object" => %{ + "type" => "Note", + "actor" => zero.ap_id, + "content" => "#{Enum.at(inline_mentions, 0)} #{Enum.at(inline_mentions, 1)} erm", + "to" => [ + rogerick.ap_id, + greg.ap_id, + Constants.as_public() + ], + "inReplyTo" => Object.normalize(post).data["id"] + } + } + + {:ok, %{"object" => %{"content" => filtered}}} = ForceMentionsInContent.filter(activity) + + assert filtered == + "#{Enum.at(inline_mentions, 0)} #{Enum.at(inline_mentions, 1)} erm" + end end From f970091c6a58d06a42594e2c4a0baa5a86617652 Mon Sep 17 00:00:00 2001 From: tusooa Date: Fri, 26 May 2023 17:17:13 -0400 Subject: [PATCH 034/199] Add instructions to serve media on another domain --- docs/configuration/hardening.md | 14 ++++ installation/pleroma-mediaproxy.nginx | 97 +++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 installation/pleroma-mediaproxy.nginx diff --git a/docs/configuration/hardening.md b/docs/configuration/hardening.md index d3bfc4e4a..4f40873e6 100644 --- a/docs/configuration/hardening.md +++ b/docs/configuration/hardening.md @@ -62,6 +62,20 @@ An additional “Expect-CT” header will be sent with the configured `ct_max_ag If you click on a link, your browser’s request to the other site will include from where it is coming from. The “Referrer policy” header tells the browser how and if it should send this information. (see [Referrer policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy)) +### Uploaded media and media proxy + +It is STRONGLY RECOMMENDED to serve both the locally-uploaded media and the media proxy from another domain than the domain that Pleroma runs on, if applicable. + +```elixir +config :pleroma, :media_proxy, + base_url: "https://some.other.domain" + +config :pleroma, Pleroma.Upload, + base_url: "https://some.other.domain" +``` + +See `installation/pleroma-mediaproxy.nginx` for examples on how to configure your media proxy. + ## systemd A systemd unit example is provided at `installation/pleroma.service`. diff --git a/installation/pleroma-mediaproxy.nginx b/installation/pleroma-mediaproxy.nginx new file mode 100644 index 000000000..6f2edf212 --- /dev/null +++ b/installation/pleroma-mediaproxy.nginx @@ -0,0 +1,97 @@ +# This file is for those who want to serve uploaded media and media proxy over +# another domain. This is STRONGLY RECOMMENDED. +# This is meant to be used ALONG WITH `pleroma.nginx`. + +# If this is a new instance, replace the `location ~ ^/(media|proxy)` section in +# `pleroma.nginx` with the following to completely disable access to media from the main domain: +# location ~ ^/(media|proxy) { +# return 404; +# } +# +# If you are configuring an existing instance to use another domain +# for media, you will want to keep redirecting all existing local media to the new domain +# so already-uploaded media will not break. +# Replace the `location ~ ^/(media|proxy)` section in `pleroma.nginx` with the following: +# +# location /media { +# return 301 https://some.other.domain$request_uri; +# } +# +# location /proxy { +# return 404; +# } + +server { + server_name some.other.domain; + + listen 80; + listen [::]:80; + + # Uncomment this if you need to use the 'webroot' method with certbot. Make sure + # that the directory exists and that it is accessible by the webserver. If you followed + # the guide, you already ran 'mkdir -p /var/lib/letsencrypt' to create the folder. + # You may need to load this file with the ssl server block commented out, run certbot + # to get the certificate, and then uncomment it. + # + # location ~ /\.well-known/acme-challenge { + # root /var/lib/letsencrypt/; + # } + location / { + return 301 https://$server_name$request_uri; + } +} + +server { + server_name some.other.domain; + + listen 443 ssl http2; + listen [::]:443 ssl http2; + ssl_session_timeout 1d; + ssl_session_cache shared:MozSSL:10m; # about 40000 sessions + ssl_session_tickets off; + + ssl_trusted_certificate /etc/letsencrypt/live/some.other.domain/chain.pem; + ssl_certificate /etc/letsencrypt/live/some.other.domain/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/some.other.domain/privkey.pem; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4"; + ssl_prefer_server_ciphers off; + # In case of an old server with an OpenSSL version of 1.0.2 or below, + # leave only prime256v1 or comment out the following line. + ssl_ecdh_curve X25519:prime256v1:secp384r1:secp521r1; + ssl_stapling on; + ssl_stapling_verify on; + + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_buffers 16 8k; + gzip_http_version 1.1; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript application/activity+json application/atom+xml; + + # the nginx default is 1m, not enough for large media uploads + client_max_body_size 16m; + ignore_invalid_headers off; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + location / { return 404; } + + location ~ ^/(media|proxy) { + proxy_cache pleroma_media_cache; + slice 1m; + proxy_cache_key $host$uri$is_args$args$slice_range; + proxy_set_header Range $slice_range; + proxy_cache_valid 200 206 301 304 1h; + proxy_cache_lock on; + proxy_ignore_client_abort on; + proxy_buffering on; + chunked_transfer_encoding on; + proxy_pass http://phoenix; + } +} From 85902ad1ae5cfcc5d4c92cdb364a83d1edbbad3d Mon Sep 17 00:00:00 2001 From: tusooa Date: Fri, 26 May 2023 17:27:35 -0400 Subject: [PATCH 035/199] Recommend users to serve media on another domain in guide --- docs/installation/otp_en.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/installation/otp_en.md b/docs/installation/otp_en.md index f2812346b..ed52e57eb 100644 --- a/docs/installation/otp_en.md +++ b/docs/installation/otp_en.md @@ -198,6 +198,10 @@ $EDITOR path-to-nginx-config # Verify that the config is valid nginx -t ``` +#### (Strongly recommended) serve media on another domain + +Refer to the [Hardening your instance](../configuration/hardening.md) document on how to serve media on another domain. We STRONGLY RECOMMEND you to do this to minimize attack vectors. + #### Start nginx === "Alpine" From 408ea697aa685bb860deddfc63cd755a3b64c39a Mon Sep 17 00:00:00 2001 From: tusooa Date: Fri, 26 May 2023 17:28:41 -0400 Subject: [PATCH 036/199] Add changelog --- changelog.d/media-altdomain.skip | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 changelog.d/media-altdomain.skip diff --git a/changelog.d/media-altdomain.skip b/changelog.d/media-altdomain.skip new file mode 100644 index 000000000..e69de29bb From e92eb5f4823fbb6c01218ec72ad8d41fb12e20a1 Mon Sep 17 00:00:00 2001 From: tusooa Date: Sat, 27 May 2023 00:57:22 +0000 Subject: [PATCH 037/199] Add instructions to other distro's guides --- docs/installation/alpine_linux_en.md | 3 +++ docs/installation/arch_linux_en.md | 5 +++++ docs/installation/debian_based_en.md | 5 +++++ docs/installation/freebsd_en.md | 4 ++++ docs/installation/gentoo_en.md | 4 ++++ docs/installation/netbsd_en.md | 4 ++++ docs/installation/openbsd_en.md | 4 ++++ 7 files changed, 29 insertions(+) diff --git a/docs/installation/alpine_linux_en.md b/docs/installation/alpine_linux_en.md index c37ff0c63..7154bca48 100644 --- a/docs/installation/alpine_linux_en.md +++ b/docs/installation/alpine_linux_en.md @@ -183,6 +183,9 @@ server { ... } ``` +* (Strongly recommended) serve media on another domain + +Refer to the [Hardening your instance](../configuration/hardening.md) document on how to serve media on another domain. We STRONGLY RECOMMEND you to do this to minimize attack vectors. * Enable and start nginx: diff --git a/docs/installation/arch_linux_en.md b/docs/installation/arch_linux_en.md index 285743d56..f7d722ef9 100644 --- a/docs/installation/arch_linux_en.md +++ b/docs/installation/arch_linux_en.md @@ -173,6 +173,11 @@ sudo ln -s /etc/nginx/sites-available/pleroma.nginx /etc/nginx/sites-enabled/ple ``` * Before starting nginx edit the configuration and change it to your needs (e.g. change servername, change cert paths) + +* (Strongly recommended) serve media on another domain + +Refer to the [Hardening your instance](../configuration/hardening.md) document on how to serve media on another domain. We STRONGLY RECOMMEND you to do this to minimize attack vectors. + * Enable and start nginx: ```shell diff --git a/docs/installation/debian_based_en.md b/docs/installation/debian_based_en.md index 4e52b2155..fe85a05bf 100644 --- a/docs/installation/debian_based_en.md +++ b/docs/installation/debian_based_en.md @@ -136,6 +136,11 @@ sudo ln -s /etc/nginx/sites-available/pleroma.nginx /etc/nginx/sites-enabled/ple ``` * Before starting nginx edit the configuration and change it to your needs (e.g. change servername, change cert paths) + +* (Strongly recommended) serve media on another domain + +Refer to the [Hardening your instance](../configuration/hardening.md) document on how to serve media on another domain. We STRONGLY RECOMMEND you to do this to minimize attack vectors. + * Enable and start nginx: ```shell diff --git a/docs/installation/freebsd_en.md b/docs/installation/freebsd_en.md index 9cbe0f203..50ed30d74 100644 --- a/docs/installation/freebsd_en.md +++ b/docs/installation/freebsd_en.md @@ -173,6 +173,10 @@ Edit the defaults of `/usr/local/etc/nginx/sites-available/pleroma.nginx`: * Change `ssl_certificate_key` to `/var/db/acme/certs/example.tld/example.tld.key`. * Change all references of `example.tld` to your instance's domain name. +#### (Strongly recommended) serve media on another domain + +Refer to the [Hardening your instance](../configuration/hardening.md) document on how to serve media on another domain. We STRONGLY RECOMMEND you to do this to minimize attack vectors. + ## Creating a startup script for Pleroma Pleroma will need to compile when it initially starts, which typically takes a longer diff --git a/docs/installation/gentoo_en.md b/docs/installation/gentoo_en.md index 36882c8c8..693f83825 100644 --- a/docs/installation/gentoo_en.md +++ b/docs/installation/gentoo_en.md @@ -227,6 +227,10 @@ Replace all instances of `example.tld` with your instance's public URL. If for w Pay special attention to the line that begins with `ssl_ecdh_curve`. It is stongly advised to comment that line out so that OpenSSL will use its full capabilities, and it is also possible you are running OpenSSL 1.0.2 necessitating that you do this. +* (Strongly recommended) serve media on another domain + +Refer to the [Hardening your instance](../configuration/hardening.md) document on how to serve media on another domain. We STRONGLY RECOMMEND you to do this to minimize attack vectors. + * Enable and start nginx: ```shell diff --git a/docs/installation/netbsd_en.md b/docs/installation/netbsd_en.md index 41b3b0072..2ade7df98 100644 --- a/docs/installation/netbsd_en.md +++ b/docs/installation/netbsd_en.md @@ -123,6 +123,10 @@ Edit the defaults: * Change `ssl_certificate_key` to `/etc/nginx/tls/key`. * Change `example.tld` to your instance's domain name. +### (Strongly recommended) serve media on another domain + +Refer to the [Hardening your instance](../configuration/hardening.md) document on how to serve media on another domain. We STRONGLY RECOMMEND you to do this to minimize attack vectors. + ## Configuring acme.sh We'll be using acme.sh in Stateless Mode for TLS certificate renewal. diff --git a/docs/installation/openbsd_en.md b/docs/installation/openbsd_en.md index c80c8f678..9e7e040f5 100644 --- a/docs/installation/openbsd_en.md +++ b/docs/installation/openbsd_en.md @@ -195,6 +195,10 @@ rcctl enable relayd rcctl start relayd ``` +##### (Strongly recommended) serve media on another domain + +Refer to the [Hardening your instance](../configuration/hardening.md) document on how to serve media on another domain. We STRONGLY RECOMMEND you to do this to minimize attack vectors. + #### pf Enabling and configuring pf is highly recommended. In /etc/pf.conf, insert the following configuration: From b6b7de20100e980849404a9e7384617a15df1e43 Mon Sep 17 00:00:00 2001 From: faried nawaz Date: Thu, 18 May 2023 02:12:20 +0500 Subject: [PATCH 038/199] add url to Metadata.build_tags call If static_fe is enabled, going to https://pleroma/notice/some-id results in With this fix, it is Additionally, Pleroma.Web.Metadata.Providers.OpenGraph now generates meta tags for attachments in the post. --- lib/pleroma/web/static_fe/static_fe_controller.ex | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/static_fe/static_fe_controller.ex b/lib/pleroma/web/static_fe/static_fe_controller.ex index 97c41c6f9..8019a218a 100644 --- a/lib/pleroma/web/static_fe/static_fe_controller.ex +++ b/lib/pleroma/web/static_fe/static_fe_controller.ex @@ -25,7 +25,15 @@ def show(%{assigns: %{notice_id: notice_id}} = conn, _params) do true <- Visibility.is_public?(activity.object), {_, true} <- {:visible?, Visibility.visible_for_user?(activity, _reading_user = nil)}, %User{} = user <- User.get_by_ap_id(activity.object.data["actor"]) do - meta = Metadata.build_tags(%{activity_id: notice_id, object: activity.object, user: user}) + url = Helpers.url(conn) <> conn.request_path + + meta = + Metadata.build_tags(%{ + activity_id: notice_id, + object: activity.object, + user: user, + url: url + }) timeline = activity.object.data["context"] From 52368e670222c2add5f5677d864dd64c9376c553 Mon Sep 17 00:00:00 2001 From: faried nawaz Date: Thu, 18 May 2023 02:17:29 +0500 Subject: [PATCH 039/199] fix meta tag for twitter cards and image attachments The name of the tag should be twitter:image, not twitter:player. Also, add twitter:image:alt meta tags. --- lib/pleroma/web/metadata/providers/twitter_card.ex | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/metadata/providers/twitter_card.ex b/lib/pleroma/web/metadata/providers/twitter_card.ex index 2dac22ee2..fbd4fd5a7 100644 --- a/lib/pleroma/web/metadata/providers/twitter_card.ex +++ b/lib/pleroma/web/metadata/providers/twitter_card.ex @@ -76,9 +76,11 @@ defp build_attachments(id, %{data: %{"attachment" => attachments}}) do {:meta, [name: "twitter:card", content: "summary_large_image"], []}, {:meta, [ - name: "twitter:player", + name: "twitter:image", content: MediaProxy.url(url["href"]) - ], []} + ], []}, + {:meta, [property: "twitter:image:alt", content: truncate(attachment["name"])], + []} | acc ] |> maybe_add_dimensions(url) @@ -130,4 +132,10 @@ defp maybe_add_dimensions(metadata, url) do metadata end end + + defp truncate(text) do + # truncate to 420 characters + # see https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/markup + Pleroma.Formatter.truncate(text, 420) + end end From a1af122499d3558d9fa6d5606c394e01f36e95e4 Mon Sep 17 00:00:00 2001 From: faried nawaz Date: Thu, 18 May 2023 02:28:50 +0500 Subject: [PATCH 040/199] changelog entry --- changelog.d/3885.fix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/3885.fix diff --git a/changelog.d/3885.fix b/changelog.d/3885.fix new file mode 100644 index 000000000..c5fbb0ed4 --- /dev/null +++ b/changelog.d/3885.fix @@ -0,0 +1 @@ +Fix opengraph and twitter card meta tags From 8b390d27dce0c1827ed4c8c959612b6126624c9a Mon Sep 17 00:00:00 2001 From: faried nawaz Date: Thu, 18 May 2023 03:20:44 +0500 Subject: [PATCH 041/199] twitter card: handle case where image has no alt text --- lib/pleroma/web/metadata/providers/twitter_card.ex | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/metadata/providers/twitter_card.ex b/lib/pleroma/web/metadata/providers/twitter_card.ex index fbd4fd5a7..426022c65 100644 --- a/lib/pleroma/web/metadata/providers/twitter_card.ex +++ b/lib/pleroma/web/metadata/providers/twitter_card.ex @@ -79,8 +79,7 @@ defp build_attachments(id, %{data: %{"attachment" => attachments}}) do name: "twitter:image", content: MediaProxy.url(url["href"]) ], []}, - {:meta, [property: "twitter:image:alt", content: truncate(attachment["name"])], - []} + {:meta, [name: "twitter:image:alt", content: truncate(attachment["name"])], []} | acc ] |> maybe_add_dimensions(url) @@ -133,6 +132,8 @@ defp maybe_add_dimensions(metadata, url) do end end + defp truncate(nil), do: "" + defp truncate(text) do # truncate to 420 characters # see https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/markup From 4c91c0d1baa5bdfdcbfc6ff54859be7e9db91a37 Mon Sep 17 00:00:00 2001 From: faried nawaz Date: Thu, 18 May 2023 03:21:14 +0500 Subject: [PATCH 042/199] oops, forgot the test cases --- test/pleroma/web/metadata/providers/twitter_card_test.exs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/pleroma/web/metadata/providers/twitter_card_test.exs b/test/pleroma/web/metadata/providers/twitter_card_test.exs index be4cfbe7b..f8d01c5c8 100644 --- a/test/pleroma/web/metadata/providers/twitter_card_test.exs +++ b/test/pleroma/web/metadata/providers/twitter_card_test.exs @@ -182,7 +182,8 @@ test "it renders supported types of attachments and skips unknown types" do {:meta, [name: "twitter:title", content: Utils.user_name_string(user)], []}, {:meta, [name: "twitter:description", content: "pleroma in a nutshell"], []}, {:meta, [name: "twitter:card", content: "summary_large_image"], []}, - {:meta, [name: "twitter:player", content: "https://pleroma.gov/tenshi.png"], []}, + {:meta, [name: "twitter:image", content: "https://pleroma.gov/tenshi.png"], []}, + {:meta, [name: "twitter:image:alt", content: ""], []}, {:meta, [name: "twitter:player:width", content: "1280"], []}, {:meta, [name: "twitter:player:height", content: "1024"], []}, {:meta, [name: "twitter:card", content: "player"], []}, From 8336519f300e2e12087126564a155c292915daed Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Wed, 31 May 2023 08:26:21 +0200 Subject: [PATCH 043/199] installation/debian_based_en: Elixir 1.11 means Debian 12+ and Ubuntu 22.04+ --- docs/installation/debian_based_en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/debian_based_en.md b/docs/installation/debian_based_en.md index 4e52b2155..575b8a7ab 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 11 (“bullseye”) or later. This guide should also work with Ubuntu 18.04 (“Bionic Beaver”) 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 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. {! backend/installation/generic_dependencies.include !} From 737e45c1022172b316671110468a03a22a23e809 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Wed, 31 May 2023 08:28:28 +0200 Subject: [PATCH 044/199] installation/debian_based_jp: Elixir 1.11 means Debian 12+ and Ubuntu 22.04+ I checked for what each part of the debian&ubuntu sentences meant with the help of Jisho.org, should be safe but it did make me notice that this guide hasn't been updated in years --- changelog.d/distro-docs-elixir-1.11.skip | 0 docs/installation/debian_based_jp.md | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 changelog.d/distro-docs-elixir-1.11.skip diff --git a/changelog.d/distro-docs-elixir-1.11.skip b/changelog.d/distro-docs-elixir-1.11.skip new file mode 100644 index 000000000..e69de29bb diff --git a/docs/installation/debian_based_jp.md b/docs/installation/debian_based_jp.md index 3736e857f..695e7bb7d 100644 --- a/docs/installation/debian_based_jp.md +++ b/docs/installation/debian_based_jp.md @@ -5,7 +5,7 @@ ## インストール -このガイドはDebian Stretchを利用することを想定しています。Ubuntu 16.04や18.04でもおそらく動作します。また、ユーザはrootもしくはsudoにより管理者権限を持っていることを前提とします。もし、以下の操作をrootユーザで行う場合は、 `sudo` を無視してください。ただし、`sudo -Hu pleroma` のようにユーザを指定している場合には `su -s $SHELL -c 'command'` を代わりに使ってください。 +このガイドはDebian Bookwormを利用することを想定しています。Ubuntu 22.04でもおそらく動作します。また、ユーザはrootもしくはsudoにより管理者権限を持っていることを前提とします。もし、以下の操作をrootユーザで行う場合は、 `sudo` を無視してください。ただし、`sudo -Hu pleroma` のようにユーザを指定している場合には `su -s $SHELL -c 'command'` を代わりに使ってください。 ### 必要なソフトウェア From a2bbd7c9dadad42c995e7627a559673f04c2618b Mon Sep 17 00:00:00 2001 From: Sean King Date: Wed, 31 May 2023 12:22:13 -0600 Subject: [PATCH 045/199] Fix base media and proxy URL in instructions to serve media on another domain --- docs/configuration/hardening.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration/hardening.md b/docs/configuration/hardening.md index 4f40873e6..493ba608c 100644 --- a/docs/configuration/hardening.md +++ b/docs/configuration/hardening.md @@ -68,10 +68,10 @@ It is STRONGLY RECOMMENDED to serve both the locally-uploaded media and the medi ```elixir config :pleroma, :media_proxy, - base_url: "https://some.other.domain" + base_url: "https://some.other.domain/proxy" config :pleroma, Pleroma.Upload, - base_url: "https://some.other.domain" + base_url: "https://some.other.domain/media" ``` See `installation/pleroma-mediaproxy.nginx` for examples on how to configure your media proxy. From c9cb90ff4fac5bf645720c7e7ef0bf4b6acf8afa Mon Sep 17 00:00:00 2001 From: Sean King Date: Wed, 31 May 2023 17:49:06 -0600 Subject: [PATCH 046/199] Media proxy base URL doesn't need /proxy --- docs/configuration/hardening.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/hardening.md b/docs/configuration/hardening.md index 493ba608c..cc46d1ff9 100644 --- a/docs/configuration/hardening.md +++ b/docs/configuration/hardening.md @@ -68,7 +68,7 @@ It is STRONGLY RECOMMENDED to serve both the locally-uploaded media and the medi ```elixir config :pleroma, :media_proxy, - base_url: "https://some.other.domain/proxy" + base_url: "https://some.other.domain" config :pleroma, Pleroma.Upload, base_url: "https://some.other.domain/media" From cbc5b8cebd9255e0c49e8fb02daed4680be1d336 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Fri, 2 Jun 2023 17:03:21 +0400 Subject: [PATCH 047/199] B Preload: Make sure that the preloaded json is html safe --- lib/pleroma/web/preload.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/preload.ex b/lib/pleroma/web/preload.ex index 4485383f9..6a4a8885e 100644 --- a/lib/pleroma/web/preload.ex +++ b/lib/pleroma/web/preload.ex @@ -11,7 +11,7 @@ def build_tags(_conn, params) do terms = params |> parser.generate_terms() - |> Enum.map(fn {k, v} -> {k, Base.encode64(Jason.encode!(v))} end) + |> Enum.map(fn {k, v} -> {k, Base.encode64(Jason.encode!(v, escape: :html_safe))} end) |> Enum.into(%{}) Map.merge(acc, terms) @@ -19,7 +19,7 @@ def build_tags(_conn, params) do rendered_html = preload_data - |> Jason.encode!() + |> Jason.encode!(escape: :html_safe) |> build_script_tag() |> HTML.safe_to_string() From 40d40d67a3cee4d57f9200d0980df1b21d08a834 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Fri, 2 Jun 2023 17:09:23 +0400 Subject: [PATCH 048/199] Add changelog. --- changelog.d/3901.security | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/3901.security diff --git a/changelog.d/3901.security b/changelog.d/3901.security new file mode 100644 index 000000000..a3d8bd01f --- /dev/null +++ b/changelog.d/3901.security @@ -0,0 +1 @@ +Preload: Make generated JSON html-safe. It already was html safe because it only consists of config data that is base64 encoded, but this will keep it safe it that ever changes. From fadcd7f1a9e41ec9b54f251cf782688bf3d36889 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 7 Jun 2023 09:19:22 -0400 Subject: [PATCH 049/199] Revert MediaProxy Host header validation Something is going wrong here even though the tests are correct. --- changelog.d/3896.add | 2 +- .../web/media_proxy/media_proxy_controller.ex | 25 ---------------- .../media_proxy_controller_test.exs | 29 ------------------- test/support/conn_case.ex | 5 +--- 4 files changed, 2 insertions(+), 59 deletions(-) diff --git a/changelog.d/3896.add b/changelog.d/3896.add index 36d8286ff..e8fde4c8a 100644 --- a/changelog.d/3896.add +++ b/changelog.d/3896.add @@ -1 +1 @@ -Validate Host header for MediaProxy and Uploads and return a 302 if the base_url has changed +Validate Host header for Uploads and return a 302 if the base_url has changed diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index 20f3a3438..bda5b36ed 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -12,7 +12,6 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do alias Pleroma.Web.MediaProxy alias Plug.Conn - plug(:validate_host) plug(:sandbox) def remote(conn, %{"sig" => sig64, "url" => url64}) do @@ -206,30 +205,6 @@ defp media_proxy_opts do Config.get([:media_proxy, :proxy_opts], []) end - defp validate_host(conn, _params) do - %{scheme: proxy_scheme, host: proxy_host, port: proxy_port} = - MediaProxy.base_url() |> URI.parse() - - if match?(^proxy_host, conn.host) do - conn - else - redirect_url = - %URI{ - scheme: proxy_scheme, - host: proxy_host, - port: proxy_port, - path: conn.request_path, - query: conn.query_string - } - |> URI.to_string() - |> String.trim_trailing("?") - - conn - |> Phoenix.Controller.redirect(external: redirect_url) - |> halt() - end - end - defp sandbox(conn, _params) do conn |> merge_resp_headers([{"content-security-policy", "sandbox;"}]) diff --git a/test/pleroma/web/media_proxy/media_proxy_controller_test.exs b/test/pleroma/web/media_proxy/media_proxy_controller_test.exs index deb407709..9ce092fd8 100644 --- a/test/pleroma/web/media_proxy/media_proxy_controller_test.exs +++ b/test/pleroma/web/media_proxy/media_proxy_controller_test.exs @@ -54,35 +54,6 @@ test "it returns 403 for invalid signature", %{conn: conn, url: url} do } = get(conn, "/proxy/hhgfh/eeee/fff") end - test "it returns a 302 for invalid host", %{conn: conn} do - new_proxy_base = "http://mp.localhost/" - - %{scheme: new_proxy_scheme, host: new_proxy_host, port: new_proxy_port} = - URI.parse(new_proxy_base) - - clear_config([:media_proxy, :base_url], new_proxy_base) - - proxy_url = - MediaProxy.encode_url("https://pleroma.social/logo.jpeg") - |> URI.parse() - |> Map.put(:host, "wronghost") - |> URI.to_string() - - expected_url = - URI.parse(proxy_url) - |> Map.put(:host, new_proxy_host) - |> Map.put(:port, new_proxy_port) - |> Map.put(:scheme, new_proxy_scheme) - |> URI.to_string() - - with_mock Pleroma.ReverseProxy, - call: fn _conn, _url, _opts -> %Conn{status: :success} end do - conn = get(conn, proxy_url) - - assert redirected_to(conn, 302) == expected_url - end - end - test "redirects to valid url when filename is invalidated", %{conn: conn, url: url} do invalid_url = String.replace(url, "test.png", "test-file.png") response = get(conn, invalid_url) diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index c1cb0295b..f010fec33 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -120,9 +120,6 @@ defp json_response_and_validate_schema(conn, _status) do Mox.verify_on_exit!() - {:ok, - conn: - Phoenix.ConnTest.build_conn() - |> Map.put(:host, Pleroma.Web.Endpoint.host())} + {:ok, conn: Phoenix.ConnTest.build_conn()} end end From 1ca1b4b32f2cba5f78a2d55645b8d2814bbfc593 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 7 Jun 2023 09:25:57 -0400 Subject: [PATCH 050/199] changelog.d --- changelog.d/3902.skip | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 changelog.d/3902.skip diff --git a/changelog.d/3902.skip b/changelog.d/3902.skip new file mode 100644 index 000000000..e69de29bb From 10dfa107d5d5d3b6fa7c13eb005c4cd5b6b94661 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sun, 11 Jun 2023 16:22:03 +0400 Subject: [PATCH 051/199] Update changelog --- changelog.d/3874.remove | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/3874.remove diff --git a/changelog.d/3874.remove b/changelog.d/3874.remove new file mode 100644 index 000000000..a81f744bf --- /dev/null +++ b/changelog.d/3874.remove @@ -0,0 +1 @@ +Remove a few unused indexes. From 22878cf84afd3f8b844c66e99f37022318c4c55e Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sun, 11 Jun 2023 16:22:16 +0400 Subject: [PATCH 052/199] B Migrations: Don't remove activity_visibility_index for now. --- .../20230422154018_drop_unused_indexes.exs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/priv/repo/migrations/20230422154018_drop_unused_indexes.exs b/priv/repo/migrations/20230422154018_drop_unused_indexes.exs index f2ba6b8df..0d877a798 100644 --- a/priv/repo/migrations/20230422154018_drop_unused_indexes.exs +++ b/priv/repo/migrations/20230422154018_drop_unused_indexes.exs @@ -22,13 +22,6 @@ def up do drop_if_exists( index(:activities, ["((data #> '{\"object\",\"likes\"}'))"], name: :activities_likes) ) - - drop_if_exists( - index(:activities, ["activity_visibility(actor, recipients, data)", "id DESC NULLS LAST"], - name: :activities_visibility_index, - where: "data->>'type' = 'Create'" - ) - ) end def down do @@ -76,13 +69,5 @@ def down do concurrently: true ) ) - - create_if_not_exists( - index(:activities, ["activity_visibility(actor, recipients, data)", "id DESC NULLS LAST"], - name: :activities_visibility_index, - where: "data->>'type' = 'Create'", - concurrently: true - ) - ) end end From 6611c6ce4ef3fe76bc9a5eb53677d3b6cde22793 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sun, 11 Jun 2023 16:45:31 +0400 Subject: [PATCH 053/199] B ForceMentionsInContent: Fix test, refactor. --- .../web/activity_pub/mrf/force_mentions_in_content.ex | 8 +++++--- .../activity_pub/mrf/force_mentions_in_content_test.exs | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) 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 b9b175cc3..5532093cb 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 @@ -95,11 +95,13 @@ def filter( |> Enum.reject(&is_nil/1) |> sort_replied_user(replied_to_user) - explicitly_mentioned_uris = extract_mention_uris_from_content(content) + explicitly_mentioned_uris = + extract_mention_uris_from_content(content) + |> MapSet.new() added_mentions = - Enum.reduce(mention_users, "", fn %User{ap_id: api_id, uri: uri} = user, acc -> - unless Enum.any?([api_id, uri], fn u -> u in explicitly_mentioned_uris end) do + Enum.reduce(mention_users, "", fn %User{ap_id: ap_id, uri: uri} = user, acc -> + if MapSet.disjoint?(MapSet.new([ap_id, uri]), explicitly_mentioned_uris) do acc <> Formatter.mention_from_user(user, %{mentions_format: :compact}) <> " " else acc diff --git a/test/pleroma/web/activity_pub/mrf/force_mentions_in_content_test.exs b/test/pleroma/web/activity_pub/mrf/force_mentions_in_content_test.exs index 5ebd972a6..811ef105c 100644 --- a/test/pleroma/web/activity_pub/mrf/force_mentions_in_content_test.exs +++ b/test/pleroma/web/activity_pub/mrf/force_mentions_in_content_test.exs @@ -282,7 +282,7 @@ test "don't add duplicate mentions for mastodon or misskey posts" do {:ok, post} = CommonAPI.post(rogerick, %{status: "eugh"}) inline_mentions = [ - "@rogerick", + "@rogerick", "@greg" ] From a663b73634ddf355827051cc139c0159f8eb794b Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Tue, 13 Jun 2023 12:45:18 +0200 Subject: [PATCH 054/199] Add no_new_privs to OpenRC service files --- changelog.d/no_new_privs.add | 1 + installation/init.d/pleroma | 1 + rel/files/installation/init.d/pleroma | 1 + 3 files changed, 3 insertions(+) create mode 100644 changelog.d/no_new_privs.add diff --git a/changelog.d/no_new_privs.add b/changelog.d/no_new_privs.add new file mode 100644 index 000000000..b67396a4b --- /dev/null +++ b/changelog.d/no_new_privs.add @@ -0,0 +1 @@ +(hardening) Add no_new_privs=yes to OpenRC service files diff --git a/installation/init.d/pleroma b/installation/init.d/pleroma index 384536f7e..cb6635a0b 100755 --- a/installation/init.d/pleroma +++ b/installation/init.d/pleroma @@ -8,6 +8,7 @@ pidfile="/var/run/pleroma.pid" directory=/opt/pleroma healthcheck_delay=60 healthcheck_timer=30 +no_new_privs="yes" : ${pleroma_port:-4000} diff --git a/rel/files/installation/init.d/pleroma b/rel/files/installation/init.d/pleroma index dea1db26c..ca5b842e1 100755 --- a/rel/files/installation/init.d/pleroma +++ b/rel/files/installation/init.d/pleroma @@ -9,6 +9,7 @@ command=/opt/pleroma/bin/pleroma command_args="start" command_user=pleroma command_background=1 +no_new_privs="yes" # Ask process to terminate within 30 seconds, otherwise kill it retry="SIGTERM/30/SIGKILL/5" From d5a7079f42228980471b6210aaaea69fcaa4c9ed Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Tue, 13 Jun 2023 15:39:20 +0200 Subject: [PATCH 055/199] media_graphics_packages.md: Fix markdown syntax --- .../optional/media_graphics_packages.md | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/docs/installation/optional/media_graphics_packages.md b/docs/installation/optional/media_graphics_packages.md index de402d1c4..ad01d47d1 100644 --- a/docs/installation/optional/media_graphics_packages.md +++ b/docs/installation/optional/media_graphics_packages.md @@ -1,9 +1,10 @@ # Optional software packages needed for specific functionality For specific Pleroma functionality (which is disabled by default) some or all of the below packages are required: - * `ImageMagic` - * `ffmpeg` - * `exiftool` + +* `ImageMagic` +* `ffmpeg` +* `exiftool` Please refer to documentation in `docs/installation` on how to install them on specific OS. @@ -14,20 +15,23 @@ Note: the packages are not required with the current default settings of Pleroma `ImageMagick` is a set of tools to create, edit, compose, or convert bitmap images. It is required for the following Pleroma features: - * `Pleroma.Upload.Filters.Mogrify`, `Pleroma.Upload.Filters.Mogrifun` upload filters (related config: `Plaroma.Upload/filters` in `config/config.exs`) - * Media preview proxy for still images (related config: `media_preview_proxy/enabled` in `config/config.exs`) + +* `Pleroma.Upload.Filters.Mogrify`, `Pleroma.Upload.Filters.Mogrifun` upload filters (related config: `Plaroma.Upload/filters` in `config/config.exs`) +* Media preview proxy for still images (related config: `media_preview_proxy/enabled` in `config/config.exs`) ## `ffmpeg` `ffmpeg` is software to record, convert and stream audio and video. It is required for the following Pleroma features: - * Media preview proxy for videos (related config: `media_preview_proxy/enabled` in `config/config.exs`) + +* Media preview proxy for videos (related config: `media_preview_proxy/enabled` in `config/config.exs`) ## `exiftool` `exiftool` is media files metadata reader/writer. It is required for the following Pleroma features: - * `Pleroma.Upload.Filters.Exiftool.StripLocation` upload filter (related config: `Plaroma.Upload/filters` in `config/config.exs`) - * `Pleroma.Upload.Filters.Exiftool.ReadDescription` upload filter (related config: `Plaroma.Upload/filters` in `config/config.exs`) + +* `Pleroma.Upload.Filters.Exiftool.StripLocation` upload filter (related config: `Plaroma.Upload/filters` in `config/config.exs`) +* `Pleroma.Upload.Filters.Exiftool.ReadDescription` upload filter (related config: `Plaroma.Upload/filters` in `config/config.exs`) From 4392fff212b42204990569f396a69e74b053e5ca Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Tue, 13 Jun 2023 15:40:12 +0200 Subject: [PATCH 056/199] otp_vs_from_source*: Acknowledge distro packages --- docs/installation/otp_vs_from_source.include | 9 +++++++-- docs/installation/otp_vs_from_source_source.include | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/installation/otp_vs_from_source.include b/docs/installation/otp_vs_from_source.include index 63e837a53..6c7820275 100644 --- a/docs/installation/otp_vs_from_source.include +++ b/docs/installation/otp_vs_from_source.include @@ -1,3 +1,8 @@ -## OTP releases vs from-source installations +## Packaged (OTP) installation vs Manual (from-source) installations -There are two ways to install Pleroma. You can use OTP releases or do a from-source installation. OTP releases are as close as you can get to binary releases with Erlang/Elixir. The release is self-contained, and provides everything needed to boot it, it is easily administered via the provided shell script to open up a remote console, start/stop/restart the release, start in the background, send remote commands, and more. With from source installations you install Pleroma from source, meaning you have to install certain dependencies like Erlang+Elixir and compile Pleroma yourself. +There is multiple ways to install Pleroma. +
+
Distro-provided packages
This is the recommended method, where you can get the strongest compatibility guarantees and the best dependency-management
+
Pleroma-provided OTP binaries
Intended as fallback for Alpine/Debian-compatible systems lacking a proper Pleroma package, they are heavier than proper distro packages as they also contain Erlang/Elixir and can break after system updates
+
Manual from-source installation
Needs build-dependencies to be installed and manual updates+rebuilds. Allows for easier source-customisations.
+
diff --git a/docs/installation/otp_vs_from_source_source.include b/docs/installation/otp_vs_from_source_source.include index 63482b69d..9f7134229 100644 --- a/docs/installation/otp_vs_from_source_source.include +++ b/docs/installation/otp_vs_from_source_source.include @@ -1,3 +1,3 @@ {! backend/installation/otp_vs_from_source.include !} -This guide covers a from-source installation. To install using OTP releases, please check out [the OTP guide](./otp_en.md). +This guide covers a manual from-source installation. To install using OTP releases, please check for the presence of a distro package, failing that you can use [Pleroma-provided OTP binaries](./otp_en.md). From fb19f0d844eae80ff06015419119f7a709e5095f Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Tue, 13 Jun 2023 15:40:48 +0200 Subject: [PATCH 057/199] gentoo_otp_en: Add packaged installation documentation --- docs/installation/gentoo_otp_en.md | 207 +++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 docs/installation/gentoo_otp_en.md diff --git a/docs/installation/gentoo_otp_en.md b/docs/installation/gentoo_otp_en.md new file mode 100644 index 000000000..5dcfe0b7b --- /dev/null +++ b/docs/installation/gentoo_otp_en.md @@ -0,0 +1,207 @@ +# Packaged install on Gentoo Linux + +{! backend/installation/otp_vs_from_source.include !} + +A [manual installation guide for gentoo](./gentoo_en.md) is also available. + +## Installation + +This guide will assume that you have administrative rights, either as root or a user with [sudo permissions](https://wiki.gentoo.org/wiki/Sudo). Lines that begin with `#` indicate that they should be run as the superuser. Lines using `$` should be run as the indicated user, e.g. `pleroma$` should be run as the `pleroma` user. + +{! backend/installation/generic_dependencies.include !} + +### Installing a cron daemon + +Gentoo quite pointedly does not come with a cron daemon installed, and as such it is recommended you install one to automate certbot renewals and to allow other system administration tasks to be run automatically. Gentoo has [a whole wide world of cron options](https://wiki.gentoo.org/wiki/Cron) but if you just want A Cron That Works, `emerge --ask virtual/cron` will install the default cron implementation (probably cronie) which will work just fine. For the purpouses of this guide, we will be doing just that. + +### Required ebuilds + +* `www-apps/pleroma` + +#### Optional ebuilds used in this guide + +* `www-servers/nginx` (preferred, example configs for other reverse proxies can be found in the repo) +* `app-crypt/certbot` (or any other ACME client for Let’s Encrypt certificates) +* `app-crypt/certbot-nginx` (nginx certbot plugin that allows use of the all-powerful `--nginx` flag on certbot) +* `media-gfx/imagemagick` +* `media-video/ffmpeg` +* `media-libs/exiftool` + +### Prepare the system + +* If you haven't yet done so, add the [Gentoo User Repository (GURU)](https://wiki.gentoo.org/wiki/Project:GURU), where the `www-apps/pleroma` ebuild currently lives at: +```shell + # eselect repository enable guru +``` + +* Ensure that you have the latest copy of the Gentoo and GURU ebuilds if you have not synced them yet: + +```shell + # emaint sync -a +``` + + +* Emerge all required the required and suggested software in one go: + +```shell + # emerge --ask www-apps/pleroma www-servers/nginx app-crypt/certbot app-crypt/certbot-nginx +``` + +If you would not like to install the optional packages, remove them from this line. + +If you're running this from a low-powered virtual machine, it should work though it will take some time. There were no issues on a VPS with a single core and 1GB of RAM; if you are using an even more limited device and run into issues, you can try creating a swapfile or use a more powerful machine running Gentoo to [cross build](https://wiki.gentoo.org/wiki/Cross_build_environment). If you have a wait ahead of you, now would be a good time to take a break, strech a bit, refresh your beverage of choice and/or get a snack, and reply to Arch users' posts with "I use Gentoo btw" as we do. + +### Setup PostgreSQL + +[Gentoo Wiki article](https://wiki.gentoo.org/wiki/PostgreSQL) as well as [PostgreSQL QuickStart](https://wiki.gentoo.org/wiki/PostgreSQL/QuickStart) might be worth a quick glance, as the way Gentoo handles postgres is slightly unusual, with built in capability to have two different databases running for testing and live or whatever other purpouse. While it is still straightforward to install, it does mean that the version numbers used in this guide might change for future updates, so keep an eye out for the output you get from `emerge` to ensure you are using the correct ones. + +* Initialize the database cluster + +The output from emerging postgresql should give you a command for initializing the postgres database. The default slot should be indicated in this command, ensure that it matches the command below. + +```shell + # emerge --config dev-db/postgresql:11 +``` + +### Install media / graphics packages (optional) + +See [Optional software packages needed for specific functionality](docs/installation/optional/media_graphics_packages.md) for details. + +```shell +# emerge --ask media-video/ffmpeg media-gfx/imagemagick media-libs/exiftool +``` + +### Setup PleromaBE + +* Generate the configuration: + +```shell + # pleroma_ctl instance gen --output /etc/pleroma/config.exs --output-psql /tmp/setup_db.psql" +``` + +* Create the PostgreSQL database + +```shell + # sudo -u postgres -s $SHELL -lc "psql -f /tmp/setup_db.psql" +``` + +* Now run the database migration: + +```shell + # pleroma_ctl migrate +``` + +* Optional: If you have installed RUM indexes (`dev-db/rum`) you also need to run: +``` + # sudo -Hu pleroma "pleroma_ctl migrate --migrations-path priv/repo/optional_migrations/rum_indexing/" +``` + +* Now you can start Pleroma already and add it in the default runlevel + +```shell + # rc-service pleroma start + # rc-update add pleroma default +``` + +It probably won't work over the public internet quite yet, however, as we still need to set up a web server to proxy to the pleroma application, as well as configure SSL. + +### Finalize installation + +Assuming you want to open your newly installed federated social network to, well, the federation, you should run nginx or some other webserver/proxy in front of Pleroma. It is also a good idea to set up Pleroma to run as a system service. + +#### Nginx + +* Install nginx, if not already done: + +```shell + # emerge --ask www-servers/nginx +``` + +* Create directories for available and enabled sites: + +```shell + # mkdir -p /etc/nginx/sites-{available,enabled} +``` + +* Append the following line at the end of the `http` block in `/etc/nginx/nginx.conf`: + +```Nginx +include sites-enabled/*; +``` + +* Setup your SSL cert, using your method of choice or certbot. If using certbot, install it if you haven't already: + +```shell + # emerge --ask app-crypt/certbot app-crypt/certbot-nginx +``` + +and then set it up: + +```shell + # mkdir -p /var/lib/letsencrypt/ + # certbot certonly --email -d --standalone +``` + +If that doesn't work the first time, add `--dry-run` to further attempts to avoid being ratelimited as you identify the issue, and do not remove it until the dry run succeeds. If that doesn’t work, make sure, that nginx is not already running. If it still doesn’t work, try setting up nginx first (change ssl “on” to “off” and try again). Often the answer to issues with certbot is to use the `--nginx` flag once you have nginx up and running. + +If you are using any additional subdomains, such as for a media proxy, you can re-run the same command with the subdomain in question. When it comes time to renew later, you will not need to run multiple times for each domain, one renew will handle it. + +--- + +* Copy the example nginx configuration and activate it: + +```shell + # cp /opt/pleroma/installation/pleroma.nginx /etc/nginx/sites-available/ + # ln -s /etc/nginx/sites-available/pleroma.nginx /etc/nginx/sites-enabled/pleroma.nginx +``` + +* Take some time to ensure that your nginx config is correct + +Replace all instances of `example.tld` with your instance's public URL. If for whatever reason you made changes to the port that your pleroma app runs on, be sure that is reflected in your configuration. + +Pay special attention to the line that begins with `ssl_ecdh_curve`. It is stongly advised to comment that line out so that OpenSSL will use its full capabilities, and it is also possible you are running OpenSSL 1.0.2 necessitating that you do this. + +* Enable and start nginx: + +```shell + # rc-update add nginx default + # /etc/init.d/nginx start +``` + +If you are using certbot, it is HIGHLY recommend you set up a cron job that renews your certificate, and that you install the suggested `certbot-nginx` plugin. If you don't do these things, you only have yourself to blame when your instance breaks suddenly because you forgot about it. + +First, ensure that the command you will be installing into your crontab works. + +```shell + # /usr/bin/certbot renew --nginx +``` + +Assuming not much time has passed since you got certbot working a few steps ago, you should get a message for all domains you installed certificates for saying `Cert not yet due for renewal`. + +Now, run crontab as a superuser with `crontab -e` or `sudo crontab -e` as appropriate, and add the following line to your cron: + +```cron +0 0 1 * * /usr/bin/certbot renew --nginx +``` + +This will run certbot on the first of the month at midnight. If you'd rather run more frequently, it's not a bad idea, feel free to go for it. + +#### Other webserver/proxies + +If you would like to use other webservers or proxies, there are example configurations for some popular alternatives in `/opt/pleroma/installation/`. You can, of course, check out [the Gentoo wiki](https://wiki.gentoo.org) for more information on installing and configuring said alternatives. + +#### Create your first user + +If your instance is up and running, you can create your first user with administrative rights with the following task: + +```shell +pleroma$ pleroma_ctl user new --admin +``` + +#### Further reading + +{! backend/installation/further_reading.include !} + +## 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. From eddfd41c1f2fb15f5a28b5474a64bed7b86d62d7 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Tue, 13 Jun 2023 15:41:09 +0200 Subject: [PATCH 058/199] gentoo_en: Reference packaged installation --- docs/installation/gentoo_en.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/installation/gentoo_en.md b/docs/installation/gentoo_en.md index 36882c8c8..02a3c709b 100644 --- a/docs/installation/gentoo_en.md +++ b/docs/installation/gentoo_en.md @@ -1,6 +1,8 @@ -# Installing on Gentoo GNU/Linux +# Manual install on Gentoo GNU/Linux -{! backend/installation/otp_vs_from_source_source.include !} +{! backend/installation/otp_vs_from_source.include !} + +This guide covers a manual from-source installation. To use the gentoo package, please check the [packaged installation guide for gentoo](./gentoo.en.md). ## Installation From 937fa36ec4ed8f8aa6e2e608003260bc7c5d8d57 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Tue, 13 Jun 2023 15:47:48 +0200 Subject: [PATCH 059/199] changelog.d/gentoo_otp.skip: Doc-only MR --- changelog.d/gentoo_otp.skip | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 changelog.d/gentoo_otp.skip diff --git a/changelog.d/gentoo_otp.skip b/changelog.d/gentoo_otp.skip new file mode 100644 index 000000000..e69de29bb From 8fa435f370cd1a85a37eae011d9db604256fbef1 Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 14 Jun 2023 21:48:10 +0000 Subject: [PATCH 060/199] Add "potentially outdated" notice in non-English versions --- docs/installation/debian_based_jp.md | 3 +++ docs/installation/openbsd_fi.md | 2 ++ 2 files changed, 5 insertions(+) diff --git a/docs/installation/debian_based_jp.md b/docs/installation/debian_based_jp.md index 3736e857f..2489e3473 100644 --- a/docs/installation/debian_based_jp.md +++ b/docs/installation/debian_based_jp.md @@ -1,4 +1,7 @@ # Pleromaの入れ方 + +Note: This article is potentially outdated because at this time we may not have people who can speak this language well enough to update it. To see the up-to-date version, which may have significant differences or important caveats of the installation process, look up the English version. + ## 日本語訳について この記事は [Installing on Debian based distributions](Installing on Debian based distributions) の日本語訳です。何かがおかしいと思ったら、原文を見てください。 diff --git a/docs/installation/openbsd_fi.md b/docs/installation/openbsd_fi.md index 3c40b2d1a..73aca3a6f 100644 --- a/docs/installation/openbsd_fi.md +++ b/docs/installation/openbsd_fi.md @@ -1,5 +1,7 @@ # Pleroman asennus OpenBSD:llä +Note: This article is potentially outdated because at this time we may not have people who can speak this language well enough to update it. To see the up-to-date version, which may have significant differences or important caveats of the installation process, look up the English version. + Tarvitset: * Oman domainin * OpenBSD 6.3 -serverin From a5a354a36e144c19ce3f9e79cb898227fc7ef723 Mon Sep 17 00:00:00 2001 From: Sean King Date: Wed, 21 Jun 2023 23:10:56 -0600 Subject: [PATCH 061/199] Prevent bypassing authorized fetch mode with a json file --- lib/pleroma/web/plugs/http_signature_plug.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/plugs/http_signature_plug.ex b/lib/pleroma/web/plugs/http_signature_plug.ex index 4bf325218..e814efc2c 100644 --- a/lib/pleroma/web/plugs/http_signature_plug.ex +++ b/lib/pleroma/web/plugs/http_signature_plug.ex @@ -16,7 +16,7 @@ def call(%{assigns: %{valid_signature: true}} = conn, _opts) do end def call(conn, _opts) do - if get_format(conn) == "activity+json" do + if get_format(conn) in ["json", "activity+json"] do conn |> maybe_assign_valid_signature() |> maybe_require_signature() From 994bfc4c095fae07cd1c61b0f91c80ec60080138 Mon Sep 17 00:00:00 2001 From: Sean King Date: Wed, 21 Jun 2023 23:13:16 -0600 Subject: [PATCH 062/199] Add changelog entry --- changelog.d/prevent-bypassing-authorized-fetch-mode.fix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/prevent-bypassing-authorized-fetch-mode.fix diff --git a/changelog.d/prevent-bypassing-authorized-fetch-mode.fix b/changelog.d/prevent-bypassing-authorized-fetch-mode.fix new file mode 100644 index 000000000..12f7260d7 --- /dev/null +++ b/changelog.d/prevent-bypassing-authorized-fetch-mode.fix @@ -0,0 +1 @@ +Prevent using the .json format to bypass authorized fetch mode \ No newline at end of file From 8bf8906045d15254dd536b97bdaf04bc772e6fa6 Mon Sep 17 00:00:00 2001 From: weblate-extractor Date: Thu, 22 Jun 2023 06:09:47 +0000 Subject: [PATCH 063/199] Extract translatable strings --- priv/gettext/config_descriptions.pot | 2052 +++++++++++++------------- priv/gettext/default.pot | 68 +- priv/gettext/errors.pot | 166 +-- priv/gettext/oauth_scopes.pot | 108 +- priv/gettext/static_pages.pot | 182 +-- 5 files changed, 1232 insertions(+), 1344 deletions(-) diff --git a/priv/gettext/config_descriptions.pot b/priv/gettext/config_descriptions.pot index 53b81fa41..4f60e1c85 100644 --- a/priv/gettext/config_descriptions.pot +++ b/priv/gettext/config_descriptions.pot @@ -10,6038 +10,5966 @@ msgid "" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 -msgctxt "config description at :esshd" -msgid "Before enabling this you must add :esshd to mix.exs as one of the extra_applications and generate host keys in your priv dir with ssh-keygen -m PEM -N \"\" -b 2048 -t rsa -f ssh_host_rsa_key" -msgstr "" - #, elixir-autogen, elixir-format -#: lib/pleroma/docs/translator.ex:5 msgctxt "config description at :logger" msgid "Logger-related settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :mime" msgid "Mime Types settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma" msgid "Allows setting a token that can be used to authenticate requests with admin privileges without a normal user account token. Append the `admin_token` parameter to requests to utilize it. (Please reconsider using HTTP Basic Auth or OAuth-based authentication if possible)" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma" msgid "Authenticator" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :cors_plug" msgid "CORS plug config" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 -msgctxt "config label at :esshd" -msgid "ESSHD" -msgstr "" - #, elixir-autogen, elixir-format -#: lib/pleroma/docs/translator.ex:5 msgctxt "config label at :logger" msgid "Logger" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :mime" msgid "Mime Types" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma" msgid "Pleroma Admin Token" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma" msgid "Pleroma Authenticator" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :logger-:console" msgid "Console logger settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :logger-:ex_syslogger" msgid "ExSyslogger-related settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:activitypub" msgid "ActivityPub-related settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:assets" msgid "This section configures assets to be used with various frontends. Currently the only option relates to mascots on the mastodon frontend" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:auth" msgid "Authentication / authorization settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:connections_pool" msgid "Advanced settings for `Gun` connections pool" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:email_notifications" msgid "Email notifications settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:features" msgid "Customizable features" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:feed" msgid "Configure feed rendering" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontend_configurations" msgid "This form can be used to configure a keyword list that keeps the configuration data for any kind of frontend. By default, settings for pleroma_fe are configured. If you want to add your own configuration your settings all fields must be complete." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontends" msgid "Installed frontends management" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:gopher" msgid "Gopher settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:hackney_pools" msgid "Advanced settings for `Hackney` connections pools" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:http" msgid "HTTP settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:http_security" msgid "HTTP security settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance" msgid "Instance-related settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instances_favicons" msgid "Control favicons for instances" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:ldap" msgid "Use LDAP for user authentication. When a user logs in to the Pleroma instance, the name and password will be verified by trying to authenticate (bind) to a LDAP server. If a user exists in the LDAP directory but there is no account with the same name yet on the Pleroma instance then a new Pleroma account will be created with the same name as the LDAP user name." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:majic_pool" msgid "Majic/libmagic configuration" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:manifest" msgid "This section describe PWA manifest instance-specific values. Currently this option relate only for MastoFE." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:media_preview_proxy" msgid "Media preview proxy" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:media_proxy" msgid "Media proxy" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:modules" msgid "Custom Runtime Modules" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf" msgid "General MRF settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_activity_expiration" msgid "Adds automatic expiration to all local activities" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_follow_bot" msgid "Automatically follows newly discovered accounts." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_hashtag" msgid "Reject, TWKN-remove or Set-Sensitive messsages with specific hashtags (without the leading #)\n\nNote: This MRF Policy is always enabled, if you want to disable it you have to set empty lists.\n" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_hellthread" msgid "Block messages with excessive user mentions" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_keyword" msgid "Reject or Word-Replace messages matching a keyword or [Regex](https://hexdocs.pm/elixir/Regex.html)." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_mention" msgid "Block messages which mention a specific user" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_normalize_markup" msgid "MRF NormalizeMarkup settings. Scrub configured hypertext markup." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_object_age" msgid "Rejects or delists posts based on their timestamp deviance from your server's clock." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_rejectnonpublic" msgid "RejectNonPublic drops posts with non-public visibility settings." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_simple" msgid "Simple ingress policies" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_steal_emoji" msgid "Steals emojis from selected instances when it sees them." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_subchain" msgid "This policy processes messages through an alternate pipeline when a given message matches certain criteria. All criteria are configured as a map of regular expressions to lists of policy modules." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_vocabulary" msgid "Filter messages which belong to certain activity vocabularies" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:oauth2" msgid "Configure OAuth 2 provider capabilities" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:pools" msgid "Advanced settings for `Gun` workers pools" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:populate_hashtags_table" msgid "`populate_hashtags_table` background migration settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:rate_limit" msgid "Rate limit settings. This is an advanced feature enabled only for :authentication by default." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:restrict_unauthenticated" msgid "Disallow viewing timelines, user profiles and statuses for unauthenticated users." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:rich_media" msgid "If enabled the instance will parse metadata from attached links to generate link previews" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:shout" msgid "Pleroma shout settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:static_fe" msgid "Render profiles and posts using server-generated HTML that is viewable without using JavaScript" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:streamer" msgid "Settings for notifications streamer" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:uri_schemes" msgid "URI schemes related settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:web_cache_ttl" msgid "The expiration time for the web responses cache. Values should be in milliseconds or `nil` to disable expiration." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:welcome" msgid "Welcome messages settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:workers" msgid "Includes custom worker options not interpretable directly by `Oban`" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-ConcurrentLimiter" msgid "Limits configuration for background tasks." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Oban" msgid "[Oban](https://github.com/sorentwo/oban) asynchronous job processor configuration." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Captcha" msgid "Captcha-related settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Captcha.Kocaptcha" msgid "Kocaptcha is a very simple captcha service with a single API endpoint, the source code is here: https://github.com/koto-bank/kocaptcha. The default endpoint (https://captcha.kotobank.ch) is hosted by the developer." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Emails.Mailer" msgid "Mailer-related settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Emails.NewUsersDigestEmail" msgid "New users admin email digest" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Emails.UserEmail" msgid "Email template settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Formatter" msgid "Configuration for Pleroma's link formatter which parses mentions, hashtags, and URLs." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.ScheduledActivity" msgid "Scheduled activities settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Upload" msgid "Upload general settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Upload.Filter.AnonymizeFilename" msgid "Filter replaces the filename of the upload" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Upload.Filter.Mogrify" msgid "Uploads mogrify filter settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Uploaders.Local" msgid "Local uploader-related settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Uploaders.S3" msgid "S3 uploader-related settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.User.Backup" msgid "Account Backup" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Http" msgid "HTTP invalidate settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Script" msgid "Invalidation script settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Web.Metadata" msgid "Metadata-related settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Web.Plugs.RemoteIp" msgid "`Pleroma.Web.Plugs.RemoteIp` is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.\n**If your instance is not behind at least one reverse proxy, you should not enable this plug.**\n" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Web.Preload" msgid "Preload-related settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Workers.PurgeExpiredActivity" msgid "Expired activities settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :prometheus-Pleroma.Web.Endpoint.MetricsExporter" msgid "Prometheus app metrics endpoint configuration" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :web_push_encryption-:vapid_details" msgid "Web Push Notifications configuration. You can use the mix task mix web_push.gen.keypair to generate it." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :ex_aws-:s3" msgid "S3" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :logger-:console" msgid "Console Logger" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :logger-:ex_syslogger" msgid "ExSyslogger" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:activitypub" msgid "ActivityPub" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:assets" msgid "Assets" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:auth" msgid "Auth" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:connections_pool" msgid "Connections pool" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:email_notifications" msgid "Email notifications" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:emoji" msgid "Emoji" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:features" msgid "Features" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:feed" msgid "Feed" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontend_configurations" msgid "Frontend configurations" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontends" msgid "Frontends" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:gopher" msgid "Gopher" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:hackney_pools" msgid "Hackney pools" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:http" msgid "HTTP" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:http_security" msgid "HTTP security" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance" msgid "Instance" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instances_favicons" msgid "Instances favicons" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:ldap" msgid "LDAP" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:majic_pool" msgid "Majic pool" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:manifest" msgid "Manifest" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:markup" msgid "Markup Settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:media_preview_proxy" msgid "Media preview proxy" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:media_proxy" msgid "Media proxy" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:modules" msgid "Modules" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf" msgid "MRF" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_activity_expiration" msgid "MRF Activity Expiration Policy" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_follow_bot" msgid "MRF FollowBot Policy" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_hashtag" msgid "MRF Hashtag" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_hellthread" msgid "MRF Hellthread" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_keyword" msgid "MRF Keyword" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_mention" msgid "MRF Mention" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_normalize_markup" msgid "MRF Normalize Markup" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_object_age" msgid "MRF Object Age" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_rejectnonpublic" msgid "MRF Reject Non Public" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_simple" msgid "MRF Simple" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_steal_emoji" msgid "MRF Emojis" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_subchain" msgid "MRF Subchain" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_vocabulary" msgid "MRF Vocabulary" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:oauth2" msgid "OAuth2" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:pools" msgid "Pools" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:populate_hashtags_table" msgid "Populate hashtags table" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:rate_limit" msgid "Rate limit" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:restrict_unauthenticated" msgid "Restrict Unauthenticated" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:rich_media" msgid "Rich media" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:shout" msgid "Shout" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:static_fe" msgid "Static FE" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:streamer" msgid "Streamer" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:uri_schemes" msgid "URI Schemes" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:user" msgid "User" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:web_cache_ttl" msgid "Web cache TTL" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:welcome" msgid "Welcome" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:workers" msgid "Workers" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-ConcurrentLimiter" msgid "ConcurrentLimiter" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Oban" msgid "Oban" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Captcha" msgid "Pleroma.Captcha" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Captcha.Kocaptcha" msgid "Pleroma.Captcha.Kocaptcha" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.Mailer" msgid "Pleroma.Emails.Mailer" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.NewUsersDigestEmail" msgid "Pleroma.Emails.NewUsersDigestEmail" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.UserEmail" msgid "Pleroma.Emails.UserEmail" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Formatter" msgid "Linkify" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.ScheduledActivity" msgid "Pleroma.ScheduledActivity" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Upload" msgid "Pleroma.Upload" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Upload.Filter.AnonymizeFilename" msgid "Pleroma.Upload.Filter.AnonymizeFilename" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Upload.Filter.Mogrify" msgid "Pleroma.Upload.Filter.Mogrify" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Uploaders.Local" msgid "Pleroma.Uploaders.Local" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Uploaders.S3" msgid "Pleroma.Uploaders.S3" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.User" msgid "Pleroma.User" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.User.Backup" msgid "Pleroma.User.Backup" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Web.ApiSpec.CastAndValidate" msgid "Pleroma.Web.ApiSpec.CastAndValidate" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Http" msgid "Pleroma.Web.MediaProxy.Invalidation.Http" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Script" msgid "Pleroma.Web.MediaProxy.Invalidation.Script" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Web.Metadata" msgid "Pleroma.Web.Metadata" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Web.Plugs.RemoteIp" msgid "Pleroma.Web.Plugs.RemoteIp" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Web.Preload" msgid "Pleroma.Web.Preload" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Workers.PurgeExpiredActivity" msgid "Pleroma.Workers.PurgeExpiredActivity" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :prometheus-Pleroma.Web.Endpoint.MetricsExporter" msgid "Pleroma.Web.Endpoint.MetricsExporter" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :web_push_encryption-:vapid_details" msgid "Vapid Details" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 -msgctxt "config description at :esshd > :enabled" -msgid "Enables SSH" -msgstr "" - #, elixir-autogen, elixir-format -#: lib/pleroma/docs/translator.ex:5 -msgctxt "config description at :esshd > :handler" -msgid "Handler module" -msgstr "" - -#, elixir-autogen, elixir-format -#: lib/pleroma/docs/translator.ex:5 -msgctxt "config description at :esshd > :password_authenticator" -msgid "Authenticator module" -msgstr "" - -#, elixir-autogen, elixir-format -#: lib/pleroma/docs/translator.ex:5 -msgctxt "config description at :esshd > :port" -msgid "Port to connect" -msgstr "" - -#, elixir-autogen, elixir-format -#: lib/pleroma/docs/translator.ex:5 -msgctxt "config description at :esshd > :priv_dir" -msgid "Dir with SSH keys" -msgstr "" - -#, elixir-autogen, elixir-format -#: lib/pleroma/docs/translator.ex:5 msgctxt "config description at :ex_aws-:s3 > :access_key_id" msgid "S3 access key ID" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :ex_aws-:s3 > :host" msgid "S3 host" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :ex_aws-:s3 > :region" msgid "S3 region (for AWS)" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :ex_aws-:s3 > :secret_access_key" msgid "Secret access key" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :logger > :backends" msgid "Where logs will be sent, :console - send logs to stdout, { ExSyslogger, :ex_syslogger } - to syslog, Quack.Logger - to Slack." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :logger-:console > :format" msgid "Default: \"$date $time [$level] $levelpad$node $metadata $message\"" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :logger-:console > :level" msgid "Log level" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :logger-:ex_syslogger > :format" msgid "Default: \"$date $time [$level] $levelpad$node $metadata $message\"" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :logger-:ex_syslogger > :ident" msgid "A string that's prepended to every message, and is typically set to the app name" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :logger-:ex_syslogger > :level" msgid "Log level" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma > :admin_token" msgid "Admin token" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:activitypub > :blockers_visible" msgid "Whether a user can see someone who has blocked them" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:activitypub > :follow_handshake_timeout" msgid "Following handshake timeout" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:activitypub > :note_replies_output_limit" msgid "The number of Note replies' URIs to be included with outgoing federation (`5` to match Mastodon hardcoded value, `0` to disable the output)" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:activitypub > :outgoing_blocks" msgid "Whether to federate blocks to other instances" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:activitypub > :sign_object_fetches" msgid "Sign object fetches with HTTP signatures" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:activitypub > :unfollow_blocked" msgid "Whether blocks result in people getting unfollowed" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:assets > :default_mascot" msgid "This will be used as the default mascot on MastoFE. Default: `:pleroma_fox_tan`" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:assets > :default_user_avatar" msgid "URL of the default user avatar" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:assets > :mascots" msgid "Keyword of mascots, each element must contain both an URL and a mime_type key" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:auth > :auth_template" msgid "Authentication form template. By default it's `show.html` which corresponds to `lib/pleroma/web/templates/o_auth/o_auth/show.html.ee`." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:auth > :enforce_oauth_admin_scope_usage" msgid "OAuth admin scope requirement toggle. If enabled, admin actions explicitly demand admin OAuth scope(s) presence in OAuth token (client app must support admin scopes). If disabled and token doesn't have admin scope(s), `is_admin` user flag grants access to admin-specific actions." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:auth > :oauth_consumer_strategies" msgid "The list of enabled OAuth consumer strategies. By default it's set by OAUTH_CONSUMER_STRATEGIES environment variable. Each entry in this space-delimited string should be of format \"strategy\" or \"strategy:dependency\" (e.g. twitter or keycloak:ueberauth_keycloak_strategy in case dependency is named differently than ueberauth_)." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:auth > :oauth_consumer_template" msgid "OAuth consumer mode authentication form template. By default it's `consumer.html` which corresponds to `lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex`." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:connections_pool > :connect_timeout" msgid "Timeout while `gun` will wait until connection is up. Default: 5000ms." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:connections_pool > :connection_acquisition_retries" msgid "Number of attempts to acquire the connection from the pool if it is overloaded. Default: 5" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:connections_pool > :connection_acquisition_wait" msgid "Timeout to acquire a connection from pool. The total max time is this value multiplied by the number of retries. Default: 250ms." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:connections_pool > :max_connections" msgid "Maximum number of connections in the pool. Default: 250 connections." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:connections_pool > :reclaim_multiplier" msgid "Multiplier for the number of idle connection to be reclaimed if the pool is full. For example if the pool maxes out at 250 connections and this setting is set to 0.3, the pool will reclaim at most 75 idle connections if it's overloaded. Default: 0.1" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:email_notifications > :digest" msgid "emails of \"what you've missed\" for users who have been inactive for a while" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:email_notifications > :digest > :active" msgid "Globally enable or disable digest emails" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:email_notifications > :digest > :inactivity_threshold" msgid "Minimum user inactivity threshold" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:email_notifications > :digest > :interval" msgid "Minimum interval between digest emails to one user" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:email_notifications > :digest > :schedule" msgid "When to send digest email, in crontab format. \"0 0 0\" is the default, meaning \"once a week at midnight on Sunday morning\"." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:emoji > :default_manifest" msgid "Location of the JSON-manifest. This manifest contains information about the emoji-packs you can download. Currently only one manifest can be added (no arrays)." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:emoji > :groups" msgid "Emojis are ordered in groups (tags). This is an array of key-value pairs where the key is the group name and the value is the location or array of locations. * can be used as a wildcard." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:emoji > :pack_extensions" msgid "A list of file extensions for emojis, when no emoji.txt for a pack is present" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:emoji > :shortcode_globs" msgid "Location of custom emoji files. * can be used as a wildcard." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:features > :improved_hashtag_timeline" msgid "Setting to force toggle / force disable improved hashtags timeline. `:enabled` forces hashtags to be fetched from `hashtags` table for hashtags timeline. `:disabled` forces object-embedded hashtags to be used (slower). Keep it `:auto` for automatic behaviour (it is auto-set to `:enabled` [unless overridden] when HashtagsTableMigrator completes)." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:feed > :post_title" msgid "Configure title rendering" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:feed > :post_title > :max_length" msgid "Maximum number of characters before truncating title" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:feed > :post_title > :omission" msgid "Replacement which will be used after truncating string" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe" msgid "Settings for Pleroma FE" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :alwaysShowSubjectInput" msgid "When disabled, auto-hide the subject field if it's empty" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :background" msgid "URL of the background, unless viewing a user profile with a background that is set" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :collapseMessageWithSubject" msgid "When a message has a subject (aka Content Warning), collapse it by default" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :greentext" msgid "Enables green text on lines prefixed with the > character" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :hideFilteredStatuses" msgid "Hides filtered statuses from timelines" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :hideMutedPosts" msgid "Hides muted statuses from timelines" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :hidePostStats" msgid "Hide notices statistics (repeats, favorites, ...)" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :hideSitename" msgid "Hides instance name from PleromaFE banner" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :hideUserStats" msgid "Hide profile statistics (posts, posts per day, followers, followings, ...)" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :logo" msgid "URL of the logo, defaults to Pleroma's logo" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :logoMargin" msgid "Allows you to adjust vertical margins between logo boundary and navbar borders. The idea is that to have logo's image without any extra margins and instead adjust them to your need in layout." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :logoMask" msgid "By default it assumes logo used will be monochrome with alpha channel to be compatible with both light and dark themes. If you want a colorful logo you must disable logoMask." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :minimalScopesMode" msgid "Limit scope selection to Direct, User default, and Scope of post replying to. Also prevents replying to a DM with a public post from PleromaFE." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :nsfwCensorImage" msgid "URL of the image to use for hiding NSFW media attachments in the timeline" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :postContentType" msgid "Default post formatting option" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :redirectRootLogin" msgid "Relative URL which indicates where to redirect when a user is logged in" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :redirectRootNoLogin" msgid "Relative URL which indicates where to redirect when a user isn't logged in" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :scopeCopy" msgid "Copy the scope (private/unlisted/public) in replies to posts by default" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :showFeaturesPanel" msgid "Enables panel displaying functionality of the instance on the About page" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :showInstanceSpecificPanel" msgid "Whether to show the instance's custom panel" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :sidebarRight" msgid "Change alignment of sidebar and panels to the right" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :subjectLineBehavior" msgid "Allows changing the default behaviour of subject lines in replies.\n `email`: copy and preprend re:, as in email,\n `masto`: copy verbatim, as in Mastodon,\n `noop`: don't copy the subject." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :theme" msgid "Which theme to use. Available themes are defined in styles.json" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontends > :admin" msgid "Admin frontend" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontends > :admin > name" msgid "Name of the installed frontend. Valid config must include both `Name` and `Reference` values." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontends > :admin > ref" msgid "Reference of the installed frontend to be used. Valid config must include both `Name` and `Reference` values." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontends > :available" msgid "A map containing available frontends and parameters for their installation." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontends > :available > build_dir" msgid "The directory inside the zip file " msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontends > :available > build_url" msgid "Either an url to a zip file containing the frontend or a template to build it by inserting the `ref`. The string `${ref}` will be replaced by the configured `ref`." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontends > :available > custom-http-headers" msgid "The custom HTTP headers for the frontend" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontends > :available > git" msgid "URL of the git repository of the frontend" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontends > :available > name" msgid "Name of the frontend." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontends > :available > ref" msgid "Reference of the frontend to be used." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontends > :primary" msgid "Primary frontend, the one that is served for all pages by default" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontends > :primary > name" msgid "Name of the installed frontend. Valid config must include both `Name` and `Reference` values." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontends > :primary > ref" msgid "Reference of the installed frontend to be used. Valid config must include both `Name` and `Reference` values." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:gopher > :dstport" msgid "Port advertised in URLs (optional, defaults to port)" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:gopher > :enabled" msgid "Enables the gopher interface" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:gopher > :ip" msgid "IP address to bind to" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:gopher > :port" msgid "Port to bind to" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:hackney_pools > :federation" msgid "Settings for federation pool." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:hackney_pools > :federation > :max_connections" msgid "Number workers in the pool." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:hackney_pools > :federation > :timeout" msgid "Timeout while `hackney` will wait for response." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:hackney_pools > :media" msgid "Settings for media pool." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:hackney_pools > :media > :max_connections" msgid "Number workers in the pool." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:hackney_pools > :media > :timeout" msgid "Timeout while `hackney` will wait for response." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:hackney_pools > :upload" msgid "Settings for upload pool." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:hackney_pools > :upload > :max_connections" msgid "Number workers in the pool." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:hackney_pools > :upload > :timeout" msgid "Timeout while `hackney` will wait for response." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:http > :adapter" msgid "Adapter specific options" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:http > :adapter > :ssl_options" msgid "SSL options for HTTP adapter" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:http > :adapter > :ssl_options > :versions" msgid "List of TLS version to use" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:http > :proxy_url" msgid "Proxy URL" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:http > :user_agent" msgid "What user agent to use. Must be a string or an atom `:default`. Default value is `:default`." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:http_security > :ct_max_age" msgid "The maximum age for the Expect-CT header if sent" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:http_security > :enabled" msgid "Whether the managed content security policy is enabled" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:http_security > :referrer_policy" msgid "The referrer policy to use, either \"same-origin\" or \"no-referrer\"" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:http_security > :report_uri" msgid "Adds the specified URL to report-uri and report-to group in CSP header" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:http_security > :sts" msgid "Whether to additionally send a Strict-Transport-Security header" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:http_security > :sts_max_age" msgid "The maximum age for the Strict-Transport-Security header if sent" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :account_activation_required" msgid "Require users to confirm their emails before signing in" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :account_approval_required" msgid "Require users to be manually approved by an admin before signing in" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :account_field_name_length" msgid "An account field name maximum length. Default: 512." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :account_field_value_length" msgid "An account field value maximum length. Default: 2048." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :allow_relay" msgid "Permits remote instances to subscribe to all public posts of your instance. (Important!) This may increase the visibility of your instance." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :allowed_post_formats" msgid "MIME-type list of formats allowed to be posted (transformed into HTML)" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :attachment_links" msgid "Enable to automatically add attachment link text to statuses" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :autofollowed_nicknames" msgid "Set to nicknames of (local) users that every new user should automatically follow" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :autofollowing_nicknames" msgid "Set to nicknames of (local) users that automatically follows every newly registered user" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :avatar_upload_limit" msgid "File size limit of user's profile avatars" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :background_upload_limit" msgid "File size limit of user's profile backgrounds" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :banner_upload_limit" msgid "File size limit of user's profile banners" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :birthday_required" msgid "Require users to enter their birthday." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :cleanup_attachments" msgid "Enable to remove associated attachments when status is removed.\nThis will not affect duplicates and attachments without status.\nEnabling this will increase load to database when deleting statuses on larger instances.\n" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :description" msgid "The instance's description. It can be seen in nodeinfo and `/api/v1/instance`" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :email" msgid "Email used to reach an Administrator/Moderator of the instance" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :extended_nickname_format" msgid "Enable to use extended local nicknames format (allows underscores/dashes). This will break federation with older software for theses nicknames." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :external_user_synchronization" msgid "Enabling following/followers counters synchronization for external users" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :federating" msgid "Enable federation with other instances" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :federation_incoming_replies_max_depth" msgid "Max. depth of reply-to and reply activities fetching on incoming federation, to prevent out-of-memory situations while fetching very long threads. If set to `nil`, threads of any depth will be fetched. Lower this value if you experience out-of-memory crashes." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :federation_reachability_timeout_days" msgid "Timeout (in days) of each external federation target being unreachable prior to pausing federating to it" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :healthcheck" msgid "If enabled, system data will be shown on `/api/pleroma/healthcheck`" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :instance_thumbnail" msgid "The instance thumbnail can be any image that represents your instance and is used by some apps or services when they display information about your instance." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :invites_enabled" msgid "Enable user invitations for admins (depends on `registrations_open` being disabled)" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :limit" msgid "Posts character limit (CW/Subject included in the counter)" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :limit_to_local_content" msgid "Limit unauthenticated users to search for local statutes and users only. Default: `:unauthenticated`." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :max_account_fields" msgid "The maximum number of custom fields in the user profile. Default: 10." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :max_endorsed_users" msgid "The maximum number of recommended accounts. 0 will disable the feature." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :max_media_attachments" msgid "Maximum number of post media attachments" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :max_pinned_statuses" msgid "The maximum number of pinned statuses. 0 will disable the feature." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :max_remote_account_fields" msgid "The maximum number of custom fields in the remote user profile. Default: 20." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :max_report_comment_size" msgid "The maximum size of the report comment. Default: 1000." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :multi_factor_authentication" msgid "Multi-factor authentication settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :multi_factor_authentication > :backup_codes" msgid "MFA backup codes settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :multi_factor_authentication > :backup_codes > :length" msgid "Determines the length of backup one-time pass-codes, in characters. Defaults to 16 characters." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :multi_factor_authentication > :backup_codes > :number" msgid "Number of backup codes to generate." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :multi_factor_authentication > :totp" msgid "TOTP settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :multi_factor_authentication > :totp > :digits" msgid "Determines the length of a one-time pass-code, in characters. Defaults to 6 characters." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :multi_factor_authentication > :totp > :period" msgid "A period for which the TOTP code will be valid, in seconds. Defaults to 30 seconds." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :name" msgid "Name of the instance" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :notify_email" msgid "Envelope FROM address for mail sent via Pleroma" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :poll_limits" msgid "A map with poll limits for local polls" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :poll_limits > :max_expiration" msgid "Maximum expiration time (in seconds)" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :poll_limits > :max_option_chars" msgid "Maximum number of characters per option" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :poll_limits > :max_options" msgid "Maximum number of options" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :poll_limits > :min_expiration" msgid "Minimum expiration time (in seconds)" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :profile_directory" msgid "Enable profile directory." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :public" msgid "Makes the client API in authenticated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network. Note: when setting to `false`, please also check `:restrict_unauthenticated` setting." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :quarantined_instances" msgid "List of ActivityPub instances where private (DMs, followers-only) activities will not be sent and the reason for doing so" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :registration_reason_length" msgid "Maximum registration reason length. Default: 500." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :registrations_open" msgid "Enable registrations for anyone. Invitations require this setting to be disabled." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :remote_limit" msgid "Hard character limit beyond which remote posts will be dropped" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :remote_post_retention_days" msgid "The default amount of days to retain remote posts when pruning the database" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :safe_dm_mentions" msgid "If enabled, only mentions at the beginning of a post will be used to address people in direct messages. This is to prevent accidental mentioning of people when talking about them (e.g. \"@admin please keep an eye on @bad_actor\"). Default: disabled" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :show_reactions" msgid "Let favourites and emoji reactions be viewed through the API." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :skip_thread_containment" msgid "Skip filtering out broken threads. Default: enabled." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :static_dir" msgid "Instance static directory" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :upload_limit" msgid "File size limit of uploads (except for avatar, background, banner)" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :user_bio_length" msgid "A user bio maximum length. Default: 5000." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :user_name_length" msgid "A user name maximum length. Default: 100." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instances_favicons > :enabled" msgid "Allow/disallow displaying and getting instances favicons" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:ldap > :base" msgid "LDAP base, e.g. \"dc=example,dc=com\"" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:ldap > :enabled" msgid "Enables LDAP authentication" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:ldap > :host" msgid "LDAP server hostname" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:ldap > :port" msgid "LDAP port, e.g. 389 or 636" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:ldap > :ssl" msgid "Enable to use SSL, usually implies the port 636" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:ldap > :sslopts" msgid "Additional SSL options" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:ldap > :sslopts > :cacertfile" msgid "Path to file with PEM encoded cacerts" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:ldap > :sslopts > :verify" msgid "Type of cert verification" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:ldap > :tls" msgid "Enable to use STARTTLS, usually implies the port 389" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:ldap > :tlsopts" msgid "Additional TLS options" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:ldap > :tlsopts > :cacertfile" msgid "Path to file with PEM encoded cacerts" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:ldap > :tlsopts > :verify" msgid "Type of cert verification" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:ldap > :uid" msgid "LDAP attribute name to authenticate the user, e.g. when \"cn\", the filter will be \"cn=username,base\"" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:majic_pool > :size" msgid "Number of majic workers to start." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:manifest > :background_color" msgid "Describe the background color of the app" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:manifest > :icons" msgid "Describe the icons of the app" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:manifest > :theme_color" msgid "Describe the theme color of the app" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:markup > :scrub_policy" msgid "Module names are shortened (removed leading `Pleroma.HTML.` part), but on adding custom module you need to use full name." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:media_preview_proxy > :enabled" msgid "Enables proxying of remote media preview to the instance's proxy. Requires enabled media proxy." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:media_preview_proxy > :image_quality" msgid "Quality of the output. Ranges from 0 (min quality) to 100 (max quality)." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:media_preview_proxy > :min_content_length" msgid "Min content length (in bytes) to perform preview. Media smaller in size will be served without thumbnailing." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:media_preview_proxy > :thumbnail_max_height" msgid "Max height of preview thumbnail for images (video preview always has original dimensions)." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:media_preview_proxy > :thumbnail_max_width" msgid "Max width of preview thumbnail for images (video preview always has original dimensions)." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:media_proxy > :base_url" msgid "The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:media_proxy > :enabled" msgid "Enables proxying of remote media via the instance's proxy" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:media_proxy > :invalidation > :enabled" msgid "Enables media cache object invalidation." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:media_proxy > :invalidation > :provider" msgid "Module which will be used to purge objects from the cache." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:media_proxy > :proxy_opts" msgid "Internal Pleroma.ReverseProxy settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:media_proxy > :proxy_opts > :max_body_length" msgid "Maximum file size (in bytes) allowed through the Pleroma MediaProxy cache." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:media_proxy > :proxy_opts > :max_read_duration" msgid "Timeout (in milliseconds) of GET request to the remote URI." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:media_proxy > :proxy_opts > :redirect_on_failure" msgid "Redirects the client to the origin server upon encountering HTTP errors.\n\nNote that files larger than Max Body Length will trigger an error. (e.g., Peertube videos)\n\n\n**WARNING:** This setting will allow larger files to be accessed, but exposes the\n\nIP addresses of your users to the other servers, bypassing the MediaProxy.\n" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:media_proxy > :whitelist" msgid "List of hosts with scheme to bypass the MediaProxy" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:modules > :runtime_dir" msgid "A path to custom Elixir modules (such as MRF policies)." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf > :policies" msgid "A list of MRF policies enabled. Module names are shortened (removed leading `Pleroma.Web.ActivityPub.MRF.` part), but on adding custom module you need to use full name." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf > :transparency" msgid "Make the content of your Message Rewrite Facility settings public (via nodeinfo)" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf > :transparency_exclusions" msgid "Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value. You can also provide a reason for excluding these instance names. The instances and reasons won't be publicly disclosed." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_activity_expiration > :days" msgid "Default global expiration time for all local activities (in days)" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_follow_bot > :follower_nickname" msgid "The name of the bot account to use for following newly discovered users." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_hashtag > :federated_timeline_removal" msgid "A list of hashtags which result in message being removed from federated timelines (a.k.a unlisted)." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_hashtag > :reject" msgid "A list of hashtags which result in message being rejected." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_hashtag > :sensitive" msgid "A list of hashtags which result in message being set as sensitive (a.k.a NSFW/R-18)" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_hellthread > :delist_threshold" msgid "Number of mentioned users after which the message gets removed from timelines anddisables notifications. Set to 0 to disable." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_hellthread > :reject_threshold" msgid "Number of mentioned users after which the messaged gets rejected. Set to 0 to disable." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_keyword > :federated_timeline_removal" msgid " A list of patterns which result in message being removed from federated timelines (a.k.a unlisted).\n\n Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.\n" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_keyword > :reject" msgid " A list of patterns which result in message being rejected.\n\n Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.\n" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_keyword > :replace" msgid " **Pattern**: a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.\n\n **Replacement**: a string. Leaving the field empty is permitted.\n" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_mention > :actors" msgid "A list of actors for which any post mentioning them will be dropped" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_object_age > :threshold" msgid "Required age (in seconds) of a post before actions are taken." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_rejectnonpublic > :allow_direct" msgid "Whether to allow direct messages" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_rejectnonpublic > :allow_followersonly" msgid "Whether to allow followers-only posts" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_simple > :accept" msgid "List of instances to only accept activities from (except deletes) and the reason for doing so" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_simple > :avatar_removal" msgid "List of instances to strip avatars from and the reason for doing so" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_simple > :banner_removal" msgid "List of instances to strip banners from and the reason for doing so" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_simple > :federated_timeline_removal" msgid "List of instances to remove from the Federated (aka The Whole Known Network) Timeline and the reason for doing so" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_simple > :followers_only" msgid "Force posts from the given instances to be visible by followers only and the reason for doing so" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_simple > :media_nsfw" msgid "List of instances to tag all media as NSFW (sensitive) from and the reason for doing so" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_simple > :media_removal" msgid "List of instances to strip media attachments from and the reason for doing so" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_simple > :reject" msgid "List of instances to reject activities from (except deletes) and the reason for doing so" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_simple > :reject_deletes" msgid "List of instances to reject deletions from and the reason for doing so" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_simple > :report_removal" msgid "List of instances to reject reports from and the reason for doing so" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_steal_emoji > :hosts" msgid "List of hosts to steal emojis from" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_steal_emoji > :rejected_shortcodes" msgid " A list of patterns or matches to reject shortcodes with.\n\n Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.\n" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_steal_emoji > :size_limit" msgid "File size limit (in bytes), checked before an emoji is saved to the disk" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_subchain > :match_actor" msgid "Matches a series of regular expressions against the actor field" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_vocabulary > :accept" msgid "A list of ActivityStreams terms to accept. If empty, all supported messages are accepted." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_vocabulary > :reject" msgid "A list of ActivityStreams terms to reject. If empty, no messages are rejected." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:oauth2 > :clean_expired_tokens" msgid "Enable a background job to clean expired OAuth tokens. Default: disabled." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:oauth2 > :issue_new_refresh_token" msgid "Keeps old refresh token or generate new refresh token when to obtain an access token" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:oauth2 > :token_expires_in" msgid "The lifetime in seconds of the access token" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:pools > :default" msgid "Settings for default pool." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:pools > :default > :max_waiting" msgid "Maximum number of requests waiting for other requests to finish. After this number is reached, the pool will start returning errrors when a new request is made" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:pools > :default > :recv_timeout" msgid "Timeout for the pool while gun will wait for response" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:pools > :default > :size" msgid "Maximum number of concurrent requests in the pool." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:pools > :federation" msgid "Settings for federation pool." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:pools > :federation > :max_waiting" msgid "Maximum number of requests waiting for other requests to finish. After this number is reached, the pool will start returning errrors when a new request is made" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:pools > :federation > :recv_timeout" msgid "Timeout for the pool while gun will wait for response" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:pools > :federation > :size" msgid "Maximum number of concurrent requests in the pool." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:pools > :media" msgid "Settings for media pool." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:pools > :media > :max_waiting" msgid "Maximum number of requests waiting for other requests to finish. After this number is reached, the pool will start returning errrors when a new request is made" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:pools > :media > :recv_timeout" msgid "Timeout for the pool while gun will wait for response" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:pools > :media > :size" msgid "Maximum number of concurrent requests in the pool." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:pools > :upload" msgid "Settings for upload pool." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:pools > :upload > :max_waiting" msgid "Maximum number of requests waiting for other requests to finish. After this number is reached, the pool will start returning errrors when a new request is made" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:pools > :upload > :recv_timeout" msgid "Timeout for the pool while gun will wait for response" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:pools > :upload > :size" msgid "Maximum number of concurrent requests in the pool." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:populate_hashtags_table > :fault_rate_allowance" msgid "Max accepted rate of objects that failed in the migration. Any value from 0.0 which tolerates no errors to 1.0 which will enable the feature even if hashtags transfer failed for all records." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:populate_hashtags_table > :sleep_interval_ms" msgid "Sleep interval between each chunk of processed records in order to decrease the load on the system (defaults to 0 and should be keep default on most instances)." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:rate_limit > :app_account_creation" msgid "For registering user accounts from the same IP address" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:rate_limit > :authentication" msgid "For authentication create / password check / user existence check requests" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:rate_limit > :relation_id_action" msgid "For actions on relation with a specific user (follow, unfollow)" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:rate_limit > :relations_actions" msgid "For actions on relationships with all users (follow, unfollow)" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:rate_limit > :search" msgid "For the search requests (account & status search etc.)" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:rate_limit > :status_id_action" msgid "For fav / unfav or reblog / unreblog actions on the same status by the same user" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:rate_limit > :statuses_actions" msgid "For create / delete / fav / unfav / reblog / unreblog actions on any statuses" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:rate_limit > :timeline" msgid "For requests to timelines (each timeline has it's own limiter)" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:restrict_unauthenticated > :activities" msgid "Settings for statuses." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:restrict_unauthenticated > :activities > :local" msgid "Disallow view local statuses." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:restrict_unauthenticated > :activities > :remote" msgid "Disallow view remote statuses." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:restrict_unauthenticated > :profiles" msgid "Settings for user profiles." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:restrict_unauthenticated > :profiles > :local" msgid "Disallow view local user profiles." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:restrict_unauthenticated > :profiles > :remote" msgid "Disallow view remote user profiles." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:restrict_unauthenticated > :timelines" msgid "Settings for public and federated timelines." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:restrict_unauthenticated > :timelines > :federated" msgid "Disallow view federated timeline." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:restrict_unauthenticated > :timelines > :local" msgid "Disallow view public timeline." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:rich_media > :enabled" msgid "Enables RichMedia parsing of URLs" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:rich_media > :failure_backoff" msgid "Amount of milliseconds after request failure, during which the request will not be retried." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:rich_media > :ignore_hosts" msgid "List of hosts which will be ignored by the metadata parser" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:rich_media > :ignore_tld" msgid "List TLDs (top-level domains) which will ignore for parse metadata" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:rich_media > :parsers" msgid "List of Rich Media parsers. Module names are shortened (removed leading `Pleroma.Web.RichMedia.Parsers.` part), but on adding custom module you need to use full name." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:rich_media > :ttl_setters" msgid "List of rich media TTL setters. Module names are shortened (removed leading `Pleroma.Web.RichMedia.Parser.` part), but on adding custom module you need to use full name." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:shout > :enabled" msgid "Enables the backend Shoutbox chat feature." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:shout > :limit" msgid "Shout message character limit." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:static_fe > :enabled" msgid "Enables the rendering of static HTML. Default: disabled." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:streamer > :overflow_workers" msgid "Maximum number of workers created if pool is empty" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:streamer > :workers" msgid "Number of workers to send notifications" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:uri_schemes > :valid_schemes" msgid "List of the scheme part that is considered valid to be an URL" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:web_cache_ttl > :activity_pub" msgid "Activity pub routes (except question activities). Default: `nil` (no expiration)." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:web_cache_ttl > :activity_pub_question" msgid "Activity pub routes (question activities). Default: `30_000` (30 seconds)." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:welcome > :chat_message > :enabled" msgid "Enables sending a chat message to newly registered users" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:welcome > :chat_message > :message" msgid "A message that will be sent to newly registered users as a chat message" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:welcome > :chat_message > :sender_nickname" msgid "The nickname of the local user that sends a welcome chat message" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:welcome > :direct_message > :enabled" msgid "Enables sending a direct message to newly registered users" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:welcome > :direct_message > :message" msgid "A message that will be sent to newly registered users" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:welcome > :direct_message > :sender_nickname" msgid "The nickname of the local user that sends a welcome message" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:welcome > :email > :enabled" msgid "Enables sending an email to newly registered users" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:welcome > :email > :html" msgid "HTML content of the welcome email. EEX template with user and instance_name variables can be used." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:welcome > :email > :sender" msgid "Email address and/or nickname that will be used to send the welcome email." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:welcome > :email > :subject" msgid "Subject of the welcome email. EEX template with user and instance_name variables can be used." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:welcome > :email > :text" msgid "Text content of the welcome email. EEX template with user and instance_name variables can be used." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:workers > :retries" msgid "Max retry attempts for failed jobs, per `Oban` queue" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-ConcurrentLimiter > Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy" msgid "Concurrent limits configuration for MediaProxyWarmingPolicy." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-ConcurrentLimiter > Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy > :max_running" msgid "Max running concurrently jobs." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-ConcurrentLimiter > Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy > :max_waiting" msgid "Max waiting jobs." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-ConcurrentLimiter > Pleroma.Web.RichMedia.Helpers" msgid "Concurrent limits configuration for getting RichMedia for activities." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-ConcurrentLimiter > Pleroma.Web.RichMedia.Helpers > :max_running" msgid "Max running concurrently jobs." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-ConcurrentLimiter > Pleroma.Web.RichMedia.Helpers > :max_waiting" msgid "Max waiting jobs." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Oban > :crontab" msgid "Settings for cron background jobs" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Oban > :log" msgid "Logs verbose mode" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Oban > :queues" msgid "Background jobs queues (keys: queues, values: max numbers of concurrent jobs)" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Oban > :queues > :activity_expiration" msgid "Activity expiration queue" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Oban > :queues > :attachments_cleanup" msgid "Attachment deletion queue" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Oban > :queues > :background" msgid "Background queue" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Oban > :queues > :backup" msgid "Backup queue" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Oban > :queues > :federator_incoming" msgid "Incoming federation queue" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Oban > :queues > :federator_outgoing" msgid "Outgoing federation queue" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Oban > :queues > :mailer" msgid "Email sender queue, see Pleroma.Emails.Mailer" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Oban > :queues > :scheduled_activities" msgid "Scheduled activities queue, see Pleroma.ScheduledActivities" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Oban > :queues > :transmogrifier" msgid "Transmogrifier queue" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Oban > :queues > :web_push" msgid "Web push notifications queue" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Captcha > :enabled" msgid "Whether the captcha should be shown on registration" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Captcha > :method" msgid "The method/service to use for captcha" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Captcha > :seconds_valid" msgid "The time in seconds for which the captcha is valid" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Captcha.Kocaptcha > :endpoint" msgid "The kocaptcha endpoint to use" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Emails.Mailer > :adapter" msgid "One of the mail adapters listed in [Swoosh documentation](https://hexdocs.pm/swoosh/Swoosh.html#module-adapters)" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SMTP-:auth" msgid "SMTP AUTH enforcement mode" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SMTP-:password" msgid "SMTP AUTH password" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SMTP-:port" msgid "SMTP port" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SMTP-:relay" msgid "Hostname or IP address" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SMTP-:retries" msgid "SMTP temporary (4xx) error retries" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SMTP-:ssl" msgid "Use Implicit SSL/TLS. e.g. port 465" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SMTP-:tls" msgid "Explicit TLS (STARTTLS) enforcement mode" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SMTP-:username" msgid "SMTP AUTH username" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Emails.NewUsersDigestEmail > :enabled" msgid "Enables new users admin digest email when `true`" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Emails.UserEmail > :logo" msgid "A path to a custom logo. Set it to `nil` to use the default Pleroma logo." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Emails.UserEmail > :styling" msgid "A map with color settings for email templates." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Formatter > :class" msgid "Specify the class to be added to the generated link. Disable to clear." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Formatter > :extra" msgid "Link URLs with rarely used schemes (magnet, ipfs, irc, etc.)" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Formatter > :new_window" msgid "Link URLs will open in a new window/tab." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Formatter > :rel" msgid "Override the rel attribute. Disable to clear." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Formatter > :strip_prefix" msgid "Strip the scheme prefix." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Formatter > :truncate" msgid "Set to a number to truncate URLs longer than the number. Truncated URLs will end in `...`" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Formatter > :validate_tld" msgid "Set to false to disable TLD validation for URLs/emails. Can be set to :no_scheme to validate TLDs only for URLs without a scheme (e.g `example.com` will be validated, but `http://example.loki` won't)" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.ScheduledActivity > :daily_user_limit" msgid "The number of scheduled activities a user is allowed to create in a single day. Default: 25." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.ScheduledActivity > :enabled" msgid "Whether scheduled activities are sent to the job queue to be executed" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.ScheduledActivity > :total_user_limit" msgid "The number of scheduled activities a user is allowed to create in total. Default: 300." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Upload > :base_url" msgid "Base URL for the uploads. Required if you use a CDN or host attachments under a different domain." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Upload > :filename_display_max_length" msgid "Set max length of a filename to display. 0 = no limit. Default: 30" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Upload > :filters" msgid "List of filter modules for uploads. Module names are shortened (removed leading `Pleroma.Upload.Filter.` part), but on adding custom module you need to use full name." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Upload > :link_name" msgid "If enabled, a name parameter will be added to the URL of the upload. For example `https://instance.tld/media/imagehash.png?name=realname.png`." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Upload > :proxy_remote" msgid "Proxy requests to the remote uploader.\n\nUseful if media upload endpoint is not internet accessible.\n" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Upload > :uploader" msgid "Module which will be used for uploads" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Upload.Filter.AnonymizeFilename > :text" msgid "Text to replace filenames in links. If no setting, {random}.extension will be used. You can get the original filename extension by using {extension}, for example custom-file-name.{extension}." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Upload.Filter.Mogrify > :args" msgid "List of actions for the mogrify command. It's possible to add self-written settings as string. For example `auto-orient, strip, {\"resize\", \"3840x1080>\"}` value will be parsed into valid list of the settings." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Uploaders.Local > :uploads" msgid "Path where user's uploads will be saved" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Uploaders.S3 > :bucket" msgid "S3 bucket" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Uploaders.S3 > :bucket_namespace" msgid "S3 bucket namespace" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Uploaders.S3 > :streaming_enabled" msgid "Enable streaming uploads, when enabled the file will be sent to the server in chunks as it's being read. This may be unsupported by some providers, try disabling this if you have upload problems." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Uploaders.S3 > :truncated_namespace" msgid "If you use S3 compatible service such as Digital Ocean Spaces or CDN, set folder name or \"\" etc. For example, when using CDN to S3 virtual host format, set \"\". At this time, write CNAME to CDN in Upload base_url." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.User > :email_blacklist" msgid "List of email domains users may not register with." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.User > :restricted_nicknames" msgid "List of nicknames users may not register with." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.User.Backup > :limit_days" msgid "Limit user to export not more often than once per N days" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.User.Backup > :purge_after_days" msgid "Remove backup achives after N days" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Web.ApiSpec.CastAndValidate > :strict" msgid "Enables strict input validation (useful in development, not recommended in production)" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Http > :headers" msgid "HTTP headers of request" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Http > :method" msgid "HTTP method of request. Default: :purge" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Http > :options" msgid "Request options" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Script > :script_path" msgid "Path to executable script which will purge cached items." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Script > :url_format" msgid "Optional URL format preprocessing. Only required for Apache's htcacheclean." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Web.Metadata > :providers" msgid "List of metadata providers to enable" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Web.Metadata > :unfurl_nsfw" msgid "When enabled NSFW attachments will be shown in previews" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Web.Plugs.RemoteIp > :enabled" msgid "Enable/disable the plug. Default: disabled." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Web.Plugs.RemoteIp > :headers" msgid " A list of strings naming the HTTP headers to use when deriving the true client IP. Default: `[\"x-forwarded-for\"]`.\n" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Web.Plugs.RemoteIp > :proxies" msgid "A list of upstream proxy IP subnets in CIDR notation from which we will parse the content of `headers`. Defaults to `[]`. IPv4 entries without a bitmask will be assumed to be /32 and IPv6 /128." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Web.Plugs.RemoteIp > :reserved" msgid " A list of reserved IP subnets in CIDR notation which should be ignored if found in `headers`. Defaults to `[\"127.0.0.0/8\", \"::1/128\", \"fc00::/7\", \"10.0.0.0/8\", \"172.16.0.0/12\", \"192.168.0.0/16\"]`\n" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Web.Preload > :providers" msgid "List of preload providers to enable" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Workers.PurgeExpiredActivity > :enabled" msgid "Enables expired activities addition & deletion" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Workers.PurgeExpiredActivity > :min_lifetime" msgid "Minimum lifetime for ephemeral activity (in seconds)" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :prometheus-Pleroma.Web.Endpoint.MetricsExporter > :auth" msgid "Enables HTTP Basic Auth for app metrics endpoint." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :prometheus-Pleroma.Web.Endpoint.MetricsExporter > :enabled" msgid "[Pleroma extension] Enables app metrics endpoint." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :prometheus-Pleroma.Web.Endpoint.MetricsExporter > :format" msgid "App metrics endpoint output format." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :prometheus-Pleroma.Web.Endpoint.MetricsExporter > :ip_whitelist" msgid "Restrict access of app metrics endpoint to the specified IP addresses." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :prometheus-Pleroma.Web.Endpoint.MetricsExporter > :path" msgid "App metrics endpoint URI path." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :web_push_encryption-:vapid_details > :private_key" msgid "VAPID private key" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :web_push_encryption-:vapid_details > :public_key" msgid "VAPID public key" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :web_push_encryption-:vapid_details > :subject" msgid "A mailto link for the administrative contact. It's best if this email is not a personal email address, but rather a group email to the instance moderation team." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :cors_plug > :credentials" msgid "Credentials" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :cors_plug > :expose" msgid "Expose" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :cors_plug > :headers" msgid "Headers" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :cors_plug > :max_age" msgid "Max age" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :cors_plug > :methods" msgid "Methods" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 -msgctxt "config label at :esshd > :enabled" -msgid "Enabled" -msgstr "" - #, elixir-autogen, elixir-format -#: lib/pleroma/docs/translator.ex:5 -msgctxt "config label at :esshd > :handler" -msgid "Handler" -msgstr "" - -#, elixir-autogen, elixir-format -#: lib/pleroma/docs/translator.ex:5 -msgctxt "config label at :esshd > :password_authenticator" -msgid "Password authenticator" -msgstr "" - -#, elixir-autogen, elixir-format -#: lib/pleroma/docs/translator.ex:5 -msgctxt "config label at :esshd > :port" -msgid "Port" -msgstr "" - -#, elixir-autogen, elixir-format -#: lib/pleroma/docs/translator.ex:5 -msgctxt "config label at :esshd > :priv_dir" -msgid "Priv dir" -msgstr "" - -#, elixir-autogen, elixir-format -#: lib/pleroma/docs/translator.ex:5 msgctxt "config label at :ex_aws-:s3 > :access_key_id" msgid "Access key" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :ex_aws-:s3 > :host" msgid "Host" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :ex_aws-:s3 > :region" msgid "Region" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :ex_aws-:s3 > :secret_access_key" msgid "Secret access key" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :logger > :backends" msgid "Backends" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :logger-:console > :format" msgid "Format" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :logger-:console > :level" msgid "Level" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :logger-:console > :metadata" msgid "Metadata" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :logger-:ex_syslogger > :format" msgid "Format" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :logger-:ex_syslogger > :ident" msgid "Ident" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :logger-:ex_syslogger > :level" msgid "Level" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :logger-:ex_syslogger > :metadata" msgid "Metadata" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :mime > :types" msgid "Types" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :mime > :types > application/activity+json" msgid "\"application/activity+json\"" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :mime > :types > application/jrd+json" msgid "\"application/jrd+json\"" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :mime > :types > application/ld+json" msgid "\"application/ld+json\"" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :mime > :types > application/xml" msgid "\"application/xml\"" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :mime > :types > application/xrd+xml" msgid "\"application/xrd+xml\"" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma > :admin_token" msgid "Admin token" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma > Pleroma.Web.Auth.Authenticator" msgid "Pleroma.Web.Auth.Authenticator" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:activitypub > :blockers_visible" msgid "Blockers visible" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:activitypub > :follow_handshake_timeout" msgid "Follow handshake timeout" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:activitypub > :note_replies_output_limit" msgid "Note replies output limit" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:activitypub > :outgoing_blocks" msgid "Outgoing blocks" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:activitypub > :sign_object_fetches" msgid "Sign object fetches" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:activitypub > :unfollow_blocked" msgid "Unfollow blocked" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:assets > :default_mascot" msgid "Default mascot" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:assets > :default_user_avatar" msgid "Default user avatar" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:assets > :mascots" msgid "Mascots" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:auth > :auth_template" msgid "Auth template" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:auth > :enforce_oauth_admin_scope_usage" msgid "Enforce OAuth admin scope usage" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:auth > :oauth_consumer_strategies" msgid "OAuth consumer strategies" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:auth > :oauth_consumer_template" msgid "OAuth consumer template" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:connections_pool > :connect_timeout" msgid "Connect timeout" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:connections_pool > :connection_acquisition_retries" msgid "Connection acquisition retries" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:connections_pool > :connection_acquisition_wait" msgid "Connection acquisition wait" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:connections_pool > :max_connections" msgid "Max connections" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:connections_pool > :reclaim_multiplier" msgid "Reclaim multiplier" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:email_notifications > :digest" msgid "Digest" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:email_notifications > :digest > :active" msgid "Enabled" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:email_notifications > :digest > :inactivity_threshold" msgid "Inactivity threshold" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:email_notifications > :digest > :interval" msgid "Interval" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:email_notifications > :digest > :schedule" msgid "Schedule" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:emoji > :default_manifest" msgid "Default manifest" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:emoji > :groups" msgid "Groups" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:emoji > :pack_extensions" msgid "Pack extensions" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:emoji > :shared_pack_cache_seconds_per_file" msgid "Shared pack cache s/file" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:emoji > :shortcode_globs" msgid "Shortcode globs" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:features > :improved_hashtag_timeline" msgid "Improved hashtag timeline" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:feed > :post_title" msgid "Post title" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:feed > :post_title > :max_length" msgid "Max length" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:feed > :post_title > :omission" msgid "Omission" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe" msgid "Pleroma FE" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :alwaysShowSubjectInput" msgid "Always show subject input" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :background" msgid "Background" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :collapseMessageWithSubject" msgid "Collapse message with subject" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :greentext" msgid "Greentext" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :hideFilteredStatuses" msgid "Hide Filtered Statuses" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :hideMutedPosts" msgid "Hide Muted Posts" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :hidePostStats" msgid "Hide post stats" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :hideSitename" msgid "Hide Sitename" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :hideUserStats" msgid "Hide user stats" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :logo" msgid "Logo" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :logoMargin" msgid "Logo margin" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :logoMask" msgid "Logo mask" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :minimalScopesMode" msgid "Minimal scopes mode" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :nsfwCensorImage" msgid "NSFW Censor Image" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :postContentType" msgid "Post Content Type" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :redirectRootLogin" msgid "Redirect root login" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :redirectRootNoLogin" msgid "Redirect root no login" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :scopeCopy" msgid "Scope copy" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :showFeaturesPanel" msgid "Show instance features panel" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :showInstanceSpecificPanel" msgid "Show instance specific panel" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :sidebarRight" msgid "Sidebar on Right" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :subjectLineBehavior" msgid "Subject line behavior" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :theme" msgid "Theme" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontends > :admin" msgid "Admin" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontends > :admin > name" msgid "Name" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontends > :admin > ref" msgid "Reference" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontends > :available" msgid "Available" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontends > :available > build_dir" msgid "Build directory" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontends > :available > build_url" msgid "Build URL" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontends > :available > custom-http-headers" msgid "Custom HTTP headers" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontends > :available > git" msgid "Git Repository URL" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontends > :available > name" msgid "Name" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontends > :available > ref" msgid "Reference" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontends > :primary" msgid "Primary" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontends > :primary > name" msgid "Name" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:frontends > :primary > ref" msgid "Reference" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:gopher > :dstport" msgid "Dstport" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:gopher > :enabled" msgid "Enabled" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:gopher > :ip" msgid "IP" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:gopher > :port" msgid "Port" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:hackney_pools > :federation" msgid "Federation" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:hackney_pools > :federation > :max_connections" msgid "Max connections" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:hackney_pools > :federation > :timeout" msgid "Timeout" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:hackney_pools > :media" msgid "Media" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:hackney_pools > :media > :max_connections" msgid "Max connections" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:hackney_pools > :media > :timeout" msgid "Timeout" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:hackney_pools > :upload" msgid "Upload" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:hackney_pools > :upload > :max_connections" msgid "Max connections" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:hackney_pools > :upload > :timeout" msgid "Timeout" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:http > :adapter" msgid "Adapter" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:http > :adapter > :ssl_options" msgid "SSL Options" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:http > :adapter > :ssl_options > :versions" msgid "Versions" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:http > :proxy_url" msgid "Proxy URL" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:http > :send_user_agent" msgid "Send user agent" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:http > :user_agent" msgid "User agent" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:http_security > :ct_max_age" msgid "CT max age" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:http_security > :enabled" msgid "Enabled" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:http_security > :referrer_policy" msgid "Referrer policy" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:http_security > :report_uri" msgid "Report URI" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:http_security > :sts" msgid "STS" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:http_security > :sts_max_age" msgid "STS max age" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :account_activation_required" msgid "Account activation required" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :account_approval_required" msgid "Account approval required" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :account_field_name_length" msgid "Account field name length" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :account_field_value_length" msgid "Account field value length" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :allow_relay" msgid "Allow relay" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :allowed_post_formats" msgid "Allowed post formats" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :attachment_links" msgid "Attachment links" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :autofollowed_nicknames" msgid "Autofollowed nicknames" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :autofollowing_nicknames" msgid "Autofollowing nicknames" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :avatar_upload_limit" msgid "Avatar upload limit" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :background_upload_limit" msgid "Background upload limit" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :banner_upload_limit" msgid "Banner upload limit" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :birthday_min_age" msgid "Birthday min age" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :birthday_required" msgid "Birthday required" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :cleanup_attachments" msgid "Cleanup attachments" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :description" msgid "Description" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :email" msgid "Admin Email Address" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :extended_nickname_format" msgid "Extended nickname format" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :external_user_synchronization" msgid "External user synchronization" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :federating" msgid "Federating" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :federation_incoming_replies_max_depth" msgid "Fed. incoming replies max depth" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :federation_reachability_timeout_days" msgid "Fed. reachability timeout days" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :healthcheck" msgid "Healthcheck" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :instance_thumbnail" msgid "Instance thumbnail" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :invites_enabled" msgid "Invites enabled" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :limit" msgid "Limit" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :limit_to_local_content" msgid "Limit to local content" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :max_account_fields" msgid "Max account fields" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :max_endorsed_users" msgid "Max endorsed users" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :max_media_attachments" msgid "Max media attachments" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :max_pinned_statuses" msgid "Max pinned statuses" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :max_remote_account_fields" msgid "Max remote account fields" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :max_report_comment_size" msgid "Max report comment size" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :multi_factor_authentication" msgid "Multi factor authentication" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :multi_factor_authentication > :backup_codes" msgid "Backup codes" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :multi_factor_authentication > :backup_codes > :length" msgid "Length" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :multi_factor_authentication > :backup_codes > :number" msgid "Number" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :multi_factor_authentication > :totp" msgid "TOTP settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :multi_factor_authentication > :totp > :digits" msgid "Digits" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :multi_factor_authentication > :totp > :period" msgid "Period" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :name" msgid "Name" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :notify_email" msgid "Sender Email Address" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :poll_limits" msgid "Poll limits" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :poll_limits > :max_expiration" msgid "Max expiration" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :poll_limits > :max_option_chars" msgid "Max option chars" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :poll_limits > :max_options" msgid "Max options" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :poll_limits > :min_expiration" msgid "Min expiration" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :profile_directory" msgid "Profile directory" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :public" msgid "Public" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :quarantined_instances" msgid "Quarantined instances" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :registration_reason_length" msgid "Registration reason length" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :registrations_open" msgid "Registrations open" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :remote_limit" msgid "Remote limit" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :remote_post_retention_days" msgid "Remote post retention days" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :safe_dm_mentions" msgid "Safe DM mentions" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :show_reactions" msgid "Show reactions" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :skip_thread_containment" msgid "Skip thread containment" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :static_dir" msgid "Static dir" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :upload_limit" msgid "Upload limit" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :user_bio_length" msgid "User bio length" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :user_name_length" msgid "User name length" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instances_favicons > :enabled" msgid "Enabled" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:ldap > :base" msgid "Base" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:ldap > :enabled" msgid "Enabled" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:ldap > :host" msgid "Host" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:ldap > :port" msgid "Port" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:ldap > :ssl" msgid "SSL" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:ldap > :sslopts" msgid "SSL options" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:ldap > :sslopts > :cacertfile" msgid "Cacertfile" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:ldap > :sslopts > :verify" msgid "Verify" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:ldap > :tls" msgid "TLS" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:ldap > :tlsopts" msgid "TLS options" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:ldap > :tlsopts > :cacertfile" msgid "Cacertfile" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:ldap > :tlsopts > :verify" msgid "Verify" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:ldap > :uid" msgid "UID" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:majic_pool > :size" msgid "Size" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:manifest > :background_color" msgid "Background color" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:manifest > :icons" msgid "Icons" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:manifest > :theme_color" msgid "Theme color" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:markup > :allow_fonts" msgid "Allow fonts" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:markup > :allow_headings" msgid "Allow headings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:markup > :allow_inline_images" msgid "Allow inline images" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:markup > :allow_tables" msgid "Allow tables" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:markup > :scrub_policy" msgid "Scrub policy" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:media_preview_proxy > :enabled" msgid "Enabled" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:media_preview_proxy > :image_quality" msgid "Image quality" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:media_preview_proxy > :min_content_length" msgid "Min content length" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:media_preview_proxy > :thumbnail_max_height" msgid "Thumbnail max height" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:media_preview_proxy > :thumbnail_max_width" msgid "Thumbnail max width" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:media_proxy > :base_url" msgid "Base URL" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:media_proxy > :enabled" msgid "Enabled" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:media_proxy > :invalidation" msgid "Invalidation" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:media_proxy > :invalidation > :enabled" msgid "Enabled" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:media_proxy > :invalidation > :provider" msgid "Provider" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:media_proxy > :proxy_opts" msgid "Advanced MediaProxy Options" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:media_proxy > :proxy_opts > :max_body_length" msgid "Max body length" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:media_proxy > :proxy_opts > :max_read_duration" msgid "Max read duration" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:media_proxy > :proxy_opts > :redirect_on_failure" msgid "Redirect on failure" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:media_proxy > :whitelist" msgid "Whitelist" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:modules > :runtime_dir" msgid "Runtime dir" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf > :policies" msgid "Policies" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf > :transparency" msgid "MRF transparency" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf > :transparency_exclusions" msgid "MRF transparency exclusions" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_activity_expiration > :days" msgid "Days" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_follow_bot > :follower_nickname" msgid "Follower nickname" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_hashtag > :federated_timeline_removal" msgid "Federated timeline removal" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_hashtag > :reject" msgid "Reject" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_hashtag > :sensitive" msgid "Sensitive" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_hellthread > :delist_threshold" msgid "Delist threshold" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_hellthread > :reject_threshold" msgid "Reject threshold" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_keyword > :federated_timeline_removal" msgid "Federated timeline removal" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_keyword > :reject" msgid "Reject" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_keyword > :replace" msgid "Replace" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_mention > :actors" msgid "Actors" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_normalize_markup > :scrub_policy" msgid "Scrub policy" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_object_age > :actions" msgid "Actions" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_object_age > :threshold" msgid "Threshold" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_rejectnonpublic > :allow_direct" msgid "Allow direct" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_rejectnonpublic > :allow_followersonly" msgid "Allow followers-only" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_simple > :accept" msgid "Accept" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_simple > :avatar_removal" msgid "Avatar removal" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_simple > :banner_removal" msgid "Banner removal" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_simple > :federated_timeline_removal" msgid "Federated timeline removal" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_simple > :followers_only" msgid "Followers only" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_simple > :media_nsfw" msgid "Media NSFW" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_simple > :media_removal" msgid "Media removal" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_simple > :reject" msgid "Reject" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_simple > :reject_deletes" msgid "Reject deletes" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_simple > :report_removal" msgid "Report removal" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_steal_emoji > :hosts" msgid "Hosts" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_steal_emoji > :rejected_shortcodes" msgid "Rejected shortcodes" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_steal_emoji > :size_limit" msgid "Size limit" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_subchain > :match_actor" msgid "Match actor" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_vocabulary > :accept" msgid "Accept" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_vocabulary > :reject" msgid "Reject" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:oauth2 > :clean_expired_tokens" msgid "Clean expired tokens" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:oauth2 > :issue_new_refresh_token" msgid "Issue new refresh token" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:oauth2 > :token_expires_in" msgid "Token expires in" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:pools > :default" msgid "Default" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:pools > :default > :max_waiting" msgid "Max waiting" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:pools > :default > :recv_timeout" msgid "Recv timeout" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:pools > :default > :size" msgid "Size" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:pools > :federation" msgid "Federation" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:pools > :federation > :max_waiting" msgid "Max waiting" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:pools > :federation > :recv_timeout" msgid "Recv timeout" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:pools > :federation > :size" msgid "Size" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:pools > :media" msgid "Media" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:pools > :media > :max_waiting" msgid "Max waiting" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:pools > :media > :recv_timeout" msgid "Recv timeout" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:pools > :media > :size" msgid "Size" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:pools > :upload" msgid "Upload" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:pools > :upload > :max_waiting" msgid "Max waiting" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:pools > :upload > :recv_timeout" msgid "Recv timeout" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:pools > :upload > :size" msgid "Size" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:populate_hashtags_table > :fault_rate_allowance" msgid "Fault rate allowance" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:populate_hashtags_table > :sleep_interval_ms" msgid "Sleep interval ms" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:rate_limit > :app_account_creation" msgid "App account creation" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:rate_limit > :authentication" msgid "Authentication" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:rate_limit > :relation_id_action" msgid "Relation ID action" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:rate_limit > :relations_actions" msgid "Relations actions" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:rate_limit > :search" msgid "Search" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:rate_limit > :status_id_action" msgid "Status ID action" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:rate_limit > :statuses_actions" msgid "Statuses actions" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:rate_limit > :timeline" msgid "Timeline" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:restrict_unauthenticated > :activities" msgid "Activities" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:restrict_unauthenticated > :activities > :local" msgid "Local" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:restrict_unauthenticated > :activities > :remote" msgid "Remote" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:restrict_unauthenticated > :profiles" msgid "Profiles" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:restrict_unauthenticated > :profiles > :local" msgid "Local" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:restrict_unauthenticated > :profiles > :remote" msgid "Remote" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:restrict_unauthenticated > :timelines" msgid "Timelines" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:restrict_unauthenticated > :timelines > :federated" msgid "Federated" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:restrict_unauthenticated > :timelines > :local" msgid "Local" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:rich_media > :enabled" msgid "Enabled" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:rich_media > :failure_backoff" msgid "Failure backoff" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:rich_media > :ignore_hosts" msgid "Ignore hosts" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:rich_media > :ignore_tld" msgid "Ignore TLD" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:rich_media > :parsers" msgid "Parsers" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:rich_media > :ttl_setters" msgid "TTL setters" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:shout > :enabled" msgid "Enabled" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:shout > :limit" msgid "Limit" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:static_fe > :enabled" msgid "Enabled" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:streamer > :overflow_workers" msgid "Overflow workers" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:streamer > :workers" msgid "Workers" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:uri_schemes > :valid_schemes" msgid "Valid schemes" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:user > :deny_follow_blocked" msgid "Deny follow blocked" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:web_cache_ttl > :activity_pub" msgid "Activity pub" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:web_cache_ttl > :activity_pub_question" msgid "Activity pub question" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:welcome > :chat_message" msgid "Chat message" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:welcome > :chat_message > :enabled" msgid "Enabled" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:welcome > :chat_message > :message" msgid "Message" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:welcome > :chat_message > :sender_nickname" msgid "Sender nickname" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:welcome > :direct_message" msgid "Direct message" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:welcome > :direct_message > :enabled" msgid "Enabled" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:welcome > :direct_message > :message" msgid "Message" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:welcome > :direct_message > :sender_nickname" msgid "Sender nickname" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:welcome > :email" msgid "Email" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:welcome > :email > :enabled" msgid "Enabled" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:welcome > :email > :html" msgid "Html" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:welcome > :email > :sender" msgid "Sender" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:welcome > :email > :subject" msgid "Subject" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:welcome > :email > :text" msgid "Text" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:workers > :retries" msgid "Retries" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-ConcurrentLimiter > Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy" msgid "Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-ConcurrentLimiter > Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy > :max_running" msgid "Max running" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-ConcurrentLimiter > Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy > :max_waiting" msgid "Max waiting" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-ConcurrentLimiter > Pleroma.Web.RichMedia.Helpers" msgid "Pleroma.Web.RichMedia.Helpers" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-ConcurrentLimiter > Pleroma.Web.RichMedia.Helpers > :max_running" msgid "Max running" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-ConcurrentLimiter > Pleroma.Web.RichMedia.Helpers > :max_waiting" msgid "Max waiting" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Oban > :crontab" msgid "Crontab" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Oban > :log" msgid "Log" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Oban > :queues" msgid "Queues" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Oban > :queues > :activity_expiration" msgid "Activity expiration" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Oban > :queues > :attachments_cleanup" msgid "Attachments cleanup" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Oban > :queues > :background" msgid "Background" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Oban > :queues > :backup" msgid "Backup" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Oban > :queues > :federator_incoming" msgid "Federator incoming" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Oban > :queues > :federator_outgoing" msgid "Federator outgoing" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Oban > :queues > :mailer" msgid "Mailer" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Oban > :queues > :scheduled_activities" msgid "Scheduled activities" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Oban > :queues > :transmogrifier" msgid "Transmogrifier" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Oban > :queues > :web_push" msgid "Web push" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Captcha > :enabled" msgid "Enabled" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Captcha > :method" msgid "Method" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Captcha > :seconds_valid" msgid "Seconds valid" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Captcha.Kocaptcha > :endpoint" msgid "Endpoint" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > :adapter" msgid "Adapter" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > :enabled" msgid "Mailer Enabled" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.AmazonSES-:access_key" msgid "AWS Access Key" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.AmazonSES-:region" msgid "AWS Region" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.AmazonSES-:secret" msgid "AWS Secret Key" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.Dyn-:api_key" msgid "Dyn API Key" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.Gmail-:access_token" msgid "GMail API Access Token" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.Mailgun-:api_key" msgid "Mailgun API Key" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.Mailgun-:domain" msgid "Domain" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.Mailjet-:api_key" msgid "MailJet Public API Key" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.Mailjet-:secret" msgid "MailJet Private API Key" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.Mandrill-:api_key" msgid "Mandrill API Key" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.Postmark-:api_key" msgid "Postmark API Key" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SMTP-:auth" msgid "AUTH Mode" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SMTP-:password" msgid "Password" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SMTP-:port" msgid "Port" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SMTP-:relay" msgid "Relay" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SMTP-:retries" msgid "Retries" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SMTP-:ssl" msgid "Use SSL" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SMTP-:tls" msgid "STARTTLS Mode" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SMTP-:username" msgid "Username" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.Sendgrid-:api_key" msgid "SendGrid API Key" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.Sendmail-:cmd_args" msgid "Cmd args" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.Sendmail-:cmd_path" msgid "Cmd path" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.Sendmail-:qmail" msgid "Qmail compat mode" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SocketLabs-:api_key" msgid "SocketLabs API Key" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SocketLabs-:server_id" msgid "Server ID" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SparkPost-:api_key" msgid "SparkPost API key" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SparkPost-:endpoint" msgid "Endpoint" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.NewUsersDigestEmail > :enabled" msgid "Enabled" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.UserEmail > :logo" msgid "Logo" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.UserEmail > :styling" msgid "Styling" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.UserEmail > :styling > :background_color" msgid "Background color" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.UserEmail > :styling > :content_background_color" msgid "Content background color" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.UserEmail > :styling > :header_color" msgid "Header color" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.UserEmail > :styling > :link_color" msgid "Link color" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.UserEmail > :styling > :text_color" msgid "Text color" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.UserEmail > :styling > :text_muted_color" msgid "Text muted color" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Formatter > :class" msgid "Class" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Formatter > :extra" msgid "Extra" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Formatter > :new_window" msgid "New window" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Formatter > :rel" msgid "Rel" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Formatter > :strip_prefix" msgid "Strip prefix" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Formatter > :truncate" msgid "Truncate" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Formatter > :validate_tld" msgid "Validate tld" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.ScheduledActivity > :daily_user_limit" msgid "Daily user limit" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.ScheduledActivity > :enabled" msgid "Enabled" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.ScheduledActivity > :total_user_limit" msgid "Total user limit" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Upload > :base_url" msgid "Base URL" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Upload > :filename_display_max_length" msgid "Filename display max length" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Upload > :filters" msgid "Filters" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Upload > :link_name" msgid "Link name" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Upload > :proxy_remote" msgid "Proxy remote" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Upload > :uploader" msgid "Uploader" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Upload.Filter.AnonymizeFilename > :text" msgid "Text" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Upload.Filter.Mogrify > :args" msgid "Args" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Uploaders.Local > :uploads" msgid "Uploads" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Uploaders.S3 > :bucket" msgid "Bucket" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Uploaders.S3 > :bucket_namespace" msgid "Bucket namespace" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Uploaders.S3 > :streaming_enabled" msgid "Streaming enabled" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Uploaders.S3 > :truncated_namespace" msgid "Truncated namespace" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.User > :email_blacklist" msgid "Email blacklist" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.User > :restricted_nicknames" msgid "Restricted nicknames" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.User.Backup > :limit_days" msgid "Limit days" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.User.Backup > :purge_after_days" msgid "Purge after days" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Web.ApiSpec.CastAndValidate > :strict" msgid "Strict" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Http > :headers" msgid "Headers" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Http > :method" msgid "Method" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Http > :options" msgid "Options" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Http > :options > :params" msgid "Params" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Script > :script_path" msgid "Script path" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Script > :url_format" msgid "URL Format" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Web.Metadata > :providers" msgid "Providers" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Web.Metadata > :unfurl_nsfw" msgid "Unfurl NSFW" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Web.Plugs.RemoteIp > :enabled" msgid "Enabled" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Web.Plugs.RemoteIp > :headers" msgid "Headers" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Web.Plugs.RemoteIp > :proxies" msgid "Proxies" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Web.Plugs.RemoteIp > :reserved" msgid "Reserved" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Web.Preload > :providers" msgid "Providers" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Workers.PurgeExpiredActivity > :enabled" msgid "Enabled" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Workers.PurgeExpiredActivity > :min_lifetime" msgid "Min lifetime" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :prometheus-Pleroma.Web.Endpoint.MetricsExporter > :auth" msgid "Auth" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :prometheus-Pleroma.Web.Endpoint.MetricsExporter > :enabled" msgid "Enabled" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :prometheus-Pleroma.Web.Endpoint.MetricsExporter > :format" msgid "Format" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :prometheus-Pleroma.Web.Endpoint.MetricsExporter > :ip_whitelist" msgid "IP Whitelist" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :prometheus-Pleroma.Web.Endpoint.MetricsExporter > :path" msgid "Path" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :web_push_encryption-:vapid_details > :private_key" msgid "Private key" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :web_push_encryption-:vapid_details > :public_key" msgid "Public key" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :web_push_encryption-:vapid_details > :subject" msgid "Subject" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:activitypub > :authorized_fetch_mode" msgid "Require HTTP signatures for AP fetches" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :short_description" msgid "Shorter version of instance description. It can be seen on `/api/v1/instance`" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:activitypub > :authorized_fetch_mode" msgid "Authorized fetch mode" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :short_description" msgid "Short description" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:delete_context_objects" msgid "`delete_context_objects` background migration settings" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:delete_context_objects > :fault_rate_allowance" msgid "Max accepted rate of objects that failed in the migration. Any value from 0.0 which tolerates no errors to 1.0 which will enable the feature even if context object deletion failed for all records." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:delete_context_objects > :sleep_interval_ms" msgid "Sleep interval between each chunk of processed records in order to decrease the load on the system (defaults to 0 and should be keep default on most instances)." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :birthday_min_age" msgid "Minimum required age (in days) for users to create account. Only used if birthday is required." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:delete_context_objects" msgid "Delete context objects" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:delete_context_objects > :fault_rate_allowance" msgid "Fault rate allowance" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:delete_context_objects > :sleep_interval_ms" msgid "Sleep interval ms" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :report_strip_status" msgid "Strip associated statuses in reports to ids when closed/resolved, otherwise keep a copy" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_object_age > :actions" msgid "A list of actions to apply to the post. `:delist` removes the post from public timelines; `:strip_followers` removes followers from the ActivityPub recipient list ensuring they won't be delivered to home timelines, additionally for followers-only it degrades to a direct message; `:reject` rejects the message entirely" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :report_strip_status" msgid "Report strip status" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :admin_privileges" msgid "What extra privileges to allow admins (e.g. updating user credentials, get password reset token, delete users, index and read private statuses and chats)" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :moderator_privileges" msgid "What extra privileges to allow moderators (e.g. updating user credentials, get password reset token, delete users, index and read private statuses and chats)" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :admin_privileges" msgid "Admin privileges" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :moderator_privileges" msgid "Moderator privileges" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :languages" msgid "Languages to be exposed in /api/v1/instance. Should be in the format of BCP47 language codes." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/docs/translator.ex:5 +#, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:instance > :languages" msgid "Languages" msgstr "" diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index 10a62d2a8..4819162b7 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -10,176 +10,176 @@ msgid "" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/render_error.ex:122 +#, elixir-autogen, elixir-format msgid "%{name} - %{count} is not a multiple of %{multiple}." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/render_error.ex:131 +#, elixir-autogen, elixir-format msgid "%{name} - %{value} is larger than exclusive maximum %{max}." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/render_error.ex:140 +#, elixir-autogen, elixir-format msgid "%{name} - %{value} is larger than inclusive maximum %{max}." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/render_error.ex:149 +#, elixir-autogen, elixir-format msgid "%{name} - %{value} is smaller than exclusive minimum %{min}." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/render_error.ex:158 +#, elixir-autogen, elixir-format msgid "%{name} - %{value} is smaller than inclusive minimum %{min}." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/render_error.ex:102 +#, elixir-autogen, elixir-format msgid "%{name} - Array items must be unique." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/render_error.ex:114 +#, elixir-autogen, elixir-format msgid "%{name} - Array length %{length} is larger than maxItems: %{}." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/render_error.ex:106 +#, elixir-autogen, elixir-format msgid "%{name} - Array length %{length} is smaller than minItems: %{min}." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/render_error.ex:166 +#, elixir-autogen, elixir-format msgid "%{name} - Invalid %{type}. Got: %{value}." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/render_error.ex:174 +#, elixir-autogen, elixir-format msgid "%{name} - Invalid format. Expected %{format}." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/render_error.ex:51 +#, elixir-autogen, elixir-format msgid "%{name} - Invalid schema.type. Got: %{type}." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/render_error.ex:178 +#, elixir-autogen, elixir-format msgid "%{name} - Invalid value for enum." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/render_error.ex:95 +#, elixir-autogen, elixir-format msgid "%{name} - String length is larger than maxLength: %{length}." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/render_error.ex:88 +#, elixir-autogen, elixir-format msgid "%{name} - String length is smaller than minLength: %{length}." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/render_error.ex:63 +#, elixir-autogen, elixir-format msgid "%{name} - null value where %{type} expected." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/render_error.ex:60 +#, elixir-autogen, elixir-format msgid "%{name} - null value." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/render_error.ex:182 +#, elixir-autogen, elixir-format msgid "Failed to cast to any schema in %{polymorphic_type}" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/render_error.ex:71 +#, elixir-autogen, elixir-format msgid "Failed to cast value as %{invalid_schema}. Value must be castable using `allOf` schemas listed." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/render_error.ex:84 +#, elixir-autogen, elixir-format msgid "Failed to cast value to one of: %{failed_schemas}." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/render_error.ex:78 +#, elixir-autogen, elixir-format msgid "Failed to cast value using any of: %{failed_schemas}." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/render_error.ex:212 +#, elixir-autogen, elixir-format msgid "Invalid value for header: %{name}." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/render_error.ex:204 +#, elixir-autogen, elixir-format msgid "Missing field: %{name}." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/render_error.ex:208 +#, elixir-autogen, elixir-format msgid "Missing header: %{name}." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/render_error.ex:196 +#, elixir-autogen, elixir-format msgid "No value provided for required discriminator `%{field}`." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/render_error.ex:216 +#, elixir-autogen, elixir-format msgid "Object property count %{property_count} is greater than maxProperties: %{max_properties}." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/render_error.ex:224 +#, elixir-autogen, elixir-format msgid "Object property count %{property_count} is less than minProperties: %{min_properties}" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/static_fe/static_fe/error.html.eex:2 +#, elixir-autogen, elixir-format msgid "Oops" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/render_error.ex:188 +#, elixir-autogen, elixir-format msgid "Unexpected field: %{name}." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/render_error.ex:200 +#, elixir-autogen, elixir-format msgid "Unknown schema: %{name}." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/render_error.ex:192 +#, elixir-autogen, elixir-format msgid "Value used as discriminator for `%{field}` matches no schemas." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/embed/show.html.eex:43 #: lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex:37 +#, elixir-autogen, elixir-format msgid "announces" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/embed/show.html.eex:44 #: lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex:38 +#, elixir-autogen, elixir-format msgid "likes" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/embed/show.html.eex:42 #: lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex:36 +#, elixir-autogen, elixir-format msgid "replies" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/embed/show.html.eex:27 #: lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex:22 +#, elixir-autogen, elixir-format msgid "sensitive media" msgstr "" diff --git a/priv/gettext/errors.pot b/priv/gettext/errors.pot index fa61d509e..d320ee1bd 100644 --- a/priv/gettext/errors.pot +++ b/priv/gettext/errors.pot @@ -89,153 +89,152 @@ msgstr "" msgid "must be equal to %{number}" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/common_api.ex:574 +#, elixir-autogen, elixir-format msgid "Account not found" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/common_api.ex:332 +#, elixir-autogen, elixir-format msgid "Already voted" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/o_auth/o_auth_controller.ex:402 +#, elixir-autogen, elixir-format msgid "Bad request" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/controller_helper.ex:97 #: lib/pleroma/web/controller_helper.ex:103 +#, elixir-autogen, elixir-format msgid "Can't display this activity" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:334 +#, elixir-autogen, elixir-format msgid "Can't find user" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:80 +#, elixir-autogen, elixir-format msgid "Can't get favorites" msgstr "" +#: lib/pleroma/web/common_api/utils.ex:464 #, elixir-autogen, elixir-format -#: lib/pleroma/web/common_api/utils.ex:457 msgid "Cannot post an empty status without attachments" msgstr "" +#: lib/pleroma/web/common_api/utils.ex:452 #, elixir-autogen, elixir-format -#: lib/pleroma/web/common_api/utils.ex:445 msgid "Comment must be up to %{max_size} characters" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/config_db.ex:199 +#, elixir-autogen, elixir-format msgid "Config with params %{params} not found" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/common_api.ex:183 #: lib/pleroma/web/common_api.ex:187 +#, elixir-autogen, elixir-format msgid "Could not delete" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/common_api.ex:233 +#, elixir-autogen, elixir-format msgid "Could not favorite" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/common_api.ex:270 +#, elixir-autogen, elixir-format msgid "Could not unfavorite" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/common_api.ex:218 +#, elixir-autogen, elixir-format msgid "Could not unrepeat" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/common_api.ex:581 #: lib/pleroma/web/common_api.ex:590 +#, elixir-autogen, elixir-format msgid "Could not update state" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:207 +#, elixir-autogen, elixir-format msgid "Error." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/twitter_api/twitter_api.ex:105 +#, elixir-autogen, elixir-format msgid "Invalid CAPTCHA" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:146 #: lib/pleroma/web/o_auth/o_auth_controller.ex:631 +#, elixir-autogen, elixir-format msgid "Invalid credentials" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/plugs/ensure_authenticated_plug.ex:42 +#, elixir-autogen, elixir-format msgid "Invalid credentials." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/common_api.ex:353 +#, elixir-autogen, elixir-format msgid "Invalid indices" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:29 +#, elixir-autogen, elixir-format msgid "Invalid parameters" msgstr "" +#: lib/pleroma/web/common_api/utils.ex:360 #, elixir-autogen, elixir-format -#: lib/pleroma/web/common_api/utils.ex:353 msgid "Invalid password." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:267 +#, elixir-autogen, elixir-format msgid "Invalid request" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/twitter_api/twitter_api.ex:108 +#, elixir-autogen, elixir-format msgid "Kocaptcha service unavailable" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:142 +#, elixir-autogen, elixir-format msgid "Missing parameters" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:171 #: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:197 #: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:239 +#, elixir-autogen, elixir-format msgid "No such permission_group" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:502 #: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:11 #: lib/pleroma/web/feed/tag_controller.ex:16 #: lib/pleroma/web/feed/user_controller.ex:69 #: lib/pleroma/web/o_status/o_status_controller.ex:132 -#: lib/pleroma/web/plugs/uploaded_media.ex:84 +#: lib/pleroma/web/plugs/uploaded_media.ex:104 +#, elixir-autogen, elixir-format msgid "Not found" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/common_api.ex:324 +#, elixir-autogen, elixir-format msgid "Poll's author can't vote" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:499 #: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:20 #: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:39 @@ -243,213 +242,213 @@ msgstr "" #: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:52 #: lib/pleroma/web/mastodon_api/controllers/status_controller.ex:382 #: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:71 +#, elixir-autogen, elixir-format msgid "Record not found" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:35 #: lib/pleroma/web/feed/user_controller.ex:78 #: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:42 #: lib/pleroma/web/o_status/o_status_controller.ex:138 +#, elixir-autogen, elixir-format msgid "Something went wrong" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/common_api/activity_draft.ex:143 +#, elixir-autogen, elixir-format msgid "The message visibility must be direct" msgstr "" +#: lib/pleroma/web/common_api/utils.ex:474 #, elixir-autogen, elixir-format -#: lib/pleroma/web/common_api/utils.ex:467 msgid "The status is over the character limit" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/plugs/ensure_public_or_authenticated_plug.ex:36 +#, elixir-autogen, elixir-format msgid "This resource requires authentication." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/plugs/rate_limiter.ex:208 +#, elixir-autogen, elixir-format msgid "Throttled" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/common_api.ex:354 +#, elixir-autogen, elixir-format msgid "Too many choices" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:268 +#, elixir-autogen, elixir-format msgid "You can't revoke your own admin status." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/o_auth/o_auth_controller.ex:243 #: lib/pleroma/web/o_auth/o_auth_controller.ex:333 +#, elixir-autogen, elixir-format msgid "Your account is currently disabled" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/o_auth/o_auth_controller.ex:205 #: lib/pleroma/web/o_auth/o_auth_controller.ex:356 +#, elixir-autogen, elixir-format msgid "Your login is missing a confirmed e-mail address" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:390 +#, elixir-autogen, elixir-format msgid "can't read inbox of %{nickname} as %{as_nickname}" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:489 +#, elixir-autogen, elixir-format msgid "can't update outbox of %{nickname} as %{as_nickname}" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/common_api.ex:526 +#, elixir-autogen, elixir-format msgid "conversation is already muted" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:508 +#, elixir-autogen, elixir-format msgid "error" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:34 +#, elixir-autogen, elixir-format msgid "mascots can only be images" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:63 +#, elixir-autogen, elixir-format msgid "not found" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/o_auth/o_auth_controller.ex:437 +#, elixir-autogen, elixir-format msgid "Bad OAuth request." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/twitter_api/twitter_api.ex:114 +#, elixir-autogen, elixir-format msgid "CAPTCHA already used" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/twitter_api/twitter_api.ex:111 +#, elixir-autogen, elixir-format msgid "CAPTCHA expired" msgstr "" +#: lib/pleroma/web/plugs/uploaded_media.ex:77 #, elixir-autogen, elixir-format -#: lib/pleroma/web/plugs/uploaded_media.ex:57 msgid "Failed" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/o_auth/o_auth_controller.ex:453 +#, elixir-autogen, elixir-format msgid "Failed to authenticate: %{message}." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/o_auth/o_auth_controller.ex:484 +#, elixir-autogen, elixir-format msgid "Failed to set up user account." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/plugs/o_auth_scopes_plug.ex:37 +#, elixir-autogen, elixir-format msgid "Insufficient permissions: %{permissions}." msgstr "" +#: lib/pleroma/web/plugs/uploaded_media.ex:131 #, elixir-autogen, elixir-format -#: lib/pleroma/web/plugs/uploaded_media.ex:111 msgid "Internal Error" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/o_auth/fallback_controller.ex:22 #: lib/pleroma/web/o_auth/fallback_controller.ex:29 +#, elixir-autogen, elixir-format msgid "Invalid Username/Password" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/twitter_api/twitter_api.ex:117 +#, elixir-autogen, elixir-format msgid "Invalid answer data" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:33 +#, elixir-autogen, elixir-format msgid "Nodeinfo schema version not handled" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/o_auth/o_auth_controller.ex:194 +#, elixir-autogen, elixir-format msgid "This action is outside the authorized scopes" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/o_auth/fallback_controller.ex:14 +#, elixir-autogen, elixir-format msgid "Unknown error, please check the details and try again." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/o_auth/o_auth_controller.ex:136 #: lib/pleroma/web/o_auth/o_auth_controller.ex:180 +#, elixir-autogen, elixir-format msgid "Unlisted redirect_uri." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/o_auth/o_auth_controller.ex:433 +#, elixir-autogen, elixir-format msgid "Unsupported OAuth provider: %{provider}." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/uploaders/uploader.ex:74 +#, elixir-autogen, elixir-format msgid "Uploader callback timeout" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/uploader_controller.ex:23 +#, elixir-autogen, elixir-format msgid "bad request" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/twitter_api/twitter_api.ex:102 +#, elixir-autogen, elixir-format msgid "CAPTCHA Error" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/common_api.ex:282 +#, elixir-autogen, elixir-format msgid "Could not add reaction emoji" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/common_api.ex:293 +#, elixir-autogen, elixir-format msgid "Could not remove reaction emoji" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/twitter_api/twitter_api.ex:128 +#, elixir-autogen, elixir-format msgid "Invalid CAPTCHA (Missing parameter: %{name})" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:96 +#, elixir-autogen, elixir-format msgid "List not found" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:153 +#, elixir-autogen, elixir-format msgid "Missing parameter: %{name}" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/o_auth/o_auth_controller.ex:232 #: lib/pleroma/web/o_auth/o_auth_controller.ex:346 +#, elixir-autogen, elixir-format msgid "Password reset is required" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/tests/auth_test_controller.ex:9 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:6 #: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:6 @@ -529,81 +528,82 @@ msgstr "" #: lib/pleroma/web/twitter_api/controllers/util_controller.ex:6 #: lib/pleroma/web/uploader_controller.ex:6 #: lib/pleroma/web/web_finger/web_finger_controller.ex:6 +#, elixir-autogen, elixir-format msgid "Security violation: OAuth scopes check was neither handled nor explicitly skipped." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/plugs/ensure_authenticated_plug.ex:32 +#, elixir-autogen, elixir-format msgid "Two-factor authentication enabled, you must use a access token." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61 +#, elixir-autogen, elixir-format msgid "Web push subscription is disabled on this Pleroma instance" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:234 +#, elixir-autogen, elixir-format msgid "You can't revoke your own admin/moderator status." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:131 +#, elixir-autogen, elixir-format msgid "authorization required for timeline view" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:24 +#, elixir-autogen, elixir-format msgid "Access denied" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:331 +#, elixir-autogen, elixir-format msgid "This API requires an authenticated user" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/plugs/user_is_admin_plug.ex:21 +#, elixir-autogen, elixir-format msgid "User is not an admin." msgstr "" -#, elixir-format #: lib/pleroma/user/backup.ex:73 +#, elixir-format msgid "Last export was less than a day ago" msgid_plural "Last export was less than %{days} days ago" msgstr[0] "" msgstr[1] "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:421 +#, elixir-autogen, elixir-format msgid "Character limit (%{limit} characters) exceeded, contains %{length} characters" msgstr "" +#: lib/pleroma/web/common_api/utils.ex:489 #, elixir-autogen, elixir-format -#: lib/pleroma/web/common_api/utils.ex:482 msgid "Too many attachments" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/plugs/user_is_staff_plug.ex:20 +#, elixir-autogen, elixir-format msgid "User is not a staff member." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/o_auth/o_auth_controller.ex:366 +#, elixir-autogen, elixir-format msgid "Your account is awaiting approval." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:258 #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:261 #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:264 +#, elixir-autogen, elixir-format msgid "File is too large" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/plugs/ensure_privileged_plug.ex:21 #: lib/pleroma/web/plugs/ensure_privileged_plug.ex:34 #: lib/pleroma/web/plugs/ensure_privileged_plug.ex:41 +#, elixir-autogen, elixir-format msgid "User isn't privileged." msgstr "" diff --git a/priv/gettext/oauth_scopes.pot b/priv/gettext/oauth_scopes.pot index 5f7b425f3..50ad0dd9e 100644 --- a/priv/gettext/oauth_scopes.pot +++ b/priv/gettext/oauth_scopes.pot @@ -10,252 +10,212 @@ msgid "" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "admin" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "admin:read" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "admin:write" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "follow" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 -msgid "push" -msgstr "" - #, elixir-autogen, elixir-format -#: lib/pleroma/web/api_spec/scopes/translator.ex:5 msgid "read" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "read:accounts" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 -msgid "read:backups" -msgstr "" - #, elixir-autogen, elixir-format -#: lib/pleroma/web/api_spec/scopes/translator.ex:5 msgid "read:blocks" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "read:bookmarks" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 -msgid "read:chats" -msgstr "" - #, elixir-autogen, elixir-format -#: lib/pleroma/web/api_spec/scopes/translator.ex:5 msgid "read:favourites" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "read:filters" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "read:follows" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "read:lists" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "read:notifications" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 -msgid "read:reports" -msgstr "" - #, elixir-autogen, elixir-format -#: lib/pleroma/web/api_spec/scopes/translator.ex:5 msgid "read:search" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "read:statuses" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "write" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "write:accounts" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "write:blocks" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "write:bookmarks" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 -msgid "write:chats" -msgstr "" - #, elixir-autogen, elixir-format -#: lib/pleroma/web/api_spec/scopes/translator.ex:5 msgid "write:conversations" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "write:favourites" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "write:filters" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 -msgid "write:follow" -msgstr "" - #, elixir-autogen, elixir-format -#: lib/pleroma/web/api_spec/scopes/translator.ex:5 msgid "write:follows" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "write:lists" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "write:media" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "write:mutes" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "write:notifications" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 -msgid "write:reports" -msgstr "" - #, elixir-autogen, elixir-format -#: lib/pleroma/web/api_spec/scopes/translator.ex:5 msgid "write:statuses" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "admin:read:accounts" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "admin:read:chats" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "admin:read:invites" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "admin:read:media_proxy_caches" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "admin:read:reports" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "admin:read:statuses" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "admin:write:accounts" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "admin:write:chats" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "admin:write:follows" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "admin:write:invites" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "admin:write:media_proxy_caches" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "admin:write:reports" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format msgid "admin:write:statuses" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 -msgid "read:media" -msgstr "" - #, elixir-autogen, elixir-format -#: lib/pleroma/web/api_spec/scopes/translator.ex:5 msgid "read:mutes" msgstr "" diff --git a/priv/gettext/static_pages.pot b/priv/gettext/static_pages.pot index 4c372ac4d..208528239 100644 --- a/priv/gettext/static_pages.pot +++ b/priv/gettext/static_pages.pot @@ -10,553 +10,553 @@ msgid "" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:9 +#, elixir-autogen, elixir-format msgctxt "remote follow authorization button" msgid "Authorize" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:2 +#, elixir-autogen, elixir-format msgctxt "remote follow error" msgid "Error fetching user" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:4 +#, elixir-autogen, elixir-format msgctxt "remote follow header" msgid "Remote follow" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:8 +#, elixir-autogen, elixir-format msgctxt "placeholder text for auth code entry" msgid "Authentication code" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:10 +#, elixir-autogen, elixir-format msgctxt "placeholder text for password entry" msgid "Password" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:8 +#, elixir-autogen, elixir-format msgctxt "placeholder text for username entry" msgid "Username" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:13 +#, elixir-autogen, elixir-format msgctxt "remote follow authorization button for login" msgid "Authorize" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:12 +#, elixir-autogen, elixir-format msgctxt "remote follow authorization button for mfa" msgid "Authorize" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:2 +#, elixir-autogen, elixir-format msgctxt "remote follow error" msgid "Error following account" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:4 +#, elixir-autogen, elixir-format msgctxt "remote follow header, need login" msgid "Log in to follow" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:4 +#, elixir-autogen, elixir-format msgctxt "remote follow mfa header" msgid "Two-factor authentication" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:4 +#, elixir-autogen, elixir-format msgctxt "remote follow success" msgid "Account followed!" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:7 #: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:7 +#, elixir-autogen, elixir-format msgctxt "placeholder text for account id" msgid "Your account ID, e.g. lain@quitter.se" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:8 +#, elixir-autogen, elixir-format msgctxt "remote follow authorization button for following with a remote account" msgid "Follow" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:2 +#, elixir-autogen, elixir-format msgctxt "remote follow error" msgid "Error: %{error}" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:4 +#, elixir-autogen, elixir-format msgctxt "remote follow header" msgid "Remotely follow %{nickname}" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:12 +#, elixir-autogen, elixir-format msgctxt "password reset button" msgid "Reset" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:4 +#, elixir-autogen, elixir-format msgctxt "password reset failed homepage link" msgid "Homepage" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:1 +#, elixir-autogen, elixir-format msgctxt "password reset failed message" msgid "Password reset failed" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:8 +#, elixir-autogen, elixir-format msgctxt "password reset form confirm password prompt" msgid "Confirmation" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:4 +#, elixir-autogen, elixir-format msgctxt "password reset form password prompt" msgid "Password" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex:1 +#, elixir-autogen, elixir-format msgctxt "password reset invalid token message" msgid "Invalid Token" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:2 +#, elixir-autogen, elixir-format msgctxt "password reset successful homepage link" msgid "Homepage" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:1 +#, elixir-autogen, elixir-format msgctxt "password reset successful message" msgid "Password changed!" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/feed/feed/tag.atom.eex:12 #: lib/pleroma/web/templates/feed/feed/tag.rss.eex:8 +#, elixir-autogen, elixir-format msgctxt "tag feed description" msgid "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:1 +#, elixir-autogen, elixir-format msgctxt "oauth authorization exists page title" msgid "Authorization exists" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:32 +#, elixir-autogen, elixir-format msgctxt "oauth authorize approve button" msgid "Approve" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:30 +#, elixir-autogen, elixir-format msgctxt "oauth authorize cancel button" msgid "Cancel" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:23 +#, elixir-autogen, elixir-format msgctxt "oauth authorize message" msgid "Application %{client_name} is requesting access to your account." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:1 +#, elixir-autogen, elixir-format msgctxt "oauth authorized page title" msgid "Successfully authorized" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:1 +#, elixir-autogen, elixir-format msgctxt "oauth external provider page title" msgid "Sign in with external provider" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:13 +#, elixir-autogen, elixir-format msgctxt "oauth external provider sign in button" msgid "Sign in with %{strategy}" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:54 +#, elixir-autogen, elixir-format msgctxt "oauth login button" msgid "Log In" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:51 +#, elixir-autogen, elixir-format msgctxt "oauth login password prompt" msgid "Password" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:47 +#, elixir-autogen, elixir-format msgctxt "oauth login username prompt" msgid "Username" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:39 +#, elixir-autogen, elixir-format msgctxt "oauth register nickname prompt" msgid "Pleroma Handle" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:37 +#, elixir-autogen, elixir-format msgctxt "oauth register nickname unchangeable warning" msgid "Choose carefully! You won't be able to change this later. You will be able to change your display name, though." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:18 +#, elixir-autogen, elixir-format msgctxt "oauth register page email prompt" msgid "Email" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:10 +#, elixir-autogen, elixir-format msgctxt "oauth register page fill form prompt" msgid "If you'd like to register a new account, please provide the details below." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:35 +#, elixir-autogen, elixir-format msgctxt "oauth register page login button" msgid "Proceed as existing user" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:31 +#, elixir-autogen, elixir-format msgctxt "oauth register page login password prompt" msgid "Password" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:24 +#, elixir-autogen, elixir-format msgctxt "oauth register page login prompt" msgid "Alternatively, sign in to connect to existing account." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:27 +#, elixir-autogen, elixir-format msgctxt "oauth register page login username prompt" msgid "Name or email" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:14 +#, elixir-autogen, elixir-format msgctxt "oauth register page nickname prompt" msgid "Nickname" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:22 +#, elixir-autogen, elixir-format msgctxt "oauth register page register button" msgid "Proceed as new user" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:8 +#, elixir-autogen, elixir-format msgctxt "oauth register page title" msgid "Registration Details" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:36 +#, elixir-autogen, elixir-format msgctxt "oauth register page title" msgid "This is the first time you visit! Please enter your Pleroma handle." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex:2 +#, elixir-autogen, elixir-format msgctxt "oauth scopes message" msgid "The following permissions will be granted" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:2 #: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:2 +#, elixir-autogen, elixir-format msgctxt "oauth token code message" msgid "Token code is
%{token}" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:12 +#, elixir-autogen, elixir-format msgctxt "mfa auth code prompt" msgid "Authentication code" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:8 +#, elixir-autogen, elixir-format msgctxt "mfa auth page title" msgid "Two-factor authentication" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:23 +#, elixir-autogen, elixir-format msgctxt "mfa auth page use recovery code link" msgid "Enter a two-factor recovery code" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:20 +#, elixir-autogen, elixir-format msgctxt "mfa auth verify code button" msgid "Verify" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:8 +#, elixir-autogen, elixir-format msgctxt "mfa recover page title" msgid "Two-factor recovery" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:12 +#, elixir-autogen, elixir-format msgctxt "mfa recover recovery code prompt" msgid "Recovery code" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:23 +#, elixir-autogen, elixir-format msgctxt "mfa recover use 2fa code link" msgid "Enter a two-factor code" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:20 +#, elixir-autogen, elixir-format msgctxt "mfa recover verify recovery code button" msgid "Verify" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex:8 +#, elixir-autogen, elixir-format msgctxt "static fe profile page remote follow button" msgid "Remote follow" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/email/digest.html.eex:163 +#, elixir-autogen, elixir-format msgctxt "digest email header line" msgid "Hey %{nickname}, here is what you've missed!" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/email/digest.html.eex:544 +#, elixir-autogen, elixir-format msgctxt "digest email receiver address" msgid "The email address you are subscribed as is %{email}. " msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/email/digest.html.eex:538 +#, elixir-autogen, elixir-format msgctxt "digest email sending reason" msgid "You have received this email because you have signed up to receive digest emails from %{instance} Pleroma instance." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/email/digest.html.eex:547 +#, elixir-autogen, elixir-format msgctxt "digest email unsubscribe action" msgid "To unsubscribe, please go %{here}." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/email/digest.html.eex:547 +#, elixir-autogen, elixir-format msgctxt "digest email unsubscribe action link text" msgid "here" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/mailer/subscription/unsubscribe_failure.html.eex:1 +#, elixir-autogen, elixir-format msgctxt "mailer unsubscribe failed message" msgid "UNSUBSCRIBE FAILURE" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/mailer/subscription/unsubscribe_success.html.eex:1 +#, elixir-autogen, elixir-format msgctxt "mailer unsubscribe successful message" msgid "UNSUBSCRIBE SUCCESSFUL" msgstr "" -#, elixir-format #: lib/pleroma/web/templates/email/digest.html.eex:385 +#, elixir-format msgctxt "new followers count header" msgid "%{count} New Follower" msgid_plural "%{count} New Followers" msgstr[0] "" msgstr[1] "" -#, elixir-autogen, elixir-format #: lib/pleroma/emails/user_email.ex:356 +#, elixir-autogen, elixir-format msgctxt "account archive email body - self-requested" msgid "

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

\n

%{download_url}

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

Awaiting Approval

\n

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

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

Thank you for registering on %{instance_name}

\n

Email confirmation is required to activate the account.

\n

Please click the following link to activate your account.

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

Reset your password at %{instance_name}

\n

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

\n

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

\n

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

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

Hello @%{nickname},

\n

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

\n

No further action is required to activate your account.

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

You are invited to %{instance_name}

\n

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

\n

Click the following link to register: accept invitation.

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

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

\n

%{download_url}

\n" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/twitter_api/controllers/util_controller.ex:123 +#, elixir-autogen, elixir-format msgctxt "remote follow error message - unknown error" msgid "Something went wrong." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/twitter_api/controllers/util_controller.ex:67 +#, elixir-autogen, elixir-format msgctxt "remote follow error message - user not found" msgid "Could not find user" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:8 +#, elixir-autogen, elixir-format msgctxt "status interact authorization button" msgid "Interact" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:2 +#, elixir-autogen, elixir-format msgctxt "status interact error" msgid "Error: %{error}" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/twitter_api/controllers/util_controller.ex:95 +#, elixir-autogen, elixir-format msgctxt "status interact error message - status not found" msgid "Could not find status" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/twitter_api/controllers/util_controller.ex:144 +#, elixir-autogen, elixir-format msgctxt "status interact error message - unknown error" msgid "Something went wrong." msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:4 +#, elixir-autogen, elixir-format msgctxt "status interact header" msgid "Interacting with %{nickname}'s %{status_link}" msgstr "" -#, elixir-autogen, elixir-format #: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:4 +#, elixir-autogen, elixir-format msgctxt "status interact header - status link text" msgid "status" msgstr "" From dd9f8150fce34cc9a30a92a7e3de9560d5146871 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Thu, 22 Jun 2023 21:24:58 +0200 Subject: [PATCH 064/199] Merge Revert "Merge branch 'validate-host' into 'develop'" This reverts commit d998a114e26033e98e87778e5ca659aff91831bf, reversing changes made to da6b4003acad84b0f60ad8da6d08cfe13564b058. --- changelog.d/3896.add | 1 - changelog.d/3909.skip | 0 lib/pleroma/web/plugs/uploaded_media.ex | 22 +--------------- .../web/plugs/uploaded_media_plug_test.exs | 26 ------------------- 4 files changed, 1 insertion(+), 48 deletions(-) delete mode 100644 changelog.d/3896.add create mode 100644 changelog.d/3909.skip diff --git a/changelog.d/3896.add b/changelog.d/3896.add deleted file mode 100644 index e8fde4c8a..000000000 --- a/changelog.d/3896.add +++ /dev/null @@ -1 +0,0 @@ -Validate Host header for Uploads and return a 302 if the base_url has changed diff --git a/changelog.d/3909.skip b/changelog.d/3909.skip new file mode 100644 index 000000000..e69de29bb diff --git a/lib/pleroma/web/plugs/uploaded_media.ex b/lib/pleroma/web/plugs/uploaded_media.ex index 9dd5eb239..8b3bc9acb 100644 --- a/lib/pleroma/web/plugs/uploaded_media.ex +++ b/lib/pleroma/web/plugs/uploaded_media.ex @@ -46,32 +46,12 @@ def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do config = Pleroma.Config.get(Pleroma.Upload) - %{scheme: media_scheme, host: media_host, port: media_port} = - Pleroma.Upload.base_url() |> URI.parse() - - with {:valid_host, true} <- {:valid_host, match?(^media_host, conn.host)}, - uploader <- Keyword.fetch!(config, :uploader), + with uploader <- Keyword.fetch!(config, :uploader), proxy_remote = Keyword.get(config, :proxy_remote, false), {:ok, get_method} <- uploader.get_file(file), false <- media_is_banned(conn, get_method) do get_media(conn, get_method, proxy_remote, opts) else - {:valid_host, false} -> - redirect_url = - %URI{ - scheme: media_scheme, - host: media_host, - port: media_port, - path: conn.request_path, - query: conn.query_string - } - |> URI.to_string() - |> String.trim_trailing("?") - - conn - |> Phoenix.Controller.redirect(external: redirect_url) - |> halt() - _ -> conn |> send_resp(:internal_server_error, dgettext("errors", "Failed")) diff --git a/test/pleroma/web/plugs/uploaded_media_plug_test.exs b/test/pleroma/web/plugs/uploaded_media_plug_test.exs index dbf8ca5ec..8323ff6ab 100644 --- a/test/pleroma/web/plugs/uploaded_media_plug_test.exs +++ b/test/pleroma/web/plugs/uploaded_media_plug_test.exs @@ -40,30 +40,4 @@ test "sends Content-Disposition header when name param is set", %{ &(&1 == {"content-disposition", ~s[inline; filename="\\"cofe\\".gif"]}) ) end - - test "denies access to media if wrong Host", %{ - attachment_url: attachment_url - } do - conn = get(build_conn(), attachment_url) - - assert conn.status == 200 - - new_media_base = "http://media.localhost:8080" - - %{scheme: new_media_scheme, host: new_media_host, port: new_media_port} = - URI.parse(new_media_base) - - clear_config([Pleroma.Upload, :base_url], new_media_base) - - conn = get(build_conn(), attachment_url) - - expected_url = - URI.parse(attachment_url) - |> Map.put(:host, new_media_host) - |> Map.put(:port, new_media_port) - |> Map.put(:scheme, new_media_scheme) - |> URI.to_string() - - assert redirected_to(conn, 302) == expected_url - end end From 8bc51288be21f3b04d74504dc0c7d5a7ac6aa0e3 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Tue, 27 Jun 2023 00:06:58 +0200 Subject: [PATCH 065/199] release_runtime_provider_test: Explicitely use non-existant config file --- changelog.d/testfix-system-config-use.skip | 0 test/pleroma/config/release_runtime_provider_test.exs | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 changelog.d/testfix-system-config-use.skip diff --git a/changelog.d/testfix-system-config-use.skip b/changelog.d/testfix-system-config-use.skip new file mode 100644 index 000000000..e69de29bb diff --git a/test/pleroma/config/release_runtime_provider_test.exs b/test/pleroma/config/release_runtime_provider_test.exs index 4e0d4c838..e6d10b13e 100644 --- a/test/pleroma/config/release_runtime_provider_test.exs +++ b/test/pleroma/config/release_runtime_provider_test.exs @@ -10,7 +10,7 @@ defmodule Pleroma.Config.ReleaseRuntimeProviderTest do describe "load/2" do test "loads release defaults config and warns about non-existent runtime config" do ExUnit.CaptureIO.capture_io(fn -> - merged = ReleaseRuntimeProvider.load([], []) + merged = ReleaseRuntimeProvider.load([], config_path: "/var/empty/config.exs") assert merged == Pleroma.Config.Holder.release_defaults() end) =~ "!!! Config path is not declared! Please ensure it exists and that PLEROMA_CONFIG_PATH is unset or points to an existing file" From aa4c4ab2a0fd2e3ed3abd7ea455463f3f65a5083 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Tue, 27 Jun 2023 00:23:21 +0200 Subject: [PATCH 066/199] mix: 2023-06 deps update this fixes compatibility with Erlang OTP 26 Related: https://git.pleroma.social/pleroma/pleroma/-/issues/2913 --- changelog.d/2023-06-deps-update.skip | 0 mix.exs | 3 ++- mix.lock | 38 ++++++++++++++-------------- 3 files changed, 21 insertions(+), 20 deletions(-) create mode 100644 changelog.d/2023-06-deps-update.skip diff --git a/changelog.d/2023-06-deps-update.skip b/changelog.d/2023-06-deps-update.skip new file mode 100644 index 000000000..e69de29bb diff --git a/mix.exs b/mix.exs index 62ac1c1a0..fa72a85a9 100644 --- a/mix.exs +++ b/mix.exs @@ -150,7 +150,8 @@ defp deps do {:bbcode_pleroma, "~> 0.2.0"}, {:cors_plug, "~> 2.0"}, {:web_push_encryption, "~> 0.3.1"}, - {:swoosh, "~> 1.0"}, + # swoosh 1.11.2+ requires Elixir 1.12+ + {:swoosh, "~> 1.10.0"}, {:phoenix_swoosh, "~> 1.1"}, {:gen_smtp, "~> 0.13"}, {:ex_syslogger, "~> 1.4"}, diff --git a/mix.lock b/mix.lock index b6938f44b..fc1266570 100644 --- a/mix.lock +++ b/mix.lock @@ -15,23 +15,23 @@ "concurrent_limiter": {:hex, :concurrent_limiter, "0.1.1", "43ae1dc23edda1ab03dd66febc739c4ff710d047bb4d735754909f9a474ae01c", [:mix], [{:telemetry, "~> 0.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "53968ff238c0fbb4d7ed76ddb1af0be6f3b2f77909f6796e249e737c505a16eb"}, "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.4", "54acff6cddd88d28dea663cd2e1fe20dd32fcf5f5d3aff7d59031ce44ce39efa", [:rebar3], [], "hexpm", "5c9568ba4308fda2082172737c80c31d991ea83961eb10791f06106a870d0cdc"}, - "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"}, + "covertool": {:hex, :covertool, "2.0.6", "4a291b4e3449025b0595d8f44c8d7635d4f48f033be2ce88d22a329f36f94a91", [:rebar3], [], "hexpm", "5db3fcd82180d8ea4ad857d4d1ab21a8d31b5aee0d60d2f6c0f9e25a411d1e21"}, + "cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"}, "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.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"}, "credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"}, "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.4.3", "3b9aac9f27347ec65b271847e6baeb4443d8474289bd18c1d6f4de655b70c94d", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c127c15b0fa6cfb32eed07465e05da6c815b032508d4ed7c116122871df73c12"}, - "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, + "db_connection": {:hex, :db_connection, "2.5.0", "bb6d4f30d35ded97b29fe80d8bd6f928a1912ca1ff110831edcd238a1973652c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c92d5ba26cd69ead1ff7582dbb860adeedfff39774105a4f1c92cbb654b55aa2"}, + "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, "earmark": {:hex, :earmark, "1.4.22", "ea3e45c6359446dc308be0a64ce82a03260d973de7d0625a762e6d352ff57958", [:mix], [{:earmark_parser, "~> 1.4.23", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "1caf5145665a42fd76d5317286b0c171861fb1c04f86ab103dde76868814fdfb"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.31", "a93921cdc6b9b869f519213d5bc79d9e218ba768d7270d46fdcf1c01bacff9e2", [:mix], [], "hexpm", "317d367ee0335ef037a87e46c91a2269fef6306413f731e8ec11fc45a7efd059"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.32", "fa739a0ecfa34493de19426681b23f6814573faee95dfd4b4aafe15a7b5b32c6", [:mix], [], "hexpm", "b8b0dd77d60373e77a3d7e8afa598f325e49e8663a51bcc2b88ef41838cca755"}, "eblurhash": {:hex, :eblurhash, "1.2.2", "7da4255aaea984b31bb71155f673257353b0e0554d0d30dcf859547e74602582", [:rebar3], [], "hexpm", "8c20ca00904de023a835a9dcb7b7762fed32264c85a80c3cafa85288e405044c"}, - "ecto": {:hex, :ecto, "3.9.5", "9f0aa7ae44a1577b651c98791c6988cd1b69b21bc724e3fd67090b97f7604263", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d4f3115d8cbacdc0bfa4b742865459fb1371d0715515842a1fb17fe31920b74c"}, + "ecto": {:hex, :ecto, "3.10.2", "6b887160281a61aa16843e47735b8a266caa437f80588c3ab80a8a960e6abe37", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6a895778f0d7648a4b34b486af59a1c8009041fbdf2b17f1ac215eb829c60235"}, "ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"}, - "ecto_psql_extras": {:hex, :ecto_psql_extras, "0.7.10", "e14d400930f401ca9f541b3349212634e44027d7f919bbb71224d7ac0d0e8acd", [:mix], [{:ecto_sql, "~> 3.4", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.15.7 or ~> 0.16.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "505e8cd81e4f17c090be0f99e92b1b3f0fd915f98e76965130b8ccfb891e7088"}, - "ecto_sql": {:hex, :ecto_sql, "3.9.2", "34227501abe92dba10d9c3495ab6770e75e79b836d114c41108a4bf2ce200ad5", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9.2", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1eb5eeb4358fdbcd42eac11c1fbd87e3affd7904e639d77903c1358b2abd3f70"}, + "ecto_psql_extras": {:hex, :ecto_psql_extras, "0.7.11", "6e20144c1446dcccfcdb4c142c9d8b7992a90a569b1d5958cbea5458550b25f0", [:mix], [{:ecto_sql, "~> 3.4", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.15.7 or ~> 0.16.0 or ~> 0.17.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "def61f1f92d4f40d51c80bbae2157212d6c0a459eb604be446e47369cbd40b23"}, + "ecto_sql": {:hex, :ecto_sql, "3.10.1", "6ea6b3036a0b0ca94c2a02613fd9f742614b5cfe494c41af2e6571bb034dd94c", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6a25bdbbd695f12c8171eaff0851fa4c8e72eec1e98c7364402dda9ce11c56b"}, "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.6.3", "bc07d53221216838d79e03a8019d0839786703129599e9619f4ab74c8c096eac", [:mix], [], "hexpm", "f5cbd651c5678bcaabdbb7857658ee106b12509cd976c2c2fca99688e1daf716"}, "esbuild": {:hex, :esbuild, "0.5.0", "d5bb08ff049d7880ee3609ed5c4b864bd2f46445ea40b16b4acead724fb4c4a3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "f183a0b332d963c4cfaf585477695ea59eef9a6f2204fdd0efa00e099694ffe5"}, @@ -48,10 +48,10 @@ "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "finch": {:hex, :finch, "0.10.2", "9ad27d68270d879f73f26604bb2e573d40f29bf0e907064a9a337f90a16a0312", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dd8b11b282072cec2ef30852283949c248bd5d2820c88d8acc89402b81db7550"}, "flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "31fc8090fde1acd267c07c36ea7365b8604055f897d3a53dd967658c691bd827"}, - "floki": {:hex, :floki, "0.34.2", "5fad07ef153b3b8ec110b6b155ec3780c4b2c4906297d0b4be1a7162d04a7e02", [:mix], [], "hexpm", "26b9d50f0f01796bc6be611ca815c5e0de034d2128e39cc9702eee6b66a4d1c8"}, + "floki": {:hex, :floki, "0.34.3", "5e2dcaec5d7c228ce5b1d3501502e308b2d79eb655e4191751a1fe491c37feac", [:mix], [], "hexpm", "9577440eea5b97924b4bf3c7ea55f7b8b6dce589f9b28b096cc294a8dc342341"}, "gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"}, "gettext": {:hex, :gettext, "0.22.2", "6bfca374de34ecc913a28ba391ca184d88d77810a3e427afa8454a71a51341ac", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "8a2d389673aea82d7eae387e6a2ccc12660610080ae7beb19452cfdc1ec30f60"}, - "gun": {:hex, :gun, "2.0.0", "2326bc0fd6d9cf628419708270d6fe8b02b8d002cf992e4165a77d997b1defd0", [:make, :rebar3], [{:cowlib, "2.12.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "6613cb7c62930dc8d58263c44dda72f8556346ba88358fc929dcbc5f76d04569"}, + "gun": {:hex, :gun, "2.0.1", "160a9a5394800fcba41bc7e6d421295cf9a7894c2252c0678244948e3336ad73", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "a10bc8d6096b9502205022334f719cc9a08d9adcfbfc0dbee9ef31b56274a20b"}, "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.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.3.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", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, @@ -67,31 +67,31 @@ "majic": {:hex, :majic, "1.0.0", "37e50648db5f5c2ff0c9fb46454d034d11596c03683807b9fb3850676ffdaab3", [:make, :mix], [{:elixir_make, "~> 0.6.1", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "7905858f76650d49695f14ea55cd9aaaee0c6654fa391671d4cf305c275a0a9e"}, "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, "makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"}, - "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"}, "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, "mint": {:hex, :mint, "1.5.1", "8db5239e56738552d85af398798c80648db0e90f343c8469f6c6d8898944fb6f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4a63e1e76a7c3956abd2c72f370a0d0aecddc3976dea5c27eccbecfa5e7d5b1e"}, "mochiweb": {:hex, :mochiweb, "2.18.0", "eb55f1db3e6e960fac4e6db4e2db9ec3602cc9f30b86cd1481d56545c3145d2e", [:rebar3], [], "hexpm"}, - "mock": {:hex, :mock, "0.3.7", "75b3bbf1466d7e486ea2052a73c6e062c6256fb429d6797999ab02fa32f29e03", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "4da49a4609e41fd99b7836945c26f373623ea968cfb6282742bcb94440cf7e5c"}, - "mogrify": {:hex, :mogrify, "0.9.2", "b360984adea7dd6a55f18028e6327973c58de7f548fdb86c9859848aa904d5b0", [:mix], [], "hexpm", "c18d10fd70ca20e2585301616c89f6e4f7159d92efc9cc8ee579e00c886f699d"}, + "mock": {:hex, :mock, "0.3.8", "7046a306b71db2488ef54395eeb74df0a7f335a7caca4a3d3875d1fc81c884dd", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "7fa82364c97617d79bb7d15571193fc0c4fe5afd0c932cef09426b3ee6fe2022"}, + "mogrify": {:hex, :mogrify, "0.9.3", "238c782f00271dace01369ad35ae2e9dd020feee3443b9299ea5ea6bed559841", [:mix], [], "hexpm", "0189b1e1de27455f2b9ae8cf88239cefd23d38de9276eb5add7159aea51731e6"}, "mox": {:hex, :mox, "1.0.2", "dc2057289ac478b35760ba74165b4b3f402f68803dd5aecd3bfd19c183815d64", [:mix], [], "hexpm", "f9864921b3aaf763c8741b5b8e6f908f44566f1e427b2630e89e9a73b981fef2"}, "nimble_options": {:hex, :nimble_options, "0.4.0", "c89babbab52221a24b8d1ff9e7d838be70f0d871be823165c94dd3418eea728f", [:mix], [], "hexpm", "e6701c1af326a11eea9634a3b1c62b475339ace9456c1a23ec3bc9a847bca02d"}, "nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"}, "nimble_pool": {:hex, :nimble_pool, "0.2.6", "91f2f4c357da4c4a0a548286c84a3a28004f68f05609b4534526871a22053cde", [:mix], [], "hexpm", "1c715055095d3f2705c4e236c18b618420a35490da94149ff8b580a2144f653f"}, "nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]}, "oban": {:hex, :oban, "2.13.6", "a0cb1bce3bd393770512231fb5a3695fa19fd3af10d7575bf73f837aee7abf43", [:mix], [{:ecto_sql, "~> 3.6", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c1c5eb16f377b3cbbf2ea14be24d20e3d91285af9d1ac86260b7c2af5464887"}, - "open_api_spex": {:hex, :open_api_spex, "3.16.1", "8137c338129d63060b4b04947c6c57429f86267045c479c703a38a6d3f98dee1", [:mix], [{: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", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "ef6fd778ac121af866b48b75ad4ad256b6ff33949113ea4aa1629af8bfdfdbfb"}, + "open_api_spex": {:hex, :open_api_spex, "3.17.3", "ada8e352eb786050dd639db2439d3316e92f3798eb2abd051f55bb9af825b37e", [:mix], [{: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", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "165db21a85ca83cffc8e7c8890f35b354eddda8255de7404a2848ed652b9f0fe"}, "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, "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.6.16", "e5bdd18c7a06da5852a25c7befb72246de4ddc289182285f8685a40b7b5f5451", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e15989ff34f670a96b95ef6d1d25bad0d9c50df5df40b671d8f4a669e050ac39"}, - "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.2", "b21bd01fdeffcfe2fab49e4942aa938b6d3e89e93a480d4aee58085560a0bc0d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "70242edd4601d50b69273b057ecf7b684644c19ee750989fd555625ae4ce8f5d"}, "phoenix_html": {:hex, :phoenix_html, "3.3.1", "4788757e804a30baac6b3fc9695bf5562465dd3f1da8eb8460ad5b404d9a2178", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bed1906edd4906a15fd7b412b85b05e521e1f67c9a85418c55999277e553d0d3"}, "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.6.5", "1495bb014be12c9a9252eca04b9af54246f6b5c1e4cd1f30210cd00ec540cf8e", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.3", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.17.7", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "ef4fa50dd78364409039c99cf6f98ab5209b4c5f8796c17f4db118324f0db852"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"}, "phoenix_live_view": {:hex, :phoenix_live_view, "0.17.14", "5ec615d4d61bf9d4755f158bd6c80372b715533fe6d6219e12d74fb5eedbeac1", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.0 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "afeb6ba43ce329a6f7fc1c9acdfc6d3039995345f025febb7f409a92f6faebd3"}, - "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, "phoenix_swoosh": {:hex, :phoenix_swoosh, "1.2.0", "a544d83fde4a767efb78f45404a74c9e37b2a9c5ea3339692e65a6966731f935", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "e88d117251e89a16b92222415a6d87b99a96747ddf674fc5c7631de734811dba"}, "phoenix_template": {:hex, :phoenix_template, "1.0.1", "85f79e3ad1b0180abb43f9725973e3b8c2c3354a87245f91431eec60553ed3ef", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "157dc078f6226334c91cb32c1865bf3911686f8bcd6bcff86736f6253e6993ee"}, "phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"}, @@ -101,7 +101,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.16.5", "fcc4035cc90e23933c5d69a9cd686e329469446ef7abba2cf70f08e2c4b69810", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {: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", "edead639dc6e882618c01d8fc891214c481ab9a3788dfe38dd5e37fd1d5fb2e8"}, + "postgrex": {:hex, :postgrex, "0.17.1", "01c29fd1205940ee55f7addb8f1dc25618ca63a8817e56fac4f6846fc2cddcbe", [: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", "14b057b488e73be2beee508fb1955d8db90d6485c6466428fe9ccf1d6692a555"}, "pot": {:hex, :pot, "1.0.2", "13abb849139fdc04ab8154986abbcb63bdee5de6ed2ba7e1713527e33df923dd", [:rebar3], [], "hexpm", "78fe127f5a4f5f919d6ea5a2a671827bd53eb9d37e5b4128c0ad3df99856c2e0"}, "prom_ex": {:hex, :prom_ex, "1.7.1", "39331ee3fe6f9a8587d8208bf9274a253bb80281700e127dd18786cda5e08c37", [: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.10.2", [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]}, {: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.1", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.1", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.0.2", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.0.0", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "4c978872b88a929833925a0f4d0561824804c671fdd04581e765509ed0a6ed08"}, "prometheus": {:hex, :prometheus, "4.10.0", "792adbf0130ff61b5fa8826f013772af24b6e57b984445c8d602c8a0355704a1", [:mix, :rebar3], [{:quantile_estimator, "~> 0.2.1", [hex: :quantile_estimator, repo: "hexpm", optional: false]}], "hexpm", "2a99bb6dce85e238c7236fde6b0064f9834dc420ddbd962aac4ea2a3c3d59384"}, @@ -115,10 +115,10 @@ "recon": {:hex, :recon, "2.5.3", "739107b9050ea683c30e96de050bc59248fd27ec147696f79a8797ff9fa17153", [:mix, :rebar3], [], "hexpm", "6c6683f46fd4a1dfd98404b9f78dcabc7fcd8826613a89dcb984727a8c3099d7"}, "remote_ip": {:git, "https://git.pleroma.social/pleroma/remote_ip.git", "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8", [ref: "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8"]}, "sleeplocks": {:hex, :sleeplocks, "1.1.2", "d45aa1c5513da48c888715e3381211c859af34bee9b8290490e10c90bb6ff0ca", [:rebar3], [], "hexpm", "9fe5d048c5b781d6305c1a3a0f40bb3dfc06f49bf40571f3d2d0c57eaa7f59a5"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, "sweet_xml": {:hex, :sweet_xml, "0.7.3", "debb256781c75ff6a8c5cbf7981146312b66f044a2898f453709a53e5031b45b", [:mix], [], "hexpm", "e110c867a1b3fe74bfc7dd9893aa851f0eed5518d0d7cad76d7baafd30e4f5ba"}, - "swoosh": {:hex, :swoosh, "1.9.1", "0a5d7bf9954eb41d7e55525bc0940379982b090abbaef67cd8e1fd2ed7f8ca1a", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "76dffff3ffcab80f249d5937a592eaef7cc49ac6f4cdd27e622868326ed6371e"}, + "swoosh": {:hex, :swoosh, "1.10.3", "32f1531ee3fe4e82da8175c597bf3692938f8152eb981e0cbf57107b6c5924c1", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8b7167d93047bac6e1a1c367bf7d899cf2e4fea0592ee04a70673548ef6091b9"}, "syslog": {:hex, :syslog, "1.1.0", "6419a232bea84f07b56dc575225007ffe34d9fdc91abe6f1b2f254fd71d8efc2", [:rebar3], [], "hexpm", "4c6a41373c7e20587be33ef841d3de6f3beba08519809329ecc4d27b15b659e1"}, "table_rex": {:hex, :table_rex, "3.1.1", "0c67164d1714b5e806d5067c1e96ff098ba7ae79413cc075973e17c38a587caa", [:mix], [], "hexpm", "678a23aba4d670419c23c17790f9dcd635a4a89022040df7d5d772cb21012490"}, "telemetry": {:hex, :telemetry, "1.0.0", "0f453a102cdf13d506b7c0ab158324c337c41f1cc7548f0bc0e130bbf0ae9452", [:rebar3], [], "hexpm", "73bc09fa59b4a0284efb4624335583c528e07ec9ae76aca96ea0673850aec57a"}, From 9e69adf76fcd8c13581dcd1abc68520f3f0e25d8 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Tue, 27 Jun 2023 01:52:40 +0200 Subject: [PATCH 067/199] mix: Remove override on plug --- mix.exs | 3 --- mix.lock | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/mix.exs b/mix.exs index fa72a85a9..fc4e87ee3 100644 --- a/mix.exs +++ b/mix.exs @@ -194,9 +194,6 @@ defp deps do {:open_api_spex, "~> 3.16"}, {:ecto_psql_extras, "~> 0.6"}, - # indirect dependency version override - {:plug, "~> 1.10.4", override: true}, - ## dev & test {:ex_doc, "~> 0.22", only: :dev, runtime: false}, {:ex_machina, "~> 2.4", only: :test}, diff --git a/mix.lock b/mix.lock index fc1266570..da187ac5c 100644 --- a/mix.lock +++ b/mix.lock @@ -95,7 +95,7 @@ "phoenix_swoosh": {:hex, :phoenix_swoosh, "1.2.0", "a544d83fde4a767efb78f45404a74c9e37b2a9c5ea3339692e65a6966731f935", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "e88d117251e89a16b92222415a6d87b99a96747ddf674fc5c7631de734811dba"}, "phoenix_template": {:hex, :phoenix_template, "1.0.1", "85f79e3ad1b0180abb43f9725973e3b8c2c3354a87245f91431eec60553ed3ef", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "157dc078f6226334c91cb32c1865bf3911686f8bcd6bcff86736f6253e6993ee"}, "phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"}, - "plug": {:hex, :plug, "1.10.4", "41eba7d1a2d671faaf531fa867645bd5a3dce0957d8e2a3f398ccff7d2ef017f", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ad1e233fe73d2eec56616568d260777b67f53148a999dc2d048f4eb9778fe4a0"}, + "plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"}, "plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"}, "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, "plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "79fd4fcf34d110605c26560cbae8f23c603ec4158c08298bd4360fdea90bb5cf"}, From 3a67b8f28728d3071c9a64d9b8c07ab91919676f Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Tue, 27 Jun 2023 02:08:49 +0200 Subject: [PATCH 068/199] endpoint: Use custom Multipart module for dynamic configuration --- lib/pleroma/web/endpoint.ex | 7 ++----- lib/pleroma/web/multipart.ex | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 lib/pleroma/web/multipart.ex diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index d8d40cceb..574f3ab63 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -101,13 +101,10 @@ defmodule Pleroma.Web.Endpoint do plug(Plug.Logger, log: :debug) plug(Plug.Parsers, - parsers: [ - :urlencoded, - {:multipart, length: {Config, :get, [[:instance, :upload_limit]]}}, - :json - ], + parsers: [:urlencoded, Pleroma.Web.Multipart, :json], pass: ["*/*"], json_decoder: Jason, + # Note: this is compile-time only, won't work for database-config length: Config.get([:instance, :upload_limit]), body_reader: {Pleroma.Web.Plugs.DigestPlug, :read_body, []} ) diff --git a/lib/pleroma/web/multipart.ex b/lib/pleroma/web/multipart.ex new file mode 100644 index 000000000..e24bb14c2 --- /dev/null +++ b/lib/pleroma/web/multipart.ex @@ -0,0 +1,22 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +# +defmodule Pleroma.Web.Multipart do + @multipart Plug.Parsers.MULTIPART + + def init(opts) do + opts + end + + def parse(conn, "multipart", subtype, headers, opts) do + length = Pleroma.Config.get([:instance, :upload_limit]) + opts = @multipart.init([length: length] ++ opts) + @multipart.parse(conn, "multipart", subtype, headers, opts) + end + + def parse(conn, _type, _subtype, _headers, _opts) do + {:next, conn} + end +end From d7e049d5e833cebb0a67c771943d33c532f8923a Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Tue, 27 Jun 2023 02:48:50 +0200 Subject: [PATCH 069/199] router: Fix usage of globs warning: doing a prefix match with globs is deprecated, invalid segment "pleroma*path". You can either replace by a single segment match: /foo/bar-:var Or by mixing single segment match with globs: /foo/bar-:var/*rest --- lib/pleroma/web/router.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index c1a690e28..6b9e158a3 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -996,8 +996,8 @@ defmodule Pleroma.Web.Router do scope "/", Pleroma.Web.Fallback do get("/registration/:token", RedirectController, :registration_page) get("/:maybe_nickname_or_id", RedirectController, :redirector_with_meta) - match(:*, "/api/pleroma*path", LegacyPleromaApiRerouterPlug, []) - get("/api*path", RedirectController, :api_not_implemented) + match(:*, "/api/pleroma/*path", LegacyPleromaApiRerouterPlug, []) + get("/api/*path", RedirectController, :api_not_implemented) get("/*path", RedirectController, :redirector_with_preload) options("/*path", RedirectController, :empty) From bf2b4b9400a44bfec02db9210a4ebc6d2d0f8553 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Tue, 27 Jun 2023 21:08:47 +0200 Subject: [PATCH 070/199] README.md: Update packaging state (GURU, AUR) --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 62f8fdc64..2837b6ef8 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,8 @@ If your platform is not supported, or you just want to be able to edit the sourc - [OpenBSD (fi)](https://docs-develop.pleroma.social/backend/installation/openbsd_fi/) ### OS/Distro packages -Currently Pleroma is packaged for [YunoHost](https://yunohost.org) and [NixOS](https://nixos.org). If you want to package Pleroma for any OS/Distros, we can guide you through the process on our [community channels](#community-channels). If you want to change default options in your Pleroma package, please **discuss it with us first**. +Currently Pleroma is packaged for [YunoHost](https://yunohost.org), [NixOS](https://nixos.org), [Gentoo through GURU](https://gentoo.org/) and [Archlinux through AUR](https://aur.archlinux.org/packages/pleroma). You may find more at . +If you want to package Pleroma for any OS/Distros, we can guide you through the process on our [community channels](#community-channels). If you want to change default options in your Pleroma package, please **discuss it with us first**. ### Docker While we don’t provide docker files, other people have written very good ones. Take a look at or . From 63b9f767824b47d52e615d14353d5b9a7e71929a Mon Sep 17 00:00:00 2001 From: tusooa Date: Sat, 1 Jul 2023 23:25:04 -0400 Subject: [PATCH 071/199] Force the use of amd64 runners for jobs using ci-base --- .gitlab-ci.yml | 28 +++++++++++++++++++++++----- changelog.d/amd64-runner.skip | 0 2 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 changelog.d/amd64-runner.skip diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8daa9f434..148cbe405 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -65,13 +65,21 @@ check-changelog: - "**/*.exs" - "mix.lock" +.using-ci-base: + tags: + - amd64 + build: - extends: .build_changes_policy + extends: + - .build_changes_policy + - .using-ci-base stage: build script: - mix compile --force spec-build: + extends: + - .using-ci-base stage: test rules: - changes: @@ -85,6 +93,8 @@ spec-build: - mix pleroma.openapi_spec spec.json benchmark: + extends: + - .using-ci-base stage: benchmark when: manual variables: @@ -99,7 +109,9 @@ benchmark: - mix pleroma.load_testing unit-testing: - extends: .build_changes_policy + extends: + - .build_changes_policy + - .using-ci-base stage: test cache: &testing_cache_policy <<: *global_cache_policy @@ -121,7 +133,9 @@ unit-testing: path: coverage.xml unit-testing-erratic: - extends: .build_changes_policy + extends: + - .build_changes_policy + - .using-ci-base stage: test retry: 2 allow_failure: true @@ -155,7 +169,9 @@ unit-testing-erratic: # - mix test --trace --only federated unit-testing-rum: - extends: .build_changes_policy + extends: + - .build_changes_policy + - .using-ci-base stage: test cache: *testing_cache_policy services: @@ -186,7 +202,9 @@ lint: - mix format --check-formatted analysis: - extends: .build_changes_policy + extends: + - .build_changes_policy + - .using-ci-base stage: test cache: *testing_cache_policy script: diff --git a/changelog.d/amd64-runner.skip b/changelog.d/amd64-runner.skip new file mode 100644 index 000000000..e69de29bb From a1621839cc31a92e346cbd6065c4db6a8ebeb5a9 Mon Sep 17 00:00:00 2001 From: tusooa Date: Sun, 2 Jul 2023 11:03:09 -0400 Subject: [PATCH 072/199] Fix user fetch completely broken if featured collection is not in a supported form --- .../featured-collection-shouldnt-break-user-fetch.fix | 1 + lib/pleroma/web/activity_pub/activity_pub.ex | 5 +++++ test/pleroma/web/activity_pub/activity_pub_test.exs | 8 ++++++++ 3 files changed, 14 insertions(+) create mode 100644 changelog.d/featured-collection-shouldnt-break-user-fetch.fix diff --git a/changelog.d/featured-collection-shouldnt-break-user-fetch.fix b/changelog.d/featured-collection-shouldnt-break-user-fetch.fix new file mode 100644 index 000000000..e8ce288cc --- /dev/null +++ b/changelog.d/featured-collection-shouldnt-break-user-fetch.fix @@ -0,0 +1 @@ +Fix user fetch completely broken if featured collection is not in a supported form diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 1a10aef1d..c93288b79 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1720,6 +1720,11 @@ def pin_data_from_featured_collection(%{ end) end + def pin_data_from_featured_collection(obj) do + Logger.error("Could not parse featured collection #{inspect(obj)}") + %{} + end + def fetch_and_prepare_featured_from_ap_id(nil) do {:ok, %{}} end diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs index 54fc6ef0a..1e8c14043 100644 --- a/test/pleroma/web/activity_pub/activity_pub_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_test.exs @@ -2652,4 +2652,12 @@ test "allow fetching of accounts with an empty string name field" do {:ok, user} = ActivityPub.make_user_from_ap_id("https://princess.cat/users/mewmew") assert user.name == " " end + + test "pin_data_from_featured_collection will ignore unsupported values" do + assert %{} == + ActivityPub.pin_data_from_featured_collection(%{ + "type" => "OrderedCollection", + "first" => "https://social.example/users/alice/collections/featured?page=true" + }) + end end From 6e4de2383f17810a35a32ccdfea8e9de0183dab4 Mon Sep 17 00:00:00 2001 From: tusooa Date: Sun, 2 Jul 2023 11:15:34 -0400 Subject: [PATCH 073/199] Fix handling report from a deactivated user --- .../handle-report-from-deactivated-user.fix | 1 + lib/pleroma/web/common_api.ex | 2 +- .../controllers/report_controller_test.exs | 21 +++++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 changelog.d/handle-report-from-deactivated-user.fix diff --git a/changelog.d/handle-report-from-deactivated-user.fix b/changelog.d/handle-report-from-deactivated-user.fix new file mode 100644 index 000000000..6692d1aa8 --- /dev/null +++ b/changelog.d/handle-report-from-deactivated-user.fix @@ -0,0 +1 @@ +Fix handling report from a deactivated user diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex index 65d08de49..77b3fa5d2 100644 --- a/lib/pleroma/web/common_api.ex +++ b/lib/pleroma/web/common_api.ex @@ -583,7 +583,7 @@ def update_report_state(activity_ids, state) when is_list(activity_ids) do end def update_report_state(activity_id, state) do - with %Activity{} = activity <- Activity.get_by_id(activity_id) do + with %Activity{} = activity <- Activity.get_by_id(activity_id, filter: []) do Utils.update_report_state(activity, state) else nil -> {:error, :not_found} diff --git a/test/pleroma/web/admin_api/controllers/report_controller_test.exs b/test/pleroma/web/admin_api/controllers/report_controller_test.exs index c141cf69d..fb2579a3d 100644 --- a/test/pleroma/web/admin_api/controllers/report_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/report_controller_test.exs @@ -123,6 +123,7 @@ test "returns 404 when report id is invalid", %{conn: conn} do }) %{ + reporter: reporter, id: report_id, second_report_id: second_report_id } @@ -266,6 +267,26 @@ test "updates state of multiple reports", %{ assert ModerationLog.get_log_entry_message(second_log_entry) == "@#{admin.nickname} updated report ##{second_report_id} (on user @#{second_activity.user_actor.nickname}) with 'closed' state" end + + test "works if reporter is deactivated", %{ + conn: conn, + id: id, + reporter: reporter + } do + Pleroma.User.set_activation(reporter, false) + + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/pleroma/admin/reports", %{ + "reports" => [ + %{"state" => "resolved", "id" => id} + ] + }) + |> json_response_and_validate_schema(:no_content) + + activity = Activity.get_by_id_with_user_actor(id) + assert activity.data["state"] == "resolved" + end end describe "GET /api/pleroma/admin/reports" do From 10249d1e42fd809ab75880c891f00ca1f3318f80 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Tue, 4 Jul 2023 03:03:41 +0200 Subject: [PATCH 074/199] CI: Let curl return non-0 on http failure code Otherwise it silently fails --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 148cbe405..81fd3d682 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -230,7 +230,7 @@ docs-deploy: before_script: - apk add curl script: - - curl -X POST -F"token=$DOCS_PIPELINE_TRIGGER" -F'ref=master' -F"variables[BRANCH]=$CI_COMMIT_REF_NAME" https://git.pleroma.social/api/v4/projects/673/trigger/pipeline + - curl --fail-with-body -X POST -F"token=$DOCS_PIPELINE_TRIGGER" -F'ref=master' -F"variables[BRANCH]=$CI_COMMIT_REF_NAME" https://git.pleroma.social/api/v4/projects/673/trigger/pipeline review_app: image: alpine:3.9 stage: deploy @@ -271,7 +271,7 @@ spec-deploy: before_script: - apk add curl script: - - curl -X POST -F"token=$API_DOCS_PIPELINE_TRIGGER" -F'ref=master' -F"variables[BRANCH]=$CI_COMMIT_REF_NAME" -F"variables[JOB_REF]=$CI_JOB_ID" https://git.pleroma.social/api/v4/projects/1130/trigger/pipeline + - curl --fail-with-body -X POST -F"token=$API_DOCS_PIPELINE_TRIGGER" -F'ref=master' -F"variables[BRANCH]=$CI_COMMIT_REF_NAME" -F"variables[JOB_REF]=$CI_JOB_ID" https://git.pleroma.social/api/v4/projects/1130/trigger/pipeline stop_review_app: From 8c3363a5e778899fe137273c2d188ebd3b9224fb Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Tue, 4 Jul 2023 03:23:32 +0200 Subject: [PATCH 075/199] CI: Use CI_JOB_TOKEN for cross-repo pipeline triggers --- .gitlab-ci.yml | 4 ++-- changelog.d/pipeline-triggers.skip | 0 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 changelog.d/pipeline-triggers.skip diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 81fd3d682..cd0561852 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -230,7 +230,7 @@ docs-deploy: before_script: - apk add curl script: - - curl --fail-with-body -X POST -F"token=$DOCS_PIPELINE_TRIGGER" -F'ref=master' -F"variables[BRANCH]=$CI_COMMIT_REF_NAME" https://git.pleroma.social/api/v4/projects/673/trigger/pipeline + - curl --fail-with-body -X POST -F"token=$CI_JOB_TOKEN" -F'ref=master' -F"variables[BRANCH]=$CI_COMMIT_REF_NAME" https://git.pleroma.social/api/v4/projects/673/trigger/pipeline review_app: image: alpine:3.9 stage: deploy @@ -271,7 +271,7 @@ spec-deploy: before_script: - apk add curl script: - - curl --fail-with-body -X POST -F"token=$API_DOCS_PIPELINE_TRIGGER" -F'ref=master' -F"variables[BRANCH]=$CI_COMMIT_REF_NAME" -F"variables[JOB_REF]=$CI_JOB_ID" https://git.pleroma.social/api/v4/projects/1130/trigger/pipeline + - curl --fail-with-body -X POST -F"token=$CI_JOB_TOKEN" -F'ref=master' -F"variables[BRANCH]=$CI_COMMIT_REF_NAME" -F"variables[JOB_REF]=$CI_JOB_ID" https://git.pleroma.social/api/v4/projects/1130/trigger/pipeline stop_review_app: diff --git a/changelog.d/pipeline-triggers.skip b/changelog.d/pipeline-triggers.skip new file mode 100644 index 000000000..e69de29bb From 3d79ceb23a3dc9630d38807cf6e8a62a56f29d3b Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Tue, 4 Jul 2023 03:34:51 +0200 Subject: [PATCH 076/199] Deprecate audio scrobbling --- changelog.d/deprecate-scrobbles.remove | 1 + docs/development/API/pleroma_api.md | 6 ++++++ .../web/api_spec/operations/pleroma_scrobble_operation.ex | 2 ++ 3 files changed, 9 insertions(+) create mode 100644 changelog.d/deprecate-scrobbles.remove diff --git a/changelog.d/deprecate-scrobbles.remove b/changelog.d/deprecate-scrobbles.remove new file mode 100644 index 000000000..c453a9784 --- /dev/null +++ b/changelog.d/deprecate-scrobbles.remove @@ -0,0 +1 @@ +Deprecate Pleroma's audio scrobbling diff --git a/docs/development/API/pleroma_api.md b/docs/development/API/pleroma_api.md index 47fcb7479..bd0e07f9e 100644 --- a/docs/development/API/pleroma_api.md +++ b/docs/development/API/pleroma_api.md @@ -577,6 +577,9 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa 404 if the pack does not exist ## `GET /api/v1/pleroma/accounts/:id/scrobbles` + +Audio scrobbling in Pleroma is **deprecated**. + ### Requests a list of current and recent Listen activities for an account * Method `GET` * Authentication: not required @@ -598,6 +601,9 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa ``` ## `POST /api/v1/pleroma/scrobble` + +Audio scrobbling in Pleroma is **deprecated**. + ### Creates a new Listen activity for an account * Method `POST` * Authentication: required diff --git a/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex index b6273bfcf..ca40da930 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex @@ -22,6 +22,7 @@ def create_operation do summary: "Creates a new Listen activity for an account", security: [%{"oAuth" => ["write"]}], operationId: "PleromaAPI.ScrobbleController.create", + deprecated: true, requestBody: request_body("Parameters", create_request(), requried: true), responses: %{ 200 => Operation.response("Scrobble", "application/json", scrobble()) @@ -34,6 +35,7 @@ def index_operation do tags: ["Scrobbles"], summary: "Requests a list of current and recent Listen activities for an account", operationId: "PleromaAPI.ScrobbleController.index", + deprecated: true, parameters: [ %Reference{"$ref": "#/components/parameters/accountIdOrNickname"} | pagination_params() ], From 0c3709173fe7905c3d7e9d542d3b1007d8f97042 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Tue, 4 Jul 2023 04:19:25 +0200 Subject: [PATCH 077/199] docs: Fix broken links --- changelog.d/gentoo_otp_hotfix.skip | 0 docs/installation/gentoo_en.md | 2 +- docs/installation/gentoo_otp_en.md | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 changelog.d/gentoo_otp_hotfix.skip diff --git a/changelog.d/gentoo_otp_hotfix.skip b/changelog.d/gentoo_otp_hotfix.skip new file mode 100644 index 000000000..e69de29bb diff --git a/docs/installation/gentoo_en.md b/docs/installation/gentoo_en.md index 273e21d4b..87128d6f6 100644 --- a/docs/installation/gentoo_en.md +++ b/docs/installation/gentoo_en.md @@ -2,7 +2,7 @@ {! backend/installation/otp_vs_from_source.include !} -This guide covers a manual from-source installation. To use the gentoo package, please check the [packaged installation guide for gentoo](./gentoo.en.md). +This guide covers a manual from-source installation. To use the gentoo package, please check the [packaged installation guide for gentoo](./gentoo_otp_en.md). ## Installation diff --git a/docs/installation/gentoo_otp_en.md b/docs/installation/gentoo_otp_en.md index 5dcfe0b7b..76968eb38 100644 --- a/docs/installation/gentoo_otp_en.md +++ b/docs/installation/gentoo_otp_en.md @@ -65,7 +65,7 @@ The output from emerging postgresql should give you a command for initializing t ### Install media / graphics packages (optional) -See [Optional software packages needed for specific functionality](docs/installation/optional/media_graphics_packages.md) for details. +See [Optional software packages needed for specific functionality](optional/media_graphics_packages.md) for details. ```shell # emerge --ask media-video/ffmpeg media-gfx/imagemagick media-libs/exiftool From 28ff828caaada329283a37feb3940b543ef260a3 Mon Sep 17 00:00:00 2001 From: tusooa Date: Tue, 28 Feb 2023 10:40:24 -0500 Subject: [PATCH 078/199] Add emoji policy to remove emojis matching certain urls https://git.pleroma.social/pleroma/pleroma/-/issues/2775 --- config/config.exs | 6 + lib/pleroma/web/activity_pub/emoji_policy.ex | 158 ++++++++++++++++++ .../activity_pub/mrf/emoji_policy_test.exs | 123 ++++++++++++++ 3 files changed, 287 insertions(+) create mode 100644 lib/pleroma/web/activity_pub/emoji_policy.ex create mode 100644 test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs diff --git a/config/config.exs b/config/config.exs index a92ab574b..ebcbf8b49 100644 --- a/config/config.exs +++ b/config/config.exs @@ -408,6 +408,12 @@ federated_timeline_removal: [], replace: [] +config :pleroma, :mrf_emoji, + remove_url: [], + remove_shortcode: [], + federated_timeline_removal_url: [], + federated_timeline_removal_shortcode: [] + config :pleroma, :mrf_hashtag, sensitive: ["nsfw"], reject: [], diff --git a/lib/pleroma/web/activity_pub/emoji_policy.ex b/lib/pleroma/web/activity_pub/emoji_policy.ex new file mode 100644 index 000000000..747e720b6 --- /dev/null +++ b/lib/pleroma/web/activity_pub/emoji_policy.ex @@ -0,0 +1,158 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicy do + require Pleroma.Constants + + @moduledoc "Reject or force-unlisted emojis with certain URLs or names" + + @behaviour Pleroma.Web.ActivityPub.MRF.Policy + + defp config_remove_url do + Pleroma.Config.get([:mrf_emoji, :remove_url], []) + end + + @impl Pleroma.Web.ActivityPub.MRF.Policy + def filter(%{"type" => type, "object" => %{} = object} = message) + when type in ["Create", "Update"] do + with object <- process_remove(object, :url, config_remove_url()) do + {:ok, Map.put(message, "object", object)} + end + end + + @impl Pleroma.Web.ActivityPub.MRF.Policy + def filter(%{"type" => type} = object) when type in Pleroma.Constants.actor_types() do + with object <- process_remove(object, :url, config_remove_url()) do + {:ok, object} + end + end + + @impl Pleroma.Web.ActivityPub.MRF.Policy + def filter(message) do + {:ok, message} + end + + defp match_string?(string, pattern) when is_binary(pattern) do + string == pattern + end + + defp match_string?(string, %Regex{} = pattern) do + String.match?(string, pattern) + end + + defp match_any?(string, patterns) do + Enum.any?(patterns, &match_string?(string, &1)) + end + + defp process_remove(object, :url, patterns) do + processed_tag = + Enum.filter( + object["tag"], + fn + %{"type" => "Emoji", "icon" => %{"url" => url}} when is_binary(url) -> + not match_any?(url, patterns) + + _ -> + true + end + ) + + processed_emoji = + if object["emoji"] do + object["emoji"] + |> Enum.reduce(%{}, fn {name, url}, acc -> + if not match_any?(url, patterns) do + Map.put(acc, name, url) + else + acc + end + end) + else + nil + end + + if processed_emoji do + object + |> Map.put("tag", processed_tag) + |> Map.put("emoji", processed_emoji) + else + object + |> Map.put("tag", processed_tag) + end + end + + @impl Pleroma.Web.ActivityPub.MRF.Policy + def describe do + # This horror is needed to convert regex sigils to strings + mrf_emoji = + Pleroma.Config.get(:mrf_emoji, []) + |> Enum.map(fn {key, value} -> + {key, + Enum.map(value, fn + pattern -> + if not is_binary(pattern) do + inspect(pattern) + else + pattern + end + end)} + end) + |> Enum.into(%{}) + + {:ok, %{mrf_emoji: mrf_emoji}} + end + + @impl Pleroma.Web.ActivityPub.MRF.Policy + def config_description do + %{ + key: :mrf_emoji, + related_policy: "Pleroma.Web.ActivityPub.MRF.EmojiPolicy", + label: "MRF Emoji", + description: + "Reject or force-unlisted emojis whose URLs or names match a keyword or [Regex](https://hexdocs.pm/elixir/Regex.html).", + children: [ + %{ + key: :remove_url, + type: {:list, :string}, + description: """ + A list of patterns which result in emoji whose URL matches being removed from the message. This will apply to both statuses and user profiles. + + Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`. + """, + suggestions: ["foo", ~r/foo/iu] + }, + %{ + key: :remove_shortcode, + type: {:list, :string}, + description: """ + A list of patterns which result in emoji whose shortcode matches being removed from the message. This will apply to both statuses and user profiles. + + Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`. + """, + suggestions: ["foo", ~r/foo/iu] + }, + %{ + key: :federated_timeline_removal_url, + type: {:list, :string}, + description: """ + A list of patterns which result in message with emojis whose URLs match being removed from federated timelines (a.k.a unlisted). This will apply only to statuses. + + Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`. + """, + suggestions: ["foo", ~r/foo/iu] + }, + %{ + key: :federated_timeline_removal_shortcode, + type: {:list, :string}, + description: """ + A list of patterns which result in message with emojis whose shortcodes match being removed from federated timelines (a.k.a unlisted). This will apply only to statuses. + + Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`. + """, + suggestions: ["foo", ~r/foo/iu] + } + ] + } + end +end diff --git a/test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs b/test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs new file mode 100644 index 000000000..fe1fb338e --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs @@ -0,0 +1,123 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicyTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.MRF + alias Pleroma.Web.ActivityPub.MRF.EmojiPolicy + + setup do: clear_config(:mrf_emoji) + + setup do + clear_config([:mrf_emoji], %{ + remove_url: [], + remove_shortcode: [], + federated_timeline_removal_url: [], + federated_timeline_removal_shortcode: [] + }) + end + + @emoji_tags [ + %{ + "icon" => %{ + "type" => "Image", + "url" => "https://example.org/emoji/biribiri/mikoto_smile2.png" + }, + "id" => "https://example.org/emoji/biribiri/mikoto_smile2.png", + "name" => ":mikoto_smile2:", + "type" => "Emoji", + "updated" => "1970-01-01T00:00:00Z" + }, + %{ + "icon" => %{ + "type" => "Image", + "url" => "https://example.org/emoji/biribiri/mikoto_smile3.png" + }, + "id" => "https://example.org/emoji/biribiri/mikoto_smile3.png", + "name" => ":mikoto_smile3:", + "type" => "Emoji", + "updated" => "1970-01-01T00:00:00Z" + }, + %{ + "icon" => %{ + "type" => "Image", + "url" => "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png" + }, + "id" => "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png", + "name" => ":nekomimi_girl_emoji_007:", + "type" => "Emoji", + "updated" => "1970-01-01T00:00:00Z" + }, + %{ + "icon" => %{ + "type" => "Image", + "url" => "https://example.org/test.png" + }, + "id" => "https://example.org/test.png", + "name" => ":test:", + "type" => "Emoji", + "updated" => "1970-01-01T00:00:00Z" + } + ] + + @misc_tags [%{"type" => "Placeholder"}] + + @user_data %{ + "type" => "Person", + "id" => "https://example.org/placeholder", + "name" => "lol", + "tag" => @emoji_tags ++ @misc_tags + } + + @status_data %{ + "type" => "Create", + "object" => %{ + "type" => "Note", + "id" => "https://example.org/placeholder", + "content" => "lol", + "tag" => @emoji_tags ++ @misc_tags, + "emoji" => %{ + "mikoto_smile2" => "https://example.org/emoji/biribiri/mikoto_smile2.png", + "mikoto_smile3" => "https://example.org/emoji/biribiri/mikoto_smile3.png", + "nekomimi_girl_emoji_007" => + "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png", + "test" => "https://example.org/test.png" + } + } + } + + describe "remove_url" do + setup do + clear_config([:mrf_emoji, :remove_url], [ + "https://example.org/test.png", + ~r{/biribiri/mikoto_smile[23]\.png}, + "nekomimi_girl_emoji" + ]) + + :ok + end + + test "processes user" do + {:ok, filtered} = MRF.filter_one(EmojiPolicy, @user_data) + + expected_tags = [@emoji_tags |> Enum.at(2)] ++ @misc_tags + + assert %{"tag" => ^expected_tags} = filtered + end + + test "processes status" do + {:ok, filtered} = MRF.filter_one(EmojiPolicy, @status_data) + + expected_tags = [@emoji_tags |> Enum.at(2)] ++ @misc_tags + + expected_emoji = %{ + "nekomimi_girl_emoji_007" => + "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png" + } + + assert %{"object" => %{"tag" => ^expected_tags, "emoji" => ^expected_emoji}} = filtered + end + end +end From 80ce6482f6eb85f4c1d172811affc4a17d24985a Mon Sep 17 00:00:00 2001 From: tusooa Date: Tue, 28 Feb 2023 10:51:56 -0500 Subject: [PATCH 079/199] EmojiPolicy: implement remove by shortcode --- lib/pleroma/web/activity_pub/emoji_policy.ex | 48 ++++++++++++++++--- .../activity_pub/mrf/emoji_policy_test.exs | 33 +++++++++++++ 2 files changed, 75 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/web/activity_pub/emoji_policy.ex b/lib/pleroma/web/activity_pub/emoji_policy.ex index 747e720b6..346b64699 100644 --- a/lib/pleroma/web/activity_pub/emoji_policy.ex +++ b/lib/pleroma/web/activity_pub/emoji_policy.ex @@ -13,17 +13,23 @@ defp config_remove_url do Pleroma.Config.get([:mrf_emoji, :remove_url], []) end + defp config_remove_shortcode do + Pleroma.Config.get([:mrf_emoji, :remove_shortcode], []) + end + @impl Pleroma.Web.ActivityPub.MRF.Policy def filter(%{"type" => type, "object" => %{} = object} = message) when type in ["Create", "Update"] do - with object <- process_remove(object, :url, config_remove_url()) do + with object <- process_remove(object, :url, config_remove_url()), + object <- process_remove(object, :shortcode, config_remove_shortcode()) do {:ok, Map.put(message, "object", object)} end end @impl Pleroma.Web.ActivityPub.MRF.Policy def filter(%{"type" => type} = object) when type in Pleroma.Constants.actor_types() do - with object <- process_remove(object, :url, config_remove_url()) do + with object <- process_remove(object, :url, config_remove_url()), + object <- process_remove(object, :shortcode, config_remove_shortcode()) do {:ok, object} end end @@ -46,12 +52,42 @@ defp match_any?(string, patterns) do end defp process_remove(object, :url, patterns) do + process_remove_impl( + object, + fn + %{"icon" => %{"url" => url}} -> url + _ -> nil + end, + fn {_name, url} -> url end, + patterns + ) + end + + defp process_remove(object, :shortcode, patterns) do + process_remove_impl( + object, + fn + %{"name" => name} when is_binary(name) -> String.trim(name, ":") + _ -> nil + end, + fn {name, _url} -> name end, + patterns + ) + end + + defp process_remove_impl(object, extract_from_tag, extract_from_emoji, patterns) do processed_tag = Enum.filter( object["tag"], fn - %{"type" => "Emoji", "icon" => %{"url" => url}} when is_binary(url) -> - not match_any?(url, patterns) + %{"type" => "Emoji"} = tag -> + str = extract_from_tag.(tag) + + if is_binary(str) do + not match_any?(str, patterns) + else + true + end _ -> true @@ -61,8 +97,8 @@ defp process_remove(object, :url, patterns) do processed_emoji = if object["emoji"] do object["emoji"] - |> Enum.reduce(%{}, fn {name, url}, acc -> - if not match_any?(url, patterns) do + |> Enum.reduce(%{}, fn {name, url} = emoji, acc -> + if not match_any?(extract_from_emoji.(emoji), patterns) do Map.put(acc, name, url) else acc diff --git a/test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs b/test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs index fe1fb338e..6511f6532 100644 --- a/test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs @@ -120,4 +120,37 @@ test "processes status" do assert %{"object" => %{"tag" => ^expected_tags, "emoji" => ^expected_emoji}} = filtered end end + + describe "remove_shortcode" do + setup do + clear_config([:mrf_emoji, :remove_shortcode], [ + "test", + ~r{mikoto_s}, + "nekomimi_girl_emoji" + ]) + + :ok + end + + test "processes user" do + {:ok, filtered} = MRF.filter_one(EmojiPolicy, @user_data) + + expected_tags = [@emoji_tags |> Enum.at(2)] ++ @misc_tags + + assert %{"tag" => ^expected_tags} = filtered + end + + test "processes status" do + {:ok, filtered} = MRF.filter_one(EmojiPolicy, @status_data) + + expected_tags = [@emoji_tags |> Enum.at(2)] ++ @misc_tags + + expected_emoji = %{ + "nekomimi_girl_emoji_007" => + "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png" + } + + assert %{"object" => %{"tag" => ^expected_tags, "emoji" => ^expected_emoji}} = filtered + end + end end From 7eb8abf7bb50e6bf4c6587ea308e1a52d944375a Mon Sep 17 00:00:00 2001 From: tusooa Date: Tue, 28 Feb 2023 11:47:53 -0500 Subject: [PATCH 080/199] EmojiPolicy: Implement delist --- lib/pleroma/web/activity_pub/emoji_policy.ex | 117 ++++++++++-- .../activity_pub/mrf/emoji_policy_test.exs | 180 +++++++++++++++++- 2 files changed, 274 insertions(+), 23 deletions(-) diff --git a/lib/pleroma/web/activity_pub/emoji_policy.ex b/lib/pleroma/web/activity_pub/emoji_policy.ex index 346b64699..df1566ec3 100644 --- a/lib/pleroma/web/activity_pub/emoji_policy.ex +++ b/lib/pleroma/web/activity_pub/emoji_policy.ex @@ -5,6 +5,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicy do require Pleroma.Constants + alias Pleroma.Object.Updater + @moduledoc "Reject or force-unlisted emojis with certain URLs or names" @behaviour Pleroma.Web.ActivityPub.MRF.Policy @@ -17,12 +19,31 @@ defp config_remove_shortcode do Pleroma.Config.get([:mrf_emoji, :remove_shortcode], []) end + defp config_unlist_url do + Pleroma.Config.get([:mrf_emoji, :federated_timeline_removal_url], []) + end + + defp config_unlist_shortcode do + Pleroma.Config.get([:mrf_emoji, :federated_timeline_removal_shortcode], []) + end + + @impl Pleroma.Web.ActivityPub.MRF.Policy + def history_awareness, do: :manual + @impl Pleroma.Web.ActivityPub.MRF.Policy def filter(%{"type" => type, "object" => %{} = object} = message) when type in ["Create", "Update"] do - with object <- process_remove(object, :url, config_remove_url()), - object <- process_remove(object, :shortcode, config_remove_shortcode()) do - {:ok, Map.put(message, "object", object)} + with {:ok, object} <- + Updater.do_with_history(object, fn object -> + {:ok, process_remove(object, :url, config_remove_url())} + end), + {:ok, object} <- + Updater.do_with_history(object, fn object -> + {:ok, process_remove(object, :shortcode, config_remove_shortcode())} + end), + activity <- Map.put(message, "object", object), + activity <- maybe_delist(activity) do + {:ok, activity} end end @@ -51,28 +72,22 @@ defp match_any?(string, patterns) do Enum.any?(patterns, &match_string?(string, &1)) end + defp url_from_tag(%{"icon" => %{"url" => url}}), do: url + defp url_from_tag(_), do: nil + + defp url_from_emoji({_name, url}), do: url + + defp shortcode_from_tag(%{"name" => name}) when is_binary(name), do: String.trim(name, ":") + defp shortcode_from_tag(_), do: nil + + defp shortcode_from_emoji({name, _url}), do: name + defp process_remove(object, :url, patterns) do - process_remove_impl( - object, - fn - %{"icon" => %{"url" => url}} -> url - _ -> nil - end, - fn {_name, url} -> url end, - patterns - ) + process_remove_impl(object, &url_from_tag/1, &url_from_emoji/1, patterns) end defp process_remove(object, :shortcode, patterns) do - process_remove_impl( - object, - fn - %{"name" => name} when is_binary(name) -> String.trim(name, ":") - _ -> nil - end, - fn {name, _url} -> name end, - patterns - ) + process_remove_impl(object, &shortcode_from_tag/1, &shortcode_from_emoji/1, patterns) end defp process_remove_impl(object, extract_from_tag, extract_from_emoji, patterns) do @@ -118,6 +133,66 @@ defp process_remove_impl(object, extract_from_tag, extract_from_emoji, patterns) end end + defp maybe_delist(%{"object" => object, "to" => to, "type" => "Create"} = activity) do + check = fn object -> + if any_emoji_match?(object, &url_from_tag/1, &url_from_emoji/1, config_unlist_url()) or + any_emoji_match?( + object, + &shortcode_from_tag/1, + &shortcode_from_emoji/1, + config_unlist_shortcode() + ) do + {:should_delist, nil} + else + {:ok, %{}} + end + end + + should_delist? = fn object -> + with {:ok, _} <- Pleroma.Object.Updater.do_with_history(object, check) do + false + else + _ -> true + end + end + + if Pleroma.Constants.as_public() in to and should_delist?.(object) do + to = List.delete(to, Pleroma.Constants.as_public()) + cc = [Pleroma.Constants.as_public() | activity["cc"] || []] + + activity + |> Map.put("to", to) + |> Map.put("cc", cc) + else + activity + end + end + + defp maybe_delist(activity), do: activity + + defp any_emoji_match?(object, extract_from_tag, extract_from_emoji, patterns) do + Kernel.||( + Enum.any?( + object["tag"], + fn + %{"type" => "Emoji"} = tag -> + str = extract_from_tag.(tag) + + if is_binary(str) do + match_any?(str, patterns) + else + false + end + + _ -> + false + end + ), + object["emoji"] + |> Enum.any?(fn emoji -> match_any?(extract_from_emoji.(emoji), patterns) end) + ) + end + @impl Pleroma.Web.ActivityPub.MRF.Policy def describe do # This horror is needed to convert regex sigils to strings diff --git a/test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs b/test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs index 6511f6532..1e1b5c578 100644 --- a/test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs @@ -5,6 +5,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicyTest do use Pleroma.DataCase + require Pleroma.Constants + alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.MRF.EmojiPolicy @@ -84,8 +86,27 @@ defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicyTest do "nekomimi_girl_emoji_007" => "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png", "test" => "https://example.org/test.png" - } - } + }, + "to" => ["https://example.org/self", Pleroma.Constants.as_public()], + "cc" => ["https://example.org/someone"] + }, + "to" => ["https://example.org/self", Pleroma.Constants.as_public()], + "cc" => ["https://example.org/someone"] + } + + @status_data_with_history %{ + "type" => "Create", + "object" => + @status_data["object"] + |> Map.merge(%{ + "formerRepresentations" => %{ + "type" => "OrderedCollection", + "orderedItems" => [@status_data["object"] |> Map.put("content", "older")], + "totalItems" => 1 + } + }), + "to" => ["https://example.org/self", Pleroma.Constants.as_public()], + "cc" => ["https://example.org/someone"] } describe "remove_url" do @@ -119,6 +140,49 @@ test "processes status" do assert %{"object" => %{"tag" => ^expected_tags, "emoji" => ^expected_emoji}} = filtered end + + test "processes status with history" do + {:ok, filtered} = MRF.filter_one(EmojiPolicy, @status_data_with_history) + + expected_tags = [@emoji_tags |> Enum.at(2)] ++ @misc_tags + + expected_emoji = %{ + "nekomimi_girl_emoji_007" => + "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png" + } + + assert %{ + "object" => %{ + "tag" => ^expected_tags, + "emoji" => ^expected_emoji, + "formerRepresentations" => %{"orderedItems" => [item]} + } + } = filtered + + assert %{"tag" => ^expected_tags, "emoji" => ^expected_emoji} = item + end + + test "processes updates" do + {:ok, filtered} = + MRF.filter_one(EmojiPolicy, @status_data_with_history |> Map.put("type", "Update")) + + expected_tags = [@emoji_tags |> Enum.at(2)] ++ @misc_tags + + expected_emoji = %{ + "nekomimi_girl_emoji_007" => + "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png" + } + + assert %{ + "object" => %{ + "tag" => ^expected_tags, + "emoji" => ^expected_emoji, + "formerRepresentations" => %{"orderedItems" => [item]} + } + } = filtered + + assert %{"tag" => ^expected_tags, "emoji" => ^expected_emoji} = item + end end describe "remove_shortcode" do @@ -152,5 +216,117 @@ test "processes status" do assert %{"object" => %{"tag" => ^expected_tags, "emoji" => ^expected_emoji}} = filtered end + + test "processes status with history" do + {:ok, filtered} = MRF.filter_one(EmojiPolicy, @status_data_with_history) + + expected_tags = [@emoji_tags |> Enum.at(2)] ++ @misc_tags + + expected_emoji = %{ + "nekomimi_girl_emoji_007" => + "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png" + } + + assert %{ + "object" => %{ + "tag" => ^expected_tags, + "emoji" => ^expected_emoji, + "formerRepresentations" => %{"orderedItems" => [item]} + } + } = filtered + + assert %{"tag" => ^expected_tags, "emoji" => ^expected_emoji} = item + end + + test "processes updates" do + {:ok, filtered} = + MRF.filter_one(EmojiPolicy, @status_data_with_history |> Map.put("type", "Update")) + + expected_tags = [@emoji_tags |> Enum.at(2)] ++ @misc_tags + + expected_emoji = %{ + "nekomimi_girl_emoji_007" => + "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png" + } + + assert %{ + "object" => %{ + "tag" => ^expected_tags, + "emoji" => ^expected_emoji, + "formerRepresentations" => %{"orderedItems" => [item]} + } + } = filtered + + assert %{"tag" => ^expected_tags, "emoji" => ^expected_emoji} = item + end + end + + describe "federated_timeline_removal_url" do + setup do + clear_config([:mrf_emoji, :federated_timeline_removal_url], [ + "https://example.org/test.png", + ~r{/biribiri/mikoto_smile[23]\.png}, + "nekomimi_girl_emoji" + ]) + + :ok + end + + test "processes status" do + {:ok, filtered} = MRF.filter_one(EmojiPolicy, @status_data) + + expected_tags = @status_data["object"]["tag"] + expected_emoji = @status_data["object"]["emoji"] + + expected_to = ["https://example.org/self"] + expected_cc = [Pleroma.Constants.as_public(), "https://example.org/someone"] + + assert %{ + "to" => ^expected_to, + "cc" => ^expected_cc, + "object" => %{"tag" => ^expected_tags, "emoji" => ^expected_emoji} + } = filtered + end + + test "ignore updates" do + {:ok, filtered} = MRF.filter_one(EmojiPolicy, @status_data |> Map.put("type", "Update")) + + expected_tags = @status_data["object"]["tag"] + expected_emoji = @status_data["object"]["emoji"] + + expected_to = ["https://example.org/self", Pleroma.Constants.as_public()] + expected_cc = ["https://example.org/someone"] + + assert %{ + "to" => ^expected_to, + "cc" => ^expected_cc, + "object" => %{"tag" => ^expected_tags, "emoji" => ^expected_emoji} + } = filtered + end + + test "processes status with history" do + status = + @status_data_with_history + |> put_in(["object", "tag"], @misc_tags) + |> put_in(["object", "emoji"], %{}) + + {:ok, filtered} = MRF.filter_one(EmojiPolicy, status) + + expected_tags = @status_data["object"]["tag"] + expected_emoji = @status_data["object"]["emoji"] + + expected_to = ["https://example.org/self"] + expected_cc = [Pleroma.Constants.as_public(), "https://example.org/someone"] + + assert %{ + "to" => ^expected_to, + "cc" => ^expected_cc, + "object" => %{ + "formerRepresentations" => %{ + "orderedItems" => [%{"tag" => ^expected_tags, "emoji" => ^expected_emoji}] + } + } + } = filtered + end end end From f50422c3802cd2316cfa6312d5a88c018c722843 Mon Sep 17 00:00:00 2001 From: tusooa Date: Tue, 28 Feb 2023 11:48:34 -0500 Subject: [PATCH 081/199] Move emoji_policy.ex to the right place --- lib/pleroma/web/activity_pub/{ => mrf}/emoji_policy.ex | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lib/pleroma/web/activity_pub/{ => mrf}/emoji_policy.ex (100%) diff --git a/lib/pleroma/web/activity_pub/emoji_policy.ex b/lib/pleroma/web/activity_pub/mrf/emoji_policy.ex similarity index 100% rename from lib/pleroma/web/activity_pub/emoji_policy.ex rename to lib/pleroma/web/activity_pub/mrf/emoji_policy.ex From 18a8378beb01a7ee93afcddf16e2699c5fe6d35a Mon Sep 17 00:00:00 2001 From: tusooa Date: Tue, 28 Feb 2023 12:05:54 -0500 Subject: [PATCH 082/199] Update config cheatsheet --- docs/configuration/cheatsheet.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 1e49a79d0..696d10e74 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -261,6 +261,11 @@ Notes: * `follower_nickname`: The name of the bot account to use for following newly discovered users. Using `followbot` or similar is strongly suggested. +#### :mrf_emoji +* `remove_url`: A list of patterns which result in emoji whose URL matches being removed from the message. This will apply to both statuses and user profiles. Each pattern can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html). +* `remove_shortcode`: A list of patterns which result in emoji whose shortcode matches being removed from the message. This will apply to both statuses and user profiles. Each pattern can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html). +* `federated_timeline_removal_url`: A list of patterns which result in message with emojis whose URLs match being removed from federated timelines (a.k.a unlisted). This will apply only to statuses. Each pattern can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html). +* `federated_timeline_removal_shortcode`: A list of patterns which result in message with emojis whose shortcodes match being removed from federated timelines (a.k.a unlisted). This will apply only to statuses. Each pattern can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html). ### :activitypub * `unfollow_blocked`: Whether blocks result in people getting unfollowed From 20d193c91da587372c618c53020a1f90eff20a7b Mon Sep 17 00:00:00 2001 From: tusooa Date: Tue, 28 Feb 2023 12:14:48 -0500 Subject: [PATCH 083/199] Improve config examples for EmojiPolicy --- lib/pleroma/web/activity_pub/mrf/emoji_policy.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/activity_pub/mrf/emoji_policy.ex b/lib/pleroma/web/activity_pub/mrf/emoji_policy.ex index df1566ec3..9a2e8eade 100644 --- a/lib/pleroma/web/activity_pub/mrf/emoji_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/emoji_policy.ex @@ -231,7 +231,7 @@ def config_description do Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`. """, - suggestions: ["foo", ~r/foo/iu] + suggestions: ["https://example.org/foo.png", ~r/example.org\/foo/iu] }, %{ key: :remove_shortcode, @@ -251,7 +251,7 @@ def config_description do Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`. """, - suggestions: ["foo", ~r/foo/iu] + suggestions: ["https://example.org/foo.png", ~r/example.org\/foo/iu] }, %{ key: :federated_timeline_removal_shortcode, From ef8a6c539a98633d1a78fa42379952b93f847309 Mon Sep 17 00:00:00 2001 From: tusooa Date: Tue, 13 Jun 2023 14:53:20 -0400 Subject: [PATCH 084/199] Make EmojiPolicy aware of custom emoji reactions --- docs/configuration/cheatsheet.md | 4 +- .../web/activity_pub/mrf/emoji_policy.ex | 33 +++++++++---- .../activity_pub/mrf/emoji_policy_test.exs | 46 +++++++++++++++++++ 3 files changed, 72 insertions(+), 11 deletions(-) diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 696d10e74..f43cde114 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -262,8 +262,8 @@ Notes: * `follower_nickname`: The name of the bot account to use for following newly discovered users. Using `followbot` or similar is strongly suggested. #### :mrf_emoji -* `remove_url`: A list of patterns which result in emoji whose URL matches being removed from the message. This will apply to both statuses and user profiles. Each pattern can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html). -* `remove_shortcode`: A list of patterns which result in emoji whose shortcode matches being removed from the message. This will apply to both statuses and user profiles. Each pattern can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html). +* `remove_url`: A list of patterns which result in emoji whose URL matches being removed from the message. This will apply to statuses, emoji reactions, and user profiles. Each pattern can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html). +* `remove_shortcode`: A list of patterns which result in emoji whose shortcode matches being removed from the message. This will apply to statuses, emoji reactions, and user profiles. Each pattern can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html). * `federated_timeline_removal_url`: A list of patterns which result in message with emojis whose URLs match being removed from federated timelines (a.k.a unlisted). This will apply only to statuses. Each pattern can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html). * `federated_timeline_removal_shortcode`: A list of patterns which result in message with emojis whose shortcodes match being removed from federated timelines (a.k.a unlisted). This will apply only to statuses. Each pattern can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html). diff --git a/lib/pleroma/web/activity_pub/mrf/emoji_policy.ex b/lib/pleroma/web/activity_pub/mrf/emoji_policy.ex index 9a2e8eade..13201ac55 100644 --- a/lib/pleroma/web/activity_pub/mrf/emoji_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/emoji_policy.ex @@ -55,6 +55,17 @@ def filter(%{"type" => type} = object) when type in Pleroma.Constants.actor_type end end + @impl Pleroma.Web.ActivityPub.MRF.Policy + def filter(%{"type" => "EmojiReact"} = object) do + with {:ok, _} <- + matched_emoji_checker(config_remove_url(), config_remove_shortcode()).(object) do + {:ok, object} + else + _ -> + {:reject, "[EmojiPolicy] Rejected for having disallowed emoji"} + end + end + @impl Pleroma.Web.ActivityPub.MRF.Policy def filter(message) do {:ok, message} @@ -133,20 +144,24 @@ defp process_remove_impl(object, extract_from_tag, extract_from_emoji, patterns) end end - defp maybe_delist(%{"object" => object, "to" => to, "type" => "Create"} = activity) do - check = fn object -> - if any_emoji_match?(object, &url_from_tag/1, &url_from_emoji/1, config_unlist_url()) or + defp matched_emoji_checker(urls, shortcodes) do + fn object -> + if any_emoji_match?(object, &url_from_tag/1, &url_from_emoji/1, urls) or any_emoji_match?( object, &shortcode_from_tag/1, &shortcode_from_emoji/1, - config_unlist_shortcode() + shortcodes ) do - {:should_delist, nil} + {:matched, nil} else {:ok, %{}} end end + end + + defp maybe_delist(%{"object" => object, "to" => to, "type" => "Create"} = activity) do + check = matched_emoji_checker(config_unlist_url(), config_unlist_shortcode()) should_delist? = fn object -> with {:ok, _} <- Pleroma.Object.Updater.do_with_history(object, check) do @@ -173,7 +188,7 @@ defp maybe_delist(activity), do: activity defp any_emoji_match?(object, extract_from_tag, extract_from_emoji, patterns) do Kernel.||( Enum.any?( - object["tag"], + object["tag"] || [], fn %{"type" => "Emoji"} = tag -> str = extract_from_tag.(tag) @@ -188,7 +203,7 @@ defp any_emoji_match?(object, extract_from_tag, extract_from_emoji, patterns) do false end ), - object["emoji"] + (object["emoji"] || []) |> Enum.any?(fn emoji -> match_any?(extract_from_emoji.(emoji), patterns) end) ) end @@ -227,7 +242,7 @@ def config_description do key: :remove_url, type: {:list, :string}, description: """ - A list of patterns which result in emoji whose URL matches being removed from the message. This will apply to both statuses and user profiles. + A list of patterns which result in emoji whose URL matches being removed from the message. This will apply to statuses, emoji reactions, and user profiles. Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`. """, @@ -237,7 +252,7 @@ def config_description do key: :remove_shortcode, type: {:list, :string}, description: """ - A list of patterns which result in emoji whose shortcode matches being removed from the message. This will apply to both statuses and user profiles. + A list of patterns which result in emoji whose shortcode matches being removed from the message. This will apply to statuses, emoji reactions, and user profiles. Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`. """, diff --git a/test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs b/test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs index 1e1b5c578..ae4c44f1c 100644 --- a/test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs @@ -109,6 +109,30 @@ defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicyTest do "cc" => ["https://example.org/someone"] } + @emoji_react_data %{ + "type" => "EmojiReact", + "tag" => [@emoji_tags |> Enum.at(3)], + "object" => "https://example.org/someobject", + "to" => ["https://example.org/self"], + "cc" => ["https://example.org/someone"] + } + + @emoji_react_data_matching_regex %{ + "type" => "EmojiReact", + "tag" => [@emoji_tags |> Enum.at(1)], + "object" => "https://example.org/someobject", + "to" => ["https://example.org/self"], + "cc" => ["https://example.org/someone"] + } + + @emoji_react_data_matching_nothing %{ + "type" => "EmojiReact", + "tag" => [@emoji_tags |> Enum.at(2)], + "object" => "https://example.org/someobject", + "to" => ["https://example.org/self"], + "cc" => ["https://example.org/someone"] + } + describe "remove_url" do setup do clear_config([:mrf_emoji, :remove_url], [ @@ -183,6 +207,17 @@ test "processes updates" do assert %{"tag" => ^expected_tags, "emoji" => ^expected_emoji} = item end + + test "processes EmojiReact" do + assert {:reject, "[EmojiPolicy] Rejected for having disallowed emoji"} == + MRF.filter_one(EmojiPolicy, @emoji_react_data) + + assert {:reject, "[EmojiPolicy] Rejected for having disallowed emoji"} == + MRF.filter_one(EmojiPolicy, @emoji_react_data_matching_regex) + + assert {:ok, @emoji_react_data_matching_nothing} == + MRF.filter_one(EmojiPolicy, @emoji_react_data_matching_nothing) + end end describe "remove_shortcode" do @@ -259,6 +294,17 @@ test "processes updates" do assert %{"tag" => ^expected_tags, "emoji" => ^expected_emoji} = item end + + test "processes EmojiReact" do + assert {:reject, "[EmojiPolicy] Rejected for having disallowed emoji"} == + MRF.filter_one(EmojiPolicy, @emoji_react_data) + + assert {:reject, "[EmojiPolicy] Rejected for having disallowed emoji"} == + MRF.filter_one(EmojiPolicy, @emoji_react_data_matching_regex) + + assert {:ok, @emoji_react_data_matching_nothing} == + MRF.filter_one(EmojiPolicy, @emoji_react_data_matching_nothing) + end end describe "federated_timeline_removal_url" do From d670dbdbd3e675b7d29c97e8bfd5301dcf3c9de0 Mon Sep 17 00:00:00 2001 From: tusooa Date: Tue, 13 Jun 2023 14:55:27 -0400 Subject: [PATCH 085/199] Test that unicode emoji reactions are not affected --- .../web/activity_pub/mrf/emoji_policy_test.exs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs b/test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs index ae4c44f1c..121ee1ea0 100644 --- a/test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs @@ -133,6 +133,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicyTest do "cc" => ["https://example.org/someone"] } + @emoji_react_data_unicode %{ + "type" => "EmojiReact", + "content" => "😍", + "object" => "https://example.org/someobject", + "to" => ["https://example.org/self"], + "cc" => ["https://example.org/someone"] + } + describe "remove_url" do setup do clear_config([:mrf_emoji, :remove_url], [ @@ -217,6 +225,9 @@ test "processes EmojiReact" do assert {:ok, @emoji_react_data_matching_nothing} == MRF.filter_one(EmojiPolicy, @emoji_react_data_matching_nothing) + + assert {:ok, @emoji_react_data_unicode} == + MRF.filter_one(EmojiPolicy, @emoji_react_data_unicode) end end @@ -304,6 +315,9 @@ test "processes EmojiReact" do assert {:ok, @emoji_react_data_matching_nothing} == MRF.filter_one(EmojiPolicy, @emoji_react_data_matching_nothing) + + assert {:ok, @emoji_react_data_unicode} == + MRF.filter_one(EmojiPolicy, @emoji_react_data_unicode) end end From 0d914e17bedb49a5efb8c0268d4a6df2e7ffdf96 Mon Sep 17 00:00:00 2001 From: tusooa Date: Tue, 13 Jun 2023 14:56:16 -0400 Subject: [PATCH 086/199] Add changelog --- changelog.d/emoji-policy.add | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/emoji-policy.add diff --git a/changelog.d/emoji-policy.add b/changelog.d/emoji-policy.add new file mode 100644 index 000000000..45510c4f6 --- /dev/null +++ b/changelog.d/emoji-policy.add @@ -0,0 +1 @@ +Implement MRF policy to reject or delist according to emojis From ba3aa4f86da77ece8ac472c40da180ab1921c304 Mon Sep 17 00:00:00 2001 From: tusooa Date: Tue, 20 Jun 2023 10:14:01 -0400 Subject: [PATCH 087/199] Fix edge cases --- lib/pleroma/constants.ex | 12 +++ .../web/activity_pub/mrf/emoji_policy.ex | 79 ++++++++++--------- .../activity_pub/mrf/emoji_policy_test.exs | 33 ++++++++ 3 files changed, 87 insertions(+), 37 deletions(-) diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex index cfb405218..7b4fd03b6 100644 --- a/lib/pleroma/constants.ex +++ b/lib/pleroma/constants.ex @@ -42,6 +42,18 @@ defmodule Pleroma.Constants do ] ) + const(status_object_types, + do: [ + "Note", + "Question", + "Audio", + "Video", + "Event", + "Article", + "Page" + ] + ) + const(updatable_object_types, do: [ "Note", diff --git a/lib/pleroma/web/activity_pub/mrf/emoji_policy.ex b/lib/pleroma/web/activity_pub/mrf/emoji_policy.ex index 13201ac55..95d6a6d43 100644 --- a/lib/pleroma/web/activity_pub/mrf/emoji_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/emoji_policy.ex @@ -31,8 +31,8 @@ defp config_unlist_shortcode do def history_awareness, do: :manual @impl Pleroma.Web.ActivityPub.MRF.Policy - def filter(%{"type" => type, "object" => %{} = object} = message) - when type in ["Create", "Update"] do + def filter(%{"type" => type, "object" => %{"type" => objtype} = object} = message) + when type in ["Create", "Update"] and objtype in Pleroma.Constants.status_object_types() do with {:ok, object} <- Updater.do_with_history(object, fn object -> {:ok, process_remove(object, :url, config_remove_url())} @@ -102,46 +102,51 @@ defp process_remove(object, :shortcode, patterns) do end defp process_remove_impl(object, extract_from_tag, extract_from_emoji, patterns) do - processed_tag = - Enum.filter( - object["tag"], - fn - %{"type" => "Emoji"} = tag -> - str = extract_from_tag.(tag) + object = + if object["tag"] do + Map.put( + object, + "tag", + Enum.filter( + object["tag"], + fn + %{"type" => "Emoji"} = tag -> + str = extract_from_tag.(tag) - if is_binary(str) do - not match_any?(str, patterns) - else - true + if is_binary(str) do + not match_any?(str, patterns) + else + true + end + + _ -> + true end - - _ -> - true - end - ) - - processed_emoji = - if object["emoji"] do - object["emoji"] - |> Enum.reduce(%{}, fn {name, url} = emoji, acc -> - if not match_any?(extract_from_emoji.(emoji), patterns) do - Map.put(acc, name, url) - else - acc - end - end) + ) + ) else - nil + object end - if processed_emoji do - object - |> Map.put("tag", processed_tag) - |> Map.put("emoji", processed_emoji) - else - object - |> Map.put("tag", processed_tag) - end + object = + if object["emoji"] do + Map.put( + object, + "emoji", + object["emoji"] + |> Enum.reduce(%{}, fn {name, url} = emoji, acc -> + if not match_any?(extract_from_emoji.(emoji), patterns) do + Map.put(acc, name, url) + else + acc + end + end) + ) + else + object + end + + object end defp matched_emoji_checker(urls, shortcodes) do diff --git a/test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs b/test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs index 121ee1ea0..7350800f0 100644 --- a/test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs @@ -389,4 +389,37 @@ test "processes status with history" do } = filtered end end + + describe "edge cases" do + setup do + clear_config([:mrf_emoji, :remove_url], [ + "https://example.org/test.png", + ~r{/biribiri/mikoto_smile[23]\.png}, + "nekomimi_girl_emoji" + ]) + + :ok + end + + test "non-statuses" do + answer = @status_data |> put_in(["object", "type"], "Answer") + {:ok, filtered} = MRF.filter_one(EmojiPolicy, answer) + + assert filtered == answer + end + + test "without tag" do + status = @status_data |> Map.put("object", Map.drop(@status_data["object"], ["tag"])) + {:ok, filtered} = MRF.filter_one(EmojiPolicy, status) + + refute Map.has_key?(filtered["object"], "tag") + end + + test "without emoji" do + status = @status_data |> Map.put("object", Map.drop(@status_data["object"], ["emoji"])) + {:ok, filtered} = MRF.filter_one(EmojiPolicy, status) + + refute Map.has_key?(filtered["object"], "emoji") + end + end end From 1459d64508b10725231d8f4256fbc68166c1b8ae Mon Sep 17 00:00:00 2001 From: tusooa Date: Fri, 7 Jul 2023 07:09:35 -0400 Subject: [PATCH 088/199] Make regex-to-string descriptor reusable --- .../web/activity_pub/mrf/emoji_policy.ex | 12 ++---------- .../web/activity_pub/mrf/keyword_policy.ex | 16 ++++------------ lib/pleroma/web/activity_pub/mrf/utils.ex | 15 +++++++++++++++ .../web/activity_pub/mrf/utils_test.exs | 19 +++++++++++++++++++ 4 files changed, 40 insertions(+), 22 deletions(-) create mode 100644 lib/pleroma/web/activity_pub/mrf/utils.ex create mode 100644 test/pleroma/web/activity_pub/mrf/utils_test.exs diff --git a/lib/pleroma/web/activity_pub/mrf/emoji_policy.ex b/lib/pleroma/web/activity_pub/mrf/emoji_policy.ex index 95d6a6d43..f884962b9 100644 --- a/lib/pleroma/web/activity_pub/mrf/emoji_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/emoji_policy.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicy do require Pleroma.Constants alias Pleroma.Object.Updater + alias Pleroma.Web.ActivityPub.MRF.Utils @moduledoc "Reject or force-unlisted emojis with certain URLs or names" @@ -215,19 +216,10 @@ defp any_emoji_match?(object, extract_from_tag, extract_from_emoji, patterns) do @impl Pleroma.Web.ActivityPub.MRF.Policy def describe do - # This horror is needed to convert regex sigils to strings mrf_emoji = Pleroma.Config.get(:mrf_emoji, []) |> Enum.map(fn {key, value} -> - {key, - Enum.map(value, fn - pattern -> - if not is_binary(pattern) do - inspect(pattern) - else - pattern - end - end)} + {key, Enum.map(value, &Utils.describe_regex_or_string/1)} end) |> Enum.into(%{}) diff --git a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex index 687ec6c2f..874fe9ab9 100644 --- a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex @@ -5,6 +5,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do require Pleroma.Constants + alias Pleroma.Web.ActivityPub.MRF.Utils + @moduledoc "Reject or Word-Replace messages with a keyword or regex" @behaviour Pleroma.Web.ActivityPub.MRF.Policy @@ -128,7 +130,6 @@ def filter(message), do: {:ok, message} @impl true def describe do - # This horror is needed to convert regex sigils to strings mrf_keyword = Pleroma.Config.get(:mrf_keyword, []) |> Enum.map(fn {key, value} -> @@ -136,21 +137,12 @@ def describe do Enum.map(value, fn {pattern, replacement} -> %{ - "pattern" => - if not is_binary(pattern) do - inspect(pattern) - else - pattern - end, + "pattern" => Utils.describe_regex_or_string(pattern), "replacement" => replacement } pattern -> - if not is_binary(pattern) do - inspect(pattern) - else - pattern - end + Utils.describe_regex_or_string(pattern) end)} end) |> Enum.into(%{}) diff --git a/lib/pleroma/web/activity_pub/mrf/utils.ex b/lib/pleroma/web/activity_pub/mrf/utils.ex new file mode 100644 index 000000000..f2dc9eea9 --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/utils.ex @@ -0,0 +1,15 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.Utils do + @spec describe_regex_or_string(String.t() | Regex.t()) :: String.t() + def describe_regex_or_string(pattern) do + # This horror is needed to convert regex sigils to strings + if not is_binary(pattern) do + inspect(pattern) + else + pattern + end + end +end diff --git a/test/pleroma/web/activity_pub/mrf/utils_test.exs b/test/pleroma/web/activity_pub/mrf/utils_test.exs new file mode 100644 index 000000000..3bbc2cfd3 --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/utils_test.exs @@ -0,0 +1,19 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.UtilsTest do + use Pleroma.DataCase, async: true + + alias Pleroma.Web.ActivityPub.MRF.Utils + + describe "describe_regex_or_string/1" do + test "describes regex" do + assert "~r/foo/i" == Utils.describe_regex_or_string(~r/foo/i) + end + + test "returns string as-is" do + assert "foo" == Utils.describe_regex_or_string("foo") + end + end +end From ea4225a646b355150fb8e5e8c77d7fdc58b5e7ef Mon Sep 17 00:00:00 2001 From: tusooa Date: Tue, 18 Jul 2023 18:39:59 -0400 Subject: [PATCH 089/199] Restrict attachments to only uploaded files only --- changelog.d/attachment-type-check.fix | 1 + lib/pleroma/constants.ex | 2 ++ lib/pleroma/web/common_api/utils.ex | 7 ++++++- test/pleroma/web/common_api/utils_test.exs | 11 ++++++++--- 4 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 changelog.d/attachment-type-check.fix diff --git a/changelog.d/attachment-type-check.fix b/changelog.d/attachment-type-check.fix new file mode 100644 index 000000000..9e14b75f1 --- /dev/null +++ b/changelog.d/attachment-type-check.fix @@ -0,0 +1 @@ +Restrict attachments to only uploaded files only diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex index 7b4fd03b6..6befc6897 100644 --- a/lib/pleroma/constants.ex +++ b/lib/pleroma/constants.ex @@ -81,4 +81,6 @@ defmodule Pleroma.Constants do const(mime_regex, do: ~r/^[^[:cntrl:] ()<>@,;:\\"\/\[\]?=]+\/[^[:cntrl:] ()<>@,;:\\"\/\[\]?=]+(; .*)?$/ ) + + const(upload_object_types, do: ["Document", "Image"]) end diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index a93c97e1e..b9fe0224c 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -59,7 +59,12 @@ def attachments_from_ids_descs(ids, descs_str) do end defp get_attachment(media_id) do - Repo.get(Object, media_id) + with %Object{data: data} = object <- Repo.get(Object, media_id), + %{"type" => type} when type in Pleroma.Constants.upload_object_types() <- data do + object + else + _ -> nil + end end @spec get_to_and_cc(ActivityDraft.t()) :: {list(String.t()), list(String.t())} diff --git a/test/pleroma/web/common_api/utils_test.exs b/test/pleroma/web/common_api/utils_test.exs index d309c6ded..ca5b92683 100644 --- a/test/pleroma/web/common_api/utils_test.exs +++ b/test/pleroma/web/common_api/utils_test.exs @@ -592,7 +592,7 @@ test "returns [] when attachment ids is empty" do end test "returns list attachments with desc" do - object = insert(:note) + object = insert(:attachment) desc = Jason.encode!(%{object.id => "test-desc"}) assert Utils.attachments_from_ids_descs(["#{object.id}", "34"], desc) == [ @@ -603,7 +603,7 @@ test "returns list attachments with desc" do describe "attachments_from_ids/1" do test "returns attachments with descs" do - object = insert(:note) + object = insert(:attachment) desc = Jason.encode!(%{object.id => "test-desc"}) assert Utils.attachments_from_ids(%{ @@ -615,13 +615,18 @@ test "returns attachments with descs" do end test "returns attachments without descs" do - object = insert(:note) + object = insert(:attachment) assert Utils.attachments_from_ids(%{media_ids: ["#{object.id}"]}) == [object.data] end test "returns [] when not pass media_ids" do assert Utils.attachments_from_ids(%{}) == [] end + + test "checks that the object is of upload type" do + object = insert(:note) + assert Utils.attachments_from_ids(%{media_ids: ["#{object.id}"]}) == [] + end end describe "maybe_add_list_data/3" do From dc4de79d43ba941d8aa16f7c14887faae9f65e9f Mon Sep 17 00:00:00 2001 From: faried nawaz Date: Wed, 7 Dec 2022 22:37:50 +0500 Subject: [PATCH 090/199] status context: perform visibility check on activities around a status issue #2927 --- lib/pleroma/web/activity_pub/activity_pub.ex | 23 ++ .../controllers/status_controller_test.exs | 267 ++++++++++++++++++ 2 files changed, 290 insertions(+) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index c93288b79..f1a12d22d 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -455,6 +455,7 @@ def fetch_activities_for_context_query(context, opts) do |> maybe_preload_objects(opts) |> maybe_preload_bookmarks(opts) |> maybe_set_thread_muted_field(opts) + |> restrict_unauthenticated(opts[:user]) |> restrict_blocked(opts) |> restrict_blockers_visibility(opts) |> restrict_recipients(recipients, opts[:user]) @@ -1215,6 +1216,28 @@ defp restrict_filtered(query, %{blocking_user: %User{} = user}) do defp restrict_filtered(query, _), do: query + defp restrict_unauthenticated(query, nil) do + local = Config.restrict_unauthenticated_access?(:activities, :local) + remote = Config.restrict_unauthenticated_access?(:activities, :remote) + + cond do + local and remote -> + # is there a better way to handle this? + from(activity in query, where: 1 == 0) + + local -> + from(activity in query, where: activity.local == false) + + remote -> + from(activity in query, where: activity.local == true) + + true -> + query + end + end + + defp restrict_unauthenticated(query, _), do: query + defp exclude_poll_votes(query, %{include_poll_votes: true}), do: query defp exclude_poll_votes(query, _) do 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 4f434cb69..76c289ee7 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -771,6 +771,49 @@ defp local_and_remote_activities do {:ok, local: local, remote: remote} end + defp local_and_remote_context_activities do + local_user_1 = insert(:user) + local_user_2 = insert(:user) + remote_user = insert(:user, local: false) + + {:ok, %{id: id1, data: %{"context" => context}}} = + CommonAPI.post(local_user_1, %{status: "post"}) + + {:ok, %{id: id2} = post} = + CommonAPI.post(local_user_2, %{status: "local reply", in_reply_to_status_id: id1}) + + params = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "actor" => remote_user.ap_id, + "type" => "Create", + "context" => context, + "id" => "#{remote_user.ap_id}/activities/1", + "inReplyTo" => post.data["id"], + "object" => %{ + "type" => "Note", + "content" => "remote reply", + "context" => context, + "id" => "#{remote_user.ap_id}/objects/1", + "attributedTo" => remote_user.ap_id, + "to" => [ + local_user_1.ap_id, + local_user_2.ap_id, + "https://www.w3.org/ns/activitystreams#Public" + ] + }, + "to" => [ + local_user_1.ap_id, + local_user_2.ap_id, + "https://www.w3.org/ns/activitystreams#Public" + ] + } + + {:ok, job} = Pleroma.Web.Federator.incoming_ap_doc(params) + {:ok, remote_activity} = ObanHelpers.perform(job) + + %{locals: [id1, id2], remote: remote_activity.id, context: context} + end + describe "status with restrict unauthenticated activities for local and remote" do setup do: local_and_remote_activities() @@ -957,6 +1000,230 @@ test "if user is authenticated", %{local: local, remote: remote} do end end + describe "getting status contexts restricted unauthenticated for local and remote" do + setup do: local_and_remote_context_activities() + + setup do: clear_config([:restrict_unauthenticated, :activities, :local], true) + + setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true) + + test "if user is unauthenticated", %{conn: conn, locals: [post_id, _]} do + res_conn = get(conn, "/api/v1/statuses/#{post_id}/context") + + assert json_response_and_validate_schema(res_conn, 200) == %{ + "ancestors" => [], + "descendants" => [] + } + end + + test "if user is unauthenticated reply", %{conn: conn, locals: [_, reply_id]} do + res_conn = get(conn, "/api/v1/statuses/#{reply_id}/context") + + assert json_response_and_validate_schema(res_conn, 200) == %{ + "ancestors" => [], + "descendants" => [] + } + end + + test "if user is authenticated", %{locals: [post_id, reply_id], remote: remote_reply_id} do + %{conn: conn} = oauth_access(["read"]) + res_conn = get(conn, "/api/v1/statuses/#{post_id}/context") + + %{"ancestors" => [], "descendants" => descendants} = + json_response_and_validate_schema(res_conn, 200) + + descendant_ids = + descendants + |> Enum.map(& &1["id"]) + + assert reply_id in descendant_ids + assert remote_reply_id in descendant_ids + end + + test "if user is authenticated reply", %{locals: [post_id, reply_id], remote: remote_reply_id} do + %{conn: conn} = oauth_access(["read"]) + res_conn = get(conn, "/api/v1/statuses/#{reply_id}/context") + + %{"ancestors" => ancestors, "descendants" => descendants} = + json_response_and_validate_schema(res_conn, 200) + + ancestor_ids = + ancestors + |> Enum.map(& &1["id"]) + + descendant_ids = + descendants + |> Enum.map(& &1["id"]) + + assert post_id in ancestor_ids + assert remote_reply_id in descendant_ids + end + end + + describe "getting status contexts restricted unauthenticated for local" do + setup do: local_and_remote_context_activities() + + setup do: clear_config([:restrict_unauthenticated, :activities, :local], true) + + setup do: clear_config([:restrict_unauthenticated, :activities, :remote], false) + + test "if user is unauthenticated", %{ + conn: conn, + locals: [post_id, reply_id], + remote: remote_reply_id + } do + res_conn = get(conn, "/api/v1/statuses/#{post_id}/context") + + %{"ancestors" => [], "descendants" => descendants} = + json_response_and_validate_schema(res_conn, 200) + + descendant_ids = + descendants + |> Enum.map(& &1["id"]) + + assert reply_id not in descendant_ids + assert remote_reply_id in descendant_ids + end + + test "if user is unauthenticated reply", %{ + conn: conn, + locals: [post_id, reply_id], + remote: remote_reply_id + } do + res_conn = get(conn, "/api/v1/statuses/#{reply_id}/context") + + %{"ancestors" => ancestors, "descendants" => descendants} = + json_response_and_validate_schema(res_conn, 200) + + ancestor_ids = + ancestors + |> Enum.map(& &1["id"]) + + descendant_ids = + descendants + |> Enum.map(& &1["id"]) + + assert post_id not in ancestor_ids + assert remote_reply_id in descendant_ids + end + + test "if user is authenticated", %{locals: [post_id, reply_id], remote: remote_reply_id} do + %{conn: conn} = oauth_access(["read"]) + res_conn = get(conn, "/api/v1/statuses/#{post_id}/context") + + %{"ancestors" => [], "descendants" => descendants} = + json_response_and_validate_schema(res_conn, 200) + + descendant_ids = + descendants + |> Enum.map(& &1["id"]) + + assert reply_id in descendant_ids + assert remote_reply_id in descendant_ids + end + + test "if user is authenticated reply", %{locals: [post_id, reply_id], remote: remote_reply_id} do + %{conn: conn} = oauth_access(["read"]) + res_conn = get(conn, "/api/v1/statuses/#{reply_id}/context") + + %{"ancestors" => ancestors, "descendants" => descendants} = + json_response_and_validate_schema(res_conn, 200) + + ancestor_ids = + ancestors + |> Enum.map(& &1["id"]) + + descendant_ids = + descendants + |> Enum.map(& &1["id"]) + + assert post_id in ancestor_ids + assert remote_reply_id in descendant_ids + end + end + + describe "getting status contexts restricted unauthenticated for remote" do + setup do: local_and_remote_context_activities() + + setup do: clear_config([:restrict_unauthenticated, :activities, :local], false) + + setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true) + + test "if user is unauthenticated", %{ + conn: conn, + locals: [post_id, reply_id], + remote: remote_reply_id + } do + res_conn = get(conn, "/api/v1/statuses/#{post_id}/context") + + %{"ancestors" => [], "descendants" => descendants} = + json_response_and_validate_schema(res_conn, 200) + + descendant_ids = + descendants + |> Enum.map(& &1["id"]) + + assert reply_id in descendant_ids + assert remote_reply_id not in descendant_ids + end + + test "if user is unauthenticated reply", %{ + conn: conn, + locals: [post_id, reply_id], + remote: remote_reply_id + } do + res_conn = get(conn, "/api/v1/statuses/#{reply_id}/context") + + %{"ancestors" => ancestors, "descendants" => descendants} = + json_response_and_validate_schema(res_conn, 200) + + ancestor_ids = + ancestors + |> Enum.map(& &1["id"]) + + descendant_ids = + descendants + |> Enum.map(& &1["id"]) + + assert post_id in ancestor_ids + assert remote_reply_id not in descendant_ids + end + + test "if user is authenticated", %{locals: [post_id, reply_id], remote: remote_reply_id} do + %{conn: conn} = oauth_access(["read"]) + res_conn = get(conn, "/api/v1/statuses/#{post_id}/context") + + %{"ancestors" => [], "descendants" => descendants} = + json_response_and_validate_schema(res_conn, 200) + + reply_ids = + descendants + |> Enum.map(& &1["id"]) + + assert reply_id in reply_ids + assert remote_reply_id in reply_ids + end + + test "if user is authenticated reply", %{locals: [post_id, reply_id], remote: remote_reply_id} do + %{conn: conn} = oauth_access(["read"]) + res_conn = get(conn, "/api/v1/statuses/#{reply_id}/context") + + %{"ancestors" => ancestors, "descendants" => descendants} = + json_response_and_validate_schema(res_conn, 200) + + ancestor_ids = + ancestors + |> Enum.map(& &1["id"]) + + descendant_ids = + descendants + |> Enum.map(& &1["id"]) + + assert post_id in ancestor_ids + assert remote_reply_id in descendant_ids + end + end + describe "deleting a status" do test "when you created it" do %{user: author, conn: conn} = oauth_access(["write:statuses"]) From e5e76ec44559af62eb56043add0489c61e6c1ee5 Mon Sep 17 00:00:00 2001 From: Faried Nawaz Date: Fri, 10 Feb 2023 01:32:32 +0500 Subject: [PATCH 091/199] cleaner ecto query to handle restrict_unauthenticated for activities This fix is for this case: config :pleroma, :restrict_unauthenticated, activities: %{local: true, remote: true} --- lib/pleroma/web/activity_pub/activity_pub.ex | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index f1a12d22d..3979d418e 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1222,8 +1222,7 @@ defp restrict_unauthenticated(query, nil) do cond do local and remote -> - # is there a better way to handle this? - from(activity in query, where: 1 == 0) + from(activity in query, where: false) local -> from(activity in query, where: activity.local == false) From 11ce81d4af6b428fabb9d4c6f0098d786a21487b Mon Sep 17 00:00:00 2001 From: faried nawaz Date: Fri, 28 Jul 2023 18:49:05 +0500 Subject: [PATCH 092/199] add changelog entry --- changelog.d/3801.fix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/3801.fix diff --git a/changelog.d/3801.fix b/changelog.d/3801.fix new file mode 100644 index 000000000..8c2ec0199 --- /dev/null +++ b/changelog.d/3801.fix @@ -0,0 +1 @@ +Filter context activities using Visibility.visible_for_user? From 18a0c923d0da4c8fb6e33b383dabd1d06bb22968 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 3 Aug 2023 13:08:37 -0400 Subject: [PATCH 093/199] Resolve information disclosure vulnerability through emoji pack archive download endpoint The pack name has been sanitized so an attacker cannot upload a media file called pack.json with their own handcrafted list of emoji files as arbitrary files on the filesystem and then call the emoji pack archive download endpoint with a pack name crafted to the location of the media file they uploaded which tricks Pleroma into generating a zip file of the target files the attacker wants to download. The attack only works if the Pleroma instance does not have the AnonymizeFilename upload filter enabled, which is currently the default. Reported by: graf@poast.org --- changelog.d/emoji-pack-sanitization.security | 1 + lib/pleroma/emoji/pack.ex | 1 + test/pleroma/emoji/pack_test.exs | 4 ++++ 3 files changed, 6 insertions(+) create mode 100644 changelog.d/emoji-pack-sanitization.security diff --git a/changelog.d/emoji-pack-sanitization.security b/changelog.d/emoji-pack-sanitization.security new file mode 100644 index 000000000..f3218abd4 --- /dev/null +++ b/changelog.d/emoji-pack-sanitization.security @@ -0,0 +1 @@ +Emoji pack loader sanitizes pack names diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex index a361ea200..6e58f8898 100644 --- a/lib/pleroma/emoji/pack.ex +++ b/lib/pleroma/emoji/pack.ex @@ -285,6 +285,7 @@ def update_metadata(name, data) do @spec load_pack(String.t()) :: {:ok, t()} | {:error, :file.posix()} def load_pack(name) do + name = Path.basename(name) pack_file = Path.join([emoji_path(), name, "pack.json"]) with {:ok, _} <- File.stat(pack_file), diff --git a/test/pleroma/emoji/pack_test.exs b/test/pleroma/emoji/pack_test.exs index 18b99da75..00001abfc 100644 --- a/test/pleroma/emoji/pack_test.exs +++ b/test/pleroma/emoji/pack_test.exs @@ -90,4 +90,8 @@ test "add emoji file", %{pack: pack} do assert updated_pack.files_count == 1 end + + test "load_pack/1 ignores path traversal in a forged pack name", %{pack: pack} do + assert {:ok, ^pack} = Pack.load_pack("../../../../../dump_pack") + end end From 2c795094535537a8607cc0d3b7f076a609636f40 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 3 Aug 2023 13:08:37 -0400 Subject: [PATCH 094/199] Resolve information disclosure vulnerability through emoji pack archive download endpoint The pack name has been sanitized so an attacker cannot upload a media file called pack.json with their own handcrafted list of emoji files as arbitrary files on the filesystem and then call the emoji pack archive download endpoint with a pack name crafted to the location of the media file they uploaded which tricks Pleroma into generating a zip file of the target files the attacker wants to download. The attack only works if the Pleroma instance does not have the AnonymizeFilename upload filter enabled, which is currently the default. Reported by: graf@poast.org --- changelog.d/emoji-pack-sanitization.security | 1 + lib/pleroma/emoji/pack.ex | 1 + test/pleroma/emoji/pack_test.exs | 4 ++++ 3 files changed, 6 insertions(+) create mode 100644 changelog.d/emoji-pack-sanitization.security diff --git a/changelog.d/emoji-pack-sanitization.security b/changelog.d/emoji-pack-sanitization.security new file mode 100644 index 000000000..f3218abd4 --- /dev/null +++ b/changelog.d/emoji-pack-sanitization.security @@ -0,0 +1 @@ +Emoji pack loader sanitizes pack names diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex index a361ea200..6e58f8898 100644 --- a/lib/pleroma/emoji/pack.ex +++ b/lib/pleroma/emoji/pack.ex @@ -285,6 +285,7 @@ def update_metadata(name, data) do @spec load_pack(String.t()) :: {:ok, t()} | {:error, :file.posix()} def load_pack(name) do + name = Path.basename(name) pack_file = Path.join([emoji_path(), name, "pack.json"]) with {:ok, _} <- File.stat(pack_file), diff --git a/test/pleroma/emoji/pack_test.exs b/test/pleroma/emoji/pack_test.exs index 18b99da75..00001abfc 100644 --- a/test/pleroma/emoji/pack_test.exs +++ b/test/pleroma/emoji/pack_test.exs @@ -90,4 +90,8 @@ test "add emoji file", %{pack: pack} do assert updated_pack.files_count == 1 end + + test "load_pack/1 ignores path traversal in a forged pack name", %{pack: pack} do + assert {:ok, ^pack} = Pack.load_pack("../../../../../dump_pack") + end end From 4befb3b1d02f32eb2c56f12e4684a7bb3167b0ee Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Thu, 22 Jun 2023 00:46:52 +0200 Subject: [PATCH 095/199] Config: Restrict permissions of OTP config file --- lib/pleroma/config/release_runtime_provider.ex | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/pleroma/config/release_runtime_provider.ex b/lib/pleroma/config/release_runtime_provider.ex index 91e5f1a54..9ec0f975e 100644 --- a/lib/pleroma/config/release_runtime_provider.ex +++ b/lib/pleroma/config/release_runtime_provider.ex @@ -20,6 +20,20 @@ def load(config, opts) do with_runtime_config = if File.exists?(config_path) do + # + %File.Stat{mode: mode} = File.lstat!(config_path) + + if Bitwise.band(mode, 0o007) > 0 do + raise "Configuration at #{config_path} has world-permissions, execute the following: chmod o= #{config_path}" + end + + if Bitwise.band(mode, 0o020) > 0 do + raise "Configuration at #{config_path} has group-wise write permissions, execute the following: chmod g-w #{config_path}" + end + + # Note: Elixir doesn't provides a getuid(2) + # so cannot forbid group-read only when config is owned by us + runtime_config = Config.Reader.read!(config_path) with_defaults From bd7381f2f4139c26d9dbb8aad77ce77be7777532 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Thu, 22 Jun 2023 00:58:05 +0200 Subject: [PATCH 096/199] instance gen: Reduce permissions of pleroma directories and config files --- lib/mix/tasks/pleroma/instance.ex | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex index 5c93f19ff..5d8b254a2 100644 --- a/lib/mix/tasks/pleroma/instance.ex +++ b/lib/mix/tasks/pleroma/instance.ex @@ -266,12 +266,20 @@ def run(["gen" | rest]) do config_dir = Path.dirname(config_path) psql_dir = Path.dirname(psql_path) + # Note: Distros requiring group read (0o750) on those directories should + # pre-create the directories. [config_dir, psql_dir, static_dir, uploads_dir] |> Enum.reject(&File.exists?/1) - |> Enum.map(&File.mkdir_p!/1) + |> Enum.each(fn dir -> + File.mkdir_p!(dir) + File.chmod!(dir, 0o700) + end) shell_info("Writing config to #{config_path}.") + # Sadly no fchmod(2) equivalent in Elixir… + File.touch!(config_path) + File.chmod!(config_path, 0o640) File.write(config_path, result_config) shell_info("Writing the postgres script to #{psql_path}.") File.write(psql_path, result_psql) @@ -290,8 +298,7 @@ def run(["gen" | rest]) do else shell_error( "The task would have overwritten the following files:\n" <> - (Enum.map(will_overwrite, &"- #{&1}\n") |> Enum.join("")) <> - "Rerun with `--force` to overwrite them." + Enum.map_join(will_overwrite, &"- #{&1}\n") <> "Rerun with `--force` to overwrite them." ) end end From 22df32b3f5cfe9fe0a4a97ff799df72c091b676e Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Thu, 22 Jun 2023 01:00:25 +0200 Subject: [PATCH 097/199] changelog: Entry for config permissions restrictions Closes: https://git.pleroma.social/pleroma/pleroma/-/issues/3135 --- changelog.d/otp_perms.security | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/otp_perms.security diff --git a/changelog.d/otp_perms.security b/changelog.d/otp_perms.security new file mode 100644 index 000000000..a3da1c677 --- /dev/null +++ b/changelog.d/otp_perms.security @@ -0,0 +1 @@ +- Reduced permissions of config files and directories, distros requiring greater permissions like group-read need to pre-create the directories \ No newline at end of file From 76e408e42d1da123a955b85490f05f6d810172f9 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Fri, 4 Aug 2023 07:16:50 +0200 Subject: [PATCH 098/199] release_runtime_provider_test: chmod config for hardened permissions Git doesn't manages file permissions precisely enough for us. --- test/pleroma/config/release_runtime_provider_test.exs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/pleroma/config/release_runtime_provider_test.exs b/test/pleroma/config/release_runtime_provider_test.exs index 4e0d4c838..8ff578e63 100644 --- a/test/pleroma/config/release_runtime_provider_test.exs +++ b/test/pleroma/config/release_runtime_provider_test.exs @@ -17,6 +17,8 @@ test "loads release defaults config and warns about non-existent runtime config" end test "merged runtime config" do + assert :ok == File.chmod!("test/fixtures/config/temp.secret.exs", 0o640) + merged = ReleaseRuntimeProvider.load([], config_path: "test/fixtures/config/temp.secret.exs") @@ -25,6 +27,8 @@ test "merged runtime config" do end test "merged exported config" do + assert :ok == File.chmod!("test/fixtures/config/temp.exported_from_db.secret.exs", 0o640) + ExUnit.CaptureIO.capture_io(fn -> merged = ReleaseRuntimeProvider.load([], @@ -37,6 +41,9 @@ test "merged exported config" do end test "runtime config is merged with exported config" do + assert :ok == File.chmod!("test/fixtures/config/temp.secret.exs", 0o640) + assert :ok == File.chmod!("test/fixtures/config/temp.exported_from_db.secret.exs", 0o640) + merged = ReleaseRuntimeProvider.load([], config_path: "test/fixtures/config/temp.secret.exs", From c37561214a803f9011d5ec6af8b8c07e547c19ff Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Fri, 4 Aug 2023 06:49:19 +0200 Subject: [PATCH 099/199] Force the use of amd64 runners for jobs using ci-base --- .gitlab-ci.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8b0381d11..91e568a32 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -32,7 +32,13 @@ before_script: after_script: - rm -rf _build/*/lib/pleroma +.using-ci-base: + tags: + - amd64 + build: + extends: + - .using-ci-base stage: build only: changes: &build_changes_policy @@ -44,6 +50,8 @@ build: - mix compile --force spec-build: + extends: + - .using-ci-base stage: test only: changes: @@ -57,6 +65,8 @@ spec-build: - mix pleroma.openapi_spec spec.json benchmark: + extends: + - .using-ci-base stage: benchmark when: manual variables: @@ -71,6 +81,8 @@ benchmark: - mix pleroma.load_testing unit-testing: + extends: + - .using-ci-base stage: test only: changes: *build_changes_policy @@ -94,6 +106,8 @@ unit-testing: path: coverage.xml unit-testing-erratic: + extends: + - .using-ci-base stage: test retry: 2 allow_failure: true @@ -129,6 +143,8 @@ unit-testing-erratic: # - mix test --trace --only federated unit-testing-rum: + extends: + - .using-ci-base stage: test only: changes: *build_changes_policy @@ -162,6 +178,8 @@ lint: - mix format --check-formatted analysis: + extends: + - .using-ci-base stage: test only: changes: *build_changes_policy From 5ac2b7417d052a493b38ee05b393ae7c78e89484 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Fri, 4 Aug 2023 09:16:08 +0200 Subject: [PATCH 100/199] test: Fix warnings --- .../activity_pub/transmogrifier/emoji_react_handling_test.exs | 2 +- test/pleroma/web/mastodon_api/update_credentials_test.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs index 9d99df27c..83bf59c6f 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs @@ -65,7 +65,7 @@ test "it works for incoming unqualified emoji reactions" do object = Object.get_by_ap_id(data["object"]) assert object.data["reaction_count"] == 1 - assert match?([[emoji, _]], object.data["reactions"]) + assert match?([[^emoji, _]], object.data["reactions"]) end test "it reject invalid emoji reactions" do diff --git a/test/pleroma/web/mastodon_api/update_credentials_test.exs b/test/pleroma/web/mastodon_api/update_credentials_test.exs index 57fa0f047..40f79d103 100644 --- a/test/pleroma/web/mastodon_api/update_credentials_test.exs +++ b/test/pleroma/web/mastodon_api/update_credentials_test.exs @@ -375,7 +375,7 @@ test "updates the user's background, upload_limit, returns a HTTP 413", %{ "pleroma_background_image" => new_background_oversized }) - assert user_response = json_response_and_validate_schema(res, 413) + assert _user_response = json_response_and_validate_schema(res, 413) assert user.background == %{} clear_config([:instance, :upload_limit], upload_limit) From 57f74537486cf7f721679f125741de9008478b00 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Fri, 4 Aug 2023 05:13:28 +0200 Subject: [PATCH 101/199] Release 2.5.3 --- CHANGELOG.md | 6 ++++++ mix.exs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6fc6aaee..468ec1012 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Removed +## 2.5.3 + +### Security +- Emoji pack loader sanitizes pack names +- Reduced permissions of config files and directories, distros requiring greater permissions like group-read need to pre-create the directories + ## 2.5.2 ### Security diff --git a/mix.exs b/mix.exs index 79fd9c9ef..d1cdb151d 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do def project do [ app: :pleroma, - version: version("2.5.2"), + version: version("2.5.3"), elixir: "~> 1.11", elixirc_paths: elixirc_paths(Mix.env()), compilers: [:phoenix, :gettext] ++ Mix.compilers(), From 8cc8100120abdbf26cfe4cdac2c0a012d7919e05 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Thu, 22 Jun 2023 00:46:52 +0200 Subject: [PATCH 102/199] Config: Restrict permissions of OTP config file --- lib/pleroma/config/release_runtime_provider.ex | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/pleroma/config/release_runtime_provider.ex b/lib/pleroma/config/release_runtime_provider.ex index 91e5f1a54..9ec0f975e 100644 --- a/lib/pleroma/config/release_runtime_provider.ex +++ b/lib/pleroma/config/release_runtime_provider.ex @@ -20,6 +20,20 @@ def load(config, opts) do with_runtime_config = if File.exists?(config_path) do + # + %File.Stat{mode: mode} = File.lstat!(config_path) + + if Bitwise.band(mode, 0o007) > 0 do + raise "Configuration at #{config_path} has world-permissions, execute the following: chmod o= #{config_path}" + end + + if Bitwise.band(mode, 0o020) > 0 do + raise "Configuration at #{config_path} has group-wise write permissions, execute the following: chmod g-w #{config_path}" + end + + # Note: Elixir doesn't provides a getuid(2) + # so cannot forbid group-read only when config is owned by us + runtime_config = Config.Reader.read!(config_path) with_defaults From 69caedc591bd61029f897f37ef7ecddd470cf935 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Thu, 22 Jun 2023 00:58:05 +0200 Subject: [PATCH 103/199] instance gen: Reduce permissions of pleroma directories and config files --- lib/mix/tasks/pleroma/instance.ex | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex index 5c93f19ff..5d8b254a2 100644 --- a/lib/mix/tasks/pleroma/instance.ex +++ b/lib/mix/tasks/pleroma/instance.ex @@ -266,12 +266,20 @@ def run(["gen" | rest]) do config_dir = Path.dirname(config_path) psql_dir = Path.dirname(psql_path) + # Note: Distros requiring group read (0o750) on those directories should + # pre-create the directories. [config_dir, psql_dir, static_dir, uploads_dir] |> Enum.reject(&File.exists?/1) - |> Enum.map(&File.mkdir_p!/1) + |> Enum.each(fn dir -> + File.mkdir_p!(dir) + File.chmod!(dir, 0o700) + end) shell_info("Writing config to #{config_path}.") + # Sadly no fchmod(2) equivalent in Elixir… + File.touch!(config_path) + File.chmod!(config_path, 0o640) File.write(config_path, result_config) shell_info("Writing the postgres script to #{psql_path}.") File.write(psql_path, result_psql) @@ -290,8 +298,7 @@ def run(["gen" | rest]) do else shell_error( "The task would have overwritten the following files:\n" <> - (Enum.map(will_overwrite, &"- #{&1}\n") |> Enum.join("")) <> - "Rerun with `--force` to overwrite them." + Enum.map_join(will_overwrite, &"- #{&1}\n") <> "Rerun with `--force` to overwrite them." ) end end From 9f0ad901ed4f8f0ad3e1d896fd41d25b93a97d76 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Thu, 22 Jun 2023 01:00:25 +0200 Subject: [PATCH 104/199] changelog: Entry for config permissions restrictions Closes: https://git.pleroma.social/pleroma/pleroma/-/issues/3135 --- changelog.d/otp_perms.security | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/otp_perms.security diff --git a/changelog.d/otp_perms.security b/changelog.d/otp_perms.security new file mode 100644 index 000000000..a3da1c677 --- /dev/null +++ b/changelog.d/otp_perms.security @@ -0,0 +1 @@ +- Reduced permissions of config files and directories, distros requiring greater permissions like group-read need to pre-create the directories \ No newline at end of file From 65ef8f19c5e21baadfa7daabdbd37fb9a1fbf863 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Fri, 4 Aug 2023 07:16:50 +0200 Subject: [PATCH 105/199] release_runtime_provider_test: chmod config for hardened permissions Git doesn't manages file permissions precisely enough for us. --- test/pleroma/config/release_runtime_provider_test.exs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/pleroma/config/release_runtime_provider_test.exs b/test/pleroma/config/release_runtime_provider_test.exs index e6d10b13e..8d2a93d6c 100644 --- a/test/pleroma/config/release_runtime_provider_test.exs +++ b/test/pleroma/config/release_runtime_provider_test.exs @@ -17,6 +17,8 @@ test "loads release defaults config and warns about non-existent runtime config" end test "merged runtime config" do + assert :ok == File.chmod!("test/fixtures/config/temp.secret.exs", 0o640) + merged = ReleaseRuntimeProvider.load([], config_path: "test/fixtures/config/temp.secret.exs") @@ -25,6 +27,8 @@ test "merged runtime config" do end test "merged exported config" do + assert :ok == File.chmod!("test/fixtures/config/temp.exported_from_db.secret.exs", 0o640) + ExUnit.CaptureIO.capture_io(fn -> merged = ReleaseRuntimeProvider.load([], @@ -37,6 +41,9 @@ test "merged exported config" do end test "runtime config is merged with exported config" do + assert :ok == File.chmod!("test/fixtures/config/temp.secret.exs", 0o640) + assert :ok == File.chmod!("test/fixtures/config/temp.exported_from_db.secret.exs", 0o640) + merged = ReleaseRuntimeProvider.load([], config_path: "test/fixtures/config/temp.secret.exs", From 6a0fd77c48946b4b2100585b4f32d125680f5f82 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Fri, 4 Aug 2023 05:13:28 +0200 Subject: [PATCH 106/199] Release 2.5.53 --- CHANGELOG.md | 6 ++++++ mix.exs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42a1bbb8f..87e9c5298 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Removed - BREAKING: Support for passwords generated with `crypt(3)` (Gnu Social migration artifact) +## 2.5.3 + +### Security +- Emoji pack loader sanitizes pack names +- Reduced permissions of config files and directories, distros requiring greater permissions like group-read need to pre-create the directories + ## 2.5.2 ### Security diff --git a/mix.exs b/mix.exs index fc4e87ee3..115e7113f 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do def project do [ app: :pleroma, - version: version("2.5.52"), + version: version("2.5.53"), elixir: "~> 1.11", elixirc_paths: elixirc_paths(Mix.env()), compilers: [:phoenix] ++ Mix.compilers(), From 0e321698d21766772aa2b54b518dcd76a6abce8c Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Fri, 4 Aug 2023 17:09:50 +0200 Subject: [PATCH 107/199] gentoo_otp_en.md: Indicate which install method it covers --- changelog.d/gentoo_otp_intro.skip | 0 docs/installation/gentoo_otp_en.md | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 changelog.d/gentoo_otp_intro.skip diff --git a/changelog.d/gentoo_otp_intro.skip b/changelog.d/gentoo_otp_intro.skip new file mode 100644 index 000000000..e69de29bb diff --git a/docs/installation/gentoo_otp_en.md b/docs/installation/gentoo_otp_en.md index 76968eb38..4fafc0c17 100644 --- a/docs/installation/gentoo_otp_en.md +++ b/docs/installation/gentoo_otp_en.md @@ -2,7 +2,7 @@ {! backend/installation/otp_vs_from_source.include !} -A [manual installation guide for gentoo](./gentoo_en.md) is also available. +This guide covers installation via Gentoo provided packaging. A [manual installation guide for gentoo](./gentoo_en.md) is also available. ## Installation From ca0859b90f0f3cb9bb369d38d29868de59796c2c Mon Sep 17 00:00:00 2001 From: Mae Date: Fri, 4 Aug 2023 22:24:17 +0100 Subject: [PATCH 108/199] Prevent XML parser from loading external entities --- lib/pleroma/web/xml.ex | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/xml.ex b/lib/pleroma/web/xml.ex index b699446b0..380a80ab8 100644 --- a/lib/pleroma/web/xml.ex +++ b/lib/pleroma/web/xml.ex @@ -29,7 +29,10 @@ def parse_document(text) do {doc, _rest} = text |> :binary.bin_to_list() - |> :xmerl_scan.string(quiet: true) + |> :xmerl_scan.string( + quiet: true, + fetch_fun: fn _, _ -> raise "Resolving external entities not supported" end + ) {:ok, doc} rescue From 307692cee8cdd0dbe3e6cf40c1192fcf43910610 Mon Sep 17 00:00:00 2001 From: FloatingGhost Date: Fri, 4 Aug 2023 22:24:32 +0100 Subject: [PATCH 109/199] Add unit test for external entity loading --- test/fixtures/xml_external_entities.xml | 3 +++ test/pleroma/web/web_finger_test.exs | 23 +++++++++++++++++++++++ test/pleroma/web/xml_test.exs | 10 ++++++++++ 3 files changed, 36 insertions(+) create mode 100644 test/fixtures/xml_external_entities.xml create mode 100644 test/pleroma/web/xml_test.exs diff --git a/test/fixtures/xml_external_entities.xml b/test/fixtures/xml_external_entities.xml new file mode 100644 index 000000000..d5ff87134 --- /dev/null +++ b/test/fixtures/xml_external_entities.xml @@ -0,0 +1,3 @@ + + ]> +&xxe; diff --git a/test/pleroma/web/web_finger_test.exs b/test/pleroma/web/web_finger_test.exs index fafef54fe..be5e08776 100644 --- a/test/pleroma/web/web_finger_test.exs +++ b/test/pleroma/web/web_finger_test.exs @@ -180,5 +180,28 @@ test "respects xml content-type" do {:ok, _data} = WebFinger.finger("pekorino@pawoo.net") end + + test "refuses to process XML remote entities" do + Tesla.Mock.mock(fn + %{ + url: "https://pawoo.net/.well-known/webfinger?resource=acct:pekorino@pawoo.net" + } -> + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/xml_external_entities.xml"), + headers: [{"content-type", "application/xrd+xml"}] + }} + + %{url: "https://pawoo.net/.well-known/host-meta"} -> + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/pawoo.net_host_meta") + }} + end) + + assert :error = WebFinger.finger("pekorino@pawoo.net") + end end end diff --git a/test/pleroma/web/xml_test.exs b/test/pleroma/web/xml_test.exs new file mode 100644 index 000000000..89d4709b6 --- /dev/null +++ b/test/pleroma/web/xml_test.exs @@ -0,0 +1,10 @@ +defmodule Pleroma.Web.XMLTest do + use Pleroma.DataCase, async: true + + alias Pleroma.Web.XML + + test "refuses to load external entities from XML" do + data = File.read!("test/fixtures/xml_external_entities.xml") + assert(:error == XML.parse_document(data)) + end +end From 6d48b0f1a93a5a44b95497063e885342240fbc27 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 4 Aug 2023 22:44:09 -0400 Subject: [PATCH 110/199] Document and test that XXE processing is disabled https://vuln.be/post/xxe-in-erlang-and-elixir/ --- changelog.d/akkoma-xml-remote-entities.security | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/akkoma-xml-remote-entities.security diff --git a/changelog.d/akkoma-xml-remote-entities.security b/changelog.d/akkoma-xml-remote-entities.security new file mode 100644 index 000000000..b3c86bee1 --- /dev/null +++ b/changelog.d/akkoma-xml-remote-entities.security @@ -0,0 +1 @@ +Restrict XML parser from processing external entitites (XXE) From fc10e07ffbc9d81c7a2ac38a3f9175f2edf2bd1f Mon Sep 17 00:00:00 2001 From: Mae Date: Fri, 4 Aug 2023 22:24:17 +0100 Subject: [PATCH 111/199] Prevent XML parser from loading external entities --- lib/pleroma/web/xml.ex | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/xml.ex b/lib/pleroma/web/xml.ex index b699446b0..380a80ab8 100644 --- a/lib/pleroma/web/xml.ex +++ b/lib/pleroma/web/xml.ex @@ -29,7 +29,10 @@ def parse_document(text) do {doc, _rest} = text |> :binary.bin_to_list() - |> :xmerl_scan.string(quiet: true) + |> :xmerl_scan.string( + quiet: true, + fetch_fun: fn _, _ -> raise "Resolving external entities not supported" end + ) {:ok, doc} rescue From 77d57c974ad83fcea77e424d53dc16a27e5d88b6 Mon Sep 17 00:00:00 2001 From: FloatingGhost Date: Fri, 4 Aug 2023 22:24:32 +0100 Subject: [PATCH 112/199] Add unit test for external entity loading --- test/fixtures/xml_external_entities.xml | 3 +++ test/pleroma/web/web_finger_test.exs | 23 +++++++++++++++++++++++ test/pleroma/web/xml_test.exs | 10 ++++++++++ 3 files changed, 36 insertions(+) create mode 100644 test/fixtures/xml_external_entities.xml create mode 100644 test/pleroma/web/xml_test.exs diff --git a/test/fixtures/xml_external_entities.xml b/test/fixtures/xml_external_entities.xml new file mode 100644 index 000000000..d5ff87134 --- /dev/null +++ b/test/fixtures/xml_external_entities.xml @@ -0,0 +1,3 @@ + + ]> +&xxe; diff --git a/test/pleroma/web/web_finger_test.exs b/test/pleroma/web/web_finger_test.exs index fafef54fe..be5e08776 100644 --- a/test/pleroma/web/web_finger_test.exs +++ b/test/pleroma/web/web_finger_test.exs @@ -180,5 +180,28 @@ test "respects xml content-type" do {:ok, _data} = WebFinger.finger("pekorino@pawoo.net") end + + test "refuses to process XML remote entities" do + Tesla.Mock.mock(fn + %{ + url: "https://pawoo.net/.well-known/webfinger?resource=acct:pekorino@pawoo.net" + } -> + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/xml_external_entities.xml"), + headers: [{"content-type", "application/xrd+xml"}] + }} + + %{url: "https://pawoo.net/.well-known/host-meta"} -> + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/pawoo.net_host_meta") + }} + end) + + assert :error = WebFinger.finger("pekorino@pawoo.net") + end end end diff --git a/test/pleroma/web/xml_test.exs b/test/pleroma/web/xml_test.exs new file mode 100644 index 000000000..89d4709b6 --- /dev/null +++ b/test/pleroma/web/xml_test.exs @@ -0,0 +1,10 @@ +defmodule Pleroma.Web.XMLTest do + use Pleroma.DataCase, async: true + + alias Pleroma.Web.XML + + test "refuses to load external entities from XML" do + data = File.read!("test/fixtures/xml_external_entities.xml") + assert(:error == XML.parse_document(data)) + end +end From cc848b78dca51fcd7e785eb92a7a3a4d5d1c419e Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 4 Aug 2023 22:44:09 -0400 Subject: [PATCH 113/199] Document and test that XXE processing is disabled https://vuln.be/post/xxe-in-erlang-and-elixir/ --- changelog.d/akkoma-xml-remote-entities.security | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/akkoma-xml-remote-entities.security diff --git a/changelog.d/akkoma-xml-remote-entities.security b/changelog.d/akkoma-xml-remote-entities.security new file mode 100644 index 000000000..b3c86bee1 --- /dev/null +++ b/changelog.d/akkoma-xml-remote-entities.security @@ -0,0 +1 @@ +Restrict XML parser from processing external entitites (XXE) From b631180b38ac63029f08bef137b13231bcf57b59 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Sat, 5 Aug 2023 08:27:42 +0200 Subject: [PATCH 114/199] Release 2.5.4 --- CHANGELOG.md | 5 +++++ changelog.d/akkoma-xml-remote-entities.security | 2 +- mix.exs | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 468ec1012..9d9aadc6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Removed +## 2.5.54 + +## Security +- Fix XML External Entity (XXE) loading vulnerability allowing to fetch arbitary files from the server's filesystem + ## 2.5.3 ### Security diff --git a/changelog.d/akkoma-xml-remote-entities.security b/changelog.d/akkoma-xml-remote-entities.security index b3c86bee1..5e6725e5b 100644 --- a/changelog.d/akkoma-xml-remote-entities.security +++ b/changelog.d/akkoma-xml-remote-entities.security @@ -1 +1 @@ -Restrict XML parser from processing external entitites (XXE) +Fix XML External Entity (XXE) loading vulnerability allowing to fetch arbitary files from the server's filesystem diff --git a/mix.exs b/mix.exs index d1cdb151d..12f721364 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do def project do [ app: :pleroma, - version: version("2.5.3"), + version: version("2.5.4"), elixir: "~> 1.11", elixirc_paths: elixirc_paths(Mix.env()), compilers: [:phoenix, :gettext] ++ Mix.compilers(), From 4099ddb3dc5840fa10cff743d87464acf7898a80 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Sat, 5 Aug 2023 08:27:42 +0200 Subject: [PATCH 115/199] Mergeback release 2.5.4 --- CHANGELOG.md | 5 +++++ changelog.d/akkoma-xml-remote-entities.security | 2 +- mix.exs | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87e9c5298..65acfad3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Removed - BREAKING: Support for passwords generated with `crypt(3)` (Gnu Social migration artifact) +## 2.5.4 + +## Security +- Fix XML External Entity (XXE) loading vulnerability allowing to fetch arbitary files from the server's filesystem + ## 2.5.3 ### Security diff --git a/changelog.d/akkoma-xml-remote-entities.security b/changelog.d/akkoma-xml-remote-entities.security index b3c86bee1..5e6725e5b 100644 --- a/changelog.d/akkoma-xml-remote-entities.security +++ b/changelog.d/akkoma-xml-remote-entities.security @@ -1 +1 @@ -Restrict XML parser from processing external entitites (XXE) +Fix XML External Entity (XXE) loading vulnerability allowing to fetch arbitary files from the server's filesystem diff --git a/mix.exs b/mix.exs index 115e7113f..b071e7c7b 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do def project do [ app: :pleroma, - version: version("2.5.53"), + version: version("2.5.54"), elixir: "~> 1.11", elixirc_paths: elixirc_paths(Mix.env()), compilers: [:phoenix] ++ Mix.compilers(), From 48b1e9bdc7382ec6ef33e95f2bd8674ae92f17b2 Mon Sep 17 00:00:00 2001 From: mae Date: Sat, 5 Aug 2023 14:13:49 +0200 Subject: [PATCH 116/199] Completely disable xml entity resolution --- .../disable-xml-entity-resolution.security | 1 + lib/pleroma/web/xml.ex | 2 +- test/fixtures/xml_billion_laughs.xml | 15 +++++++++++++++ test/pleroma/web/xml_test.exs | 5 +++++ 4 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 changelog.d/disable-xml-entity-resolution.security create mode 100644 test/fixtures/xml_billion_laughs.xml diff --git a/changelog.d/disable-xml-entity-resolution.security b/changelog.d/disable-xml-entity-resolution.security new file mode 100644 index 000000000..db8e12f67 --- /dev/null +++ b/changelog.d/disable-xml-entity-resolution.security @@ -0,0 +1 @@ +Disable XML entity resolution completely to fix a dos vulnerability diff --git a/lib/pleroma/web/xml.ex b/lib/pleroma/web/xml.ex index 380a80ab8..64329e4ba 100644 --- a/lib/pleroma/web/xml.ex +++ b/lib/pleroma/web/xml.ex @@ -31,7 +31,7 @@ def parse_document(text) do |> :binary.bin_to_list() |> :xmerl_scan.string( quiet: true, - fetch_fun: fn _, _ -> raise "Resolving external entities not supported" end + allow_entities: false ) {:ok, doc} diff --git a/test/fixtures/xml_billion_laughs.xml b/test/fixtures/xml_billion_laughs.xml new file mode 100644 index 000000000..75fb24cae --- /dev/null +++ b/test/fixtures/xml_billion_laughs.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + +]> +&lol9; diff --git a/test/pleroma/web/xml_test.exs b/test/pleroma/web/xml_test.exs index 89d4709b6..49306430b 100644 --- a/test/pleroma/web/xml_test.exs +++ b/test/pleroma/web/xml_test.exs @@ -3,6 +3,11 @@ defmodule Pleroma.Web.XMLTest do alias Pleroma.Web.XML + test "refuses to parse any entities from XML" do + data = File.read!("test/fixtures/xml_billion_laughs.xml") + assert(:error == XML.parse_document(data)) + end + test "refuses to load external entities from XML" do data = File.read!("test/fixtures/xml_external_entities.xml") assert(:error == XML.parse_document(data)) From c298e0165c29b30380466910bb328964a7264c4c Mon Sep 17 00:00:00 2001 From: Cat pony Black Date: Sat, 5 Aug 2023 13:10:35 +0200 Subject: [PATCH 117/199] Fix config ownership in dockerfile to pass restriction test --- Dockerfile | 2 +- changelog.d/dockerfile-config-perms.fix | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/dockerfile-config-perms.fix diff --git a/Dockerfile b/Dockerfile index 310d18104..72c227b76 100644 --- a/Dockerfile +++ b/Dockerfile @@ -49,7 +49,7 @@ USER pleroma COPY --from=build --chown=pleroma:0 /release ${HOME} -COPY ./config/docker.exs /etc/pleroma/config.exs +COPY --chown=pleroma --chmod=640 ./config/docker.exs /etc/pleroma/config.exs COPY ./docker-entrypoint.sh ${HOME} EXPOSE 4000 diff --git a/changelog.d/dockerfile-config-perms.fix b/changelog.d/dockerfile-config-perms.fix new file mode 100644 index 000000000..49ea5becb --- /dev/null +++ b/changelog.d/dockerfile-config-perms.fix @@ -0,0 +1 @@ +- Fix config ownership in dockerfile to pass restriction test From d838d1990bf23d452c1cc830629e42e51dbd7047 Mon Sep 17 00:00:00 2001 From: Haelwenn Date: Wed, 16 Aug 2023 13:34:32 +0000 Subject: [PATCH 118/199] Apply lanodan's suggestion(s) to 1 file(s) --- lib/pleroma/web/plugs/http_security_plug.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/plugs/http_security_plug.ex b/lib/pleroma/web/plugs/http_security_plug.ex index b3dc8a3a6..a3166bc96 100644 --- a/lib/pleroma/web/plugs/http_security_plug.ex +++ b/lib/pleroma/web/plugs/http_security_plug.ex @@ -93,7 +93,7 @@ defp csp_string do img_src = "img-src 'self' data: blob:" media_src = "media-src 'self'" - connect_src = ["connect-src 'self' blob: ", ?\s, websocket_url] + connect_src = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url] # Strict multimedia CSP enforcement only when MediaProxy is enabled {img_src, media_src, connect_src} = From 3d09bc320efcc68beb9b57fba23b6b9f3dc17905 Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 30 Aug 2023 20:34:16 -0400 Subject: [PATCH 119/199] Make lint happy --- lib/pleroma/web/plugs/http_security_plug.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/plugs/http_security_plug.ex b/lib/pleroma/web/plugs/http_security_plug.ex index a3166bc96..5093414c4 100644 --- a/lib/pleroma/web/plugs/http_security_plug.ex +++ b/lib/pleroma/web/plugs/http_security_plug.ex @@ -100,6 +100,7 @@ defp csp_string do if Config.get([:media_proxy, :enabled]) && !Config.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do sources = build_csp_multimedia_source_list() + { [img_src, sources], [media_src, sources], @@ -113,7 +114,6 @@ defp csp_string do } end - connect_src = if Config.get(:env) == :dev do [connect_src, " http://localhost:3035/"] From 3c5ecca37718a1eba05be1f379b8f47362079c65 Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 30 Aug 2023 20:37:45 -0400 Subject: [PATCH 120/199] Skip changelog --- changelog.d/lint.skip | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 changelog.d/lint.skip diff --git a/changelog.d/lint.skip b/changelog.d/lint.skip new file mode 100644 index 000000000..e69de29bb From 1afde067b12ad0062c1820091ea9b0a680819281 Mon Sep 17 00:00:00 2001 From: Mint Date: Sat, 2 Sep 2023 01:43:25 +0300 Subject: [PATCH 121/199] CommonAPI: Prevent users from accessing media of other users --- .../check-attachment-attribution.security | 1 + lib/pleroma/scheduled_activity.ex | 6 ++- lib/pleroma/web/common_api.ex | 12 ++++++ lib/pleroma/web/common_api/activity_draft.ex | 2 +- lib/pleroma/web/common_api/utils.ex | 27 ++++++------ test/pleroma/web/common_api/utils_test.exs | 43 +++++++++++++------ test/pleroma/web/common_api_test.exs | 18 ++++++++ .../views/scheduled_activity_view_test.exs | 2 +- .../chat_message_reference_view_test.exs | 2 +- 9 files changed, 82 insertions(+), 31 deletions(-) create mode 100644 changelog.d/check-attachment-attribution.security diff --git a/changelog.d/check-attachment-attribution.security b/changelog.d/check-attachment-attribution.security new file mode 100644 index 000000000..e0e46525b --- /dev/null +++ b/changelog.d/check-attachment-attribution.security @@ -0,0 +1 @@ +CommonAPI: Prevent users from accessing media of other users by creating a status with reused attachment ID diff --git a/lib/pleroma/scheduled_activity.ex b/lib/pleroma/scheduled_activity.ex index a7be58512..0ed51ad07 100644 --- a/lib/pleroma/scheduled_activity.ex +++ b/lib/pleroma/scheduled_activity.ex @@ -40,7 +40,11 @@ defp with_media_attachments( %{changes: %{params: %{"media_ids" => media_ids} = params}} = changeset ) when is_list(media_ids) do - media_attachments = Utils.attachments_from_ids(%{media_ids: media_ids}) + media_attachments = + Utils.attachments_from_ids( + %{media_ids: media_ids}, + User.get_cached_by_id(changeset.data.user_id) + ) params = params diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex index 77b3fa5d2..82c7f70d2 100644 --- a/lib/pleroma/web/common_api.ex +++ b/lib/pleroma/web/common_api.ex @@ -33,6 +33,7 @@ def block(blocker, blocked) do def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]), + :ok <- validate_chat_attachment_attribution(maybe_attachment, user), :ok <- validate_chat_content_length(content, !!maybe_attachment), {_, {:ok, chat_message_data, _meta}} <- {:build_object, @@ -71,6 +72,17 @@ defp format_chat_content(content) do text end + defp validate_chat_attachment_attribution(nil, _), do: :ok + + defp validate_chat_attachment_attribution(attachment, user) do + with :ok <- Object.authorize_access(attachment, user) do + :ok + else + e -> + e + end + end + defp validate_chat_content_length(_, true), do: :ok defp validate_chat_content_length(nil, false), do: {:error, :no_content} diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index 9af635da8..63ed48a27 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -111,7 +111,7 @@ defp full_payload(%{status: status, summary: summary} = draft) do end defp attachments(%{params: params} = draft) do - attachments = Utils.attachments_from_ids(params) + attachments = Utils.attachments_from_ids(params, draft.user) draft = %__MODULE__{draft | attachments: attachments} case Utils.validate_attachments_count(attachments) do diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index b9fe0224c..0f394e951 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -23,21 +23,21 @@ defmodule Pleroma.Web.CommonAPI.Utils do require Logger require Pleroma.Constants - def attachments_from_ids(%{media_ids: ids, descriptions: desc}) do - attachments_from_ids_descs(ids, desc) + def attachments_from_ids(%{media_ids: ids, descriptions: desc}, user) do + attachments_from_ids_descs(ids, desc, user) end - def attachments_from_ids(%{media_ids: ids}) do - attachments_from_ids_no_descs(ids) + def attachments_from_ids(%{media_ids: ids}, user) do + attachments_from_ids_no_descs(ids, user) end - def attachments_from_ids(_), do: [] + def attachments_from_ids(_, _), do: [] - def attachments_from_ids_no_descs([]), do: [] + def attachments_from_ids_no_descs([], _), do: [] - def attachments_from_ids_no_descs(ids) do + def attachments_from_ids_no_descs(ids, user) do Enum.map(ids, fn media_id -> - case get_attachment(media_id) do + case get_attachment(media_id, user) do %Object{data: data} -> data _ -> nil end @@ -45,22 +45,23 @@ def attachments_from_ids_no_descs(ids) do |> Enum.reject(&is_nil/1) end - def attachments_from_ids_descs([], _), do: [] + def attachments_from_ids_descs([], _, _), do: [] - def attachments_from_ids_descs(ids, descs_str) do + def attachments_from_ids_descs(ids, descs_str, user) do {_, descs} = Jason.decode(descs_str) Enum.map(ids, fn media_id -> - with %Object{data: data} <- get_attachment(media_id) do + with %Object{data: data} <- get_attachment(media_id, user) do Map.put(data, "name", descs[media_id]) end end) |> Enum.reject(&is_nil/1) end - defp get_attachment(media_id) do + defp get_attachment(media_id, user) do with %Object{data: data} = object <- Repo.get(Object, media_id), - %{"type" => type} when type in Pleroma.Constants.upload_object_types() <- data do + %{"type" => type} when type in Pleroma.Constants.upload_object_types() <- data, + :ok <- Object.authorize_access(object, user) do object else _ -> nil diff --git a/test/pleroma/web/common_api/utils_test.exs b/test/pleroma/web/common_api/utils_test.exs index ca5b92683..4ce039d64 100644 --- a/test/pleroma/web/common_api/utils_test.exs +++ b/test/pleroma/web/common_api/utils_test.exs @@ -586,46 +586,61 @@ test "returns recipients when object not found" do end end - describe "attachments_from_ids_descs/2" do + describe "attachments_from_ids_descs/3" do test "returns [] when attachment ids is empty" do - assert Utils.attachments_from_ids_descs([], "{}") == [] + assert Utils.attachments_from_ids_descs([], "{}", nil) == [] end test "returns list attachments with desc" do - object = insert(:attachment) + user = insert(:user) + object = insert(:attachment, %{user: user}) desc = Jason.encode!(%{object.id => "test-desc"}) - assert Utils.attachments_from_ids_descs(["#{object.id}", "34"], desc) == [ + assert Utils.attachments_from_ids_descs(["#{object.id}", "34"], desc, user) == [ Map.merge(object.data, %{"name" => "test-desc"}) ] end end - describe "attachments_from_ids/1" do + describe "attachments_from_ids/2" do test "returns attachments with descs" do - object = insert(:attachment) + user = insert(:user) + object = insert(:attachment, %{user: user}) desc = Jason.encode!(%{object.id => "test-desc"}) - assert Utils.attachments_from_ids(%{ - media_ids: ["#{object.id}"], - descriptions: desc - }) == [ + assert Utils.attachments_from_ids( + %{ + media_ids: ["#{object.id}"], + descriptions: desc + }, + user + ) == [ Map.merge(object.data, %{"name" => "test-desc"}) ] end test "returns attachments without descs" do - object = insert(:attachment) - assert Utils.attachments_from_ids(%{media_ids: ["#{object.id}"]}) == [object.data] + user = insert(:user) + object = insert(:attachment, %{user: user}) + assert Utils.attachments_from_ids(%{media_ids: ["#{object.id}"]}, user) == [object.data] end test "returns [] when not pass media_ids" do - assert Utils.attachments_from_ids(%{}) == [] + assert Utils.attachments_from_ids(%{}, nil) == [] + end + + test "returns [] when media_ids not belong to current user" do + user = insert(:user) + user2 = insert(:user) + + object = insert(:attachment, %{user: user}) + + assert Utils.attachments_from_ids(%{media_ids: ["#{object.id}"]}, user2) == [] end test "checks that the object is of upload type" do object = insert(:note) - assert Utils.attachments_from_ids(%{media_ids: ["#{object.id}"]}) == [] + assert Utils.attachments_from_ids(%{media_ids: ["#{object.id}"]}, nil) == [] end end diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index 968e11a14..0d76d6581 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -279,6 +279,24 @@ test "it reject messages via MRF" do assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} == CommonAPI.post_chat_message(author, recipient, "GNO/Linux") end + + test "it reject messages with attachments not belonging to user" do + author = insert(:user) + not_author = insert(:user) + recipient = author + + attachment = insert(:attachment, %{user: not_author}) + + {:error, message} = + CommonAPI.post_chat_message( + author, + recipient, + "123", + media_id: attachment.id + ) + + assert message == :forbidden + end end describe "unblocking" do diff --git a/test/pleroma/web/mastodon_api/views/scheduled_activity_view_test.exs b/test/pleroma/web/mastodon_api/views/scheduled_activity_view_test.exs index e5e510d33..07a65a3bc 100644 --- a/test/pleroma/web/mastodon_api/views/scheduled_activity_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/scheduled_activity_view_test.exs @@ -48,7 +48,7 @@ test "A scheduled activity with a media attachment" do id: to_string(scheduled_activity.id), media_attachments: %{media_ids: [upload.id]} - |> Utils.attachments_from_ids() + |> Utils.attachments_from_ids(user) |> Enum.map(&StatusView.render("attachment.json", %{attachment: &1})), params: %{ in_reply_to_id: to_string(activity.id), diff --git a/test/pleroma/web/pleroma_api/views/chat_message_reference_view_test.exs b/test/pleroma/web/pleroma_api/views/chat_message_reference_view_test.exs index 017c9c5c0..7ab3f5acd 100644 --- a/test/pleroma/web/pleroma_api/views/chat_message_reference_view_test.exs +++ b/test/pleroma/web/pleroma_api/views/chat_message_reference_view_test.exs @@ -24,7 +24,7 @@ test "it displays a chat message" do filename: "an_image.jpg" } - {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) + {:ok, upload} = ActivityPub.upload(file, actor: recipient.ap_id) {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "kippis :firefox:", idempotency_key: "123") From 535a5ecad04c9c49105a77e7025fe9f4b4d23ba6 Mon Sep 17 00:00:00 2001 From: Mint Date: Sat, 2 Sep 2023 01:43:25 +0300 Subject: [PATCH 122/199] CommonAPI: Prevent users from accessing media of other users commit 1afde067b12ad0062c1820091ea9b0a680819281 upstream. --- .../check-attachment-attribution.security | 1 + lib/pleroma/scheduled_activity.ex | 6 ++- lib/pleroma/web/common_api.ex | 12 ++++++ lib/pleroma/web/common_api/activity_draft.ex | 2 +- lib/pleroma/web/common_api/utils.ex | 31 ++++++++------ test/pleroma/web/common_api/utils_test.exs | 41 +++++++++++++------ test/pleroma/web/common_api_test.exs | 18 ++++++++ .../views/scheduled_activity_view_test.exs | 2 +- .../chat_message_reference_view_test.exs | 2 +- 9 files changed, 85 insertions(+), 30 deletions(-) create mode 100644 changelog.d/check-attachment-attribution.security diff --git a/changelog.d/check-attachment-attribution.security b/changelog.d/check-attachment-attribution.security new file mode 100644 index 000000000..e0e46525b --- /dev/null +++ b/changelog.d/check-attachment-attribution.security @@ -0,0 +1 @@ +CommonAPI: Prevent users from accessing media of other users by creating a status with reused attachment ID diff --git a/lib/pleroma/scheduled_activity.ex b/lib/pleroma/scheduled_activity.ex index a7be58512..0ed51ad07 100644 --- a/lib/pleroma/scheduled_activity.ex +++ b/lib/pleroma/scheduled_activity.ex @@ -40,7 +40,11 @@ defp with_media_attachments( %{changes: %{params: %{"media_ids" => media_ids} = params}} = changeset ) when is_list(media_ids) do - media_attachments = Utils.attachments_from_ids(%{media_ids: media_ids}) + media_attachments = + Utils.attachments_from_ids( + %{media_ids: media_ids}, + User.get_cached_by_id(changeset.data.user_id) + ) params = params diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex index 89cc0d6fe..44eb00075 100644 --- a/lib/pleroma/web/common_api.ex +++ b/lib/pleroma/web/common_api.ex @@ -33,6 +33,7 @@ def block(blocker, blocked) do def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]), + :ok <- validate_chat_attachment_attribution(maybe_attachment, user), :ok <- validate_chat_content_length(content, !!maybe_attachment), {_, {:ok, chat_message_data, _meta}} <- {:build_object, @@ -71,6 +72,17 @@ defp format_chat_content(content) do text end + defp validate_chat_attachment_attribution(nil, _), do: :ok + + defp validate_chat_attachment_attribution(attachment, user) do + with :ok <- Object.authorize_access(attachment, user) do + :ok + else + e -> + e + end + end + defp validate_chat_content_length(_, true), do: :ok defp validate_chat_content_length(nil, false), do: {:error, :no_content} diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index 9af635da8..63ed48a27 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -111,7 +111,7 @@ defp full_payload(%{status: status, summary: summary} = draft) do end defp attachments(%{params: params} = draft) do - attachments = Utils.attachments_from_ids(params) + attachments = Utils.attachments_from_ids(params, draft.user) draft = %__MODULE__{draft | attachments: attachments} case Utils.validate_attachments_count(attachments) do diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index ff0814329..6410815ea 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -23,21 +23,21 @@ defmodule Pleroma.Web.CommonAPI.Utils do require Logger require Pleroma.Constants - def attachments_from_ids(%{media_ids: ids, descriptions: desc}) do - attachments_from_ids_descs(ids, desc) + def attachments_from_ids(%{media_ids: ids, descriptions: desc}, user) do + attachments_from_ids_descs(ids, desc, user) end - def attachments_from_ids(%{media_ids: ids}) do - attachments_from_ids_no_descs(ids) + def attachments_from_ids(%{media_ids: ids}, user) do + attachments_from_ids_no_descs(ids, user) end - def attachments_from_ids(_), do: [] + def attachments_from_ids(_, _), do: [] - def attachments_from_ids_no_descs([]), do: [] + def attachments_from_ids_no_descs([], _), do: [] - def attachments_from_ids_no_descs(ids) do + def attachments_from_ids_no_descs(ids, user) do Enum.map(ids, fn media_id -> - case get_attachment(media_id) do + case get_attachment(media_id, user) do %Object{data: data} -> data _ -> nil end @@ -45,21 +45,26 @@ def attachments_from_ids_no_descs(ids) do |> Enum.reject(&is_nil/1) end - def attachments_from_ids_descs([], _), do: [] + def attachments_from_ids_descs([], _, _), do: [] - def attachments_from_ids_descs(ids, descs_str) do + def attachments_from_ids_descs(ids, descs_str, user) do {_, descs} = Jason.decode(descs_str) Enum.map(ids, fn media_id -> - with %Object{data: data} <- get_attachment(media_id) do + with %Object{data: data} <- get_attachment(media_id, user) do Map.put(data, "name", descs[media_id]) end end) |> Enum.reject(&is_nil/1) end - defp get_attachment(media_id) do - Repo.get(Object, media_id) + defp get_attachment(media_id, user) do + with %Object{data: _data} = object <- Repo.get(Object, media_id), + :ok <- Object.authorize_access(object, user) do + object + else + _ -> nil + end end @spec get_to_and_cc(ActivityDraft.t()) :: {list(String.t()), list(String.t())} diff --git a/test/pleroma/web/common_api/utils_test.exs b/test/pleroma/web/common_api/utils_test.exs index d309c6ded..c52d3e9c5 100644 --- a/test/pleroma/web/common_api/utils_test.exs +++ b/test/pleroma/web/common_api/utils_test.exs @@ -586,41 +586,56 @@ test "returns recipients when object not found" do end end - describe "attachments_from_ids_descs/2" do + describe "attachments_from_ids_descs/3" do test "returns [] when attachment ids is empty" do - assert Utils.attachments_from_ids_descs([], "{}") == [] + assert Utils.attachments_from_ids_descs([], "{}", nil) == [] end test "returns list attachments with desc" do - object = insert(:note) + user = insert(:user) + object = insert(:note, %{user: user}) desc = Jason.encode!(%{object.id => "test-desc"}) - assert Utils.attachments_from_ids_descs(["#{object.id}", "34"], desc) == [ + assert Utils.attachments_from_ids_descs(["#{object.id}", "34"], desc, user) == [ Map.merge(object.data, %{"name" => "test-desc"}) ] end end - describe "attachments_from_ids/1" do + describe "attachments_from_ids/2" do test "returns attachments with descs" do - object = insert(:note) + user = insert(:user) + object = insert(:note, %{user: user}) desc = Jason.encode!(%{object.id => "test-desc"}) - assert Utils.attachments_from_ids(%{ - media_ids: ["#{object.id}"], - descriptions: desc - }) == [ + assert Utils.attachments_from_ids( + %{ + media_ids: ["#{object.id}"], + descriptions: desc + }, + user + ) == [ Map.merge(object.data, %{"name" => "test-desc"}) ] end test "returns attachments without descs" do - object = insert(:note) - assert Utils.attachments_from_ids(%{media_ids: ["#{object.id}"]}) == [object.data] + user = insert(:user) + object = insert(:note, %{user: user}) + assert Utils.attachments_from_ids(%{media_ids: ["#{object.id}"]}, user) == [object.data] end test "returns [] when not pass media_ids" do - assert Utils.attachments_from_ids(%{}) == [] + assert Utils.attachments_from_ids(%{}, nil) == [] + end + + test "returns [] when media_ids not belong to current user" do + user = insert(:user) + user2 = insert(:user) + + object = insert(:attachment, %{user: user}) + + assert Utils.attachments_from_ids(%{media_ids: ["#{object.id}"]}, user2) == [] end end diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index 5c9103e9f..e60691995 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -279,6 +279,24 @@ test "it reject messages via MRF" do assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} == CommonAPI.post_chat_message(author, recipient, "GNO/Linux") end + + test "it reject messages with attachments not belonging to user" do + author = insert(:user) + not_author = insert(:user) + recipient = author + + attachment = insert(:attachment, %{user: not_author}) + + {:error, message} = + CommonAPI.post_chat_message( + author, + recipient, + "123", + media_id: attachment.id + ) + + assert message == :forbidden + end end describe "unblocking" do diff --git a/test/pleroma/web/mastodon_api/views/scheduled_activity_view_test.exs b/test/pleroma/web/mastodon_api/views/scheduled_activity_view_test.exs index e5e510d33..07a65a3bc 100644 --- a/test/pleroma/web/mastodon_api/views/scheduled_activity_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/scheduled_activity_view_test.exs @@ -48,7 +48,7 @@ test "A scheduled activity with a media attachment" do id: to_string(scheduled_activity.id), media_attachments: %{media_ids: [upload.id]} - |> Utils.attachments_from_ids() + |> Utils.attachments_from_ids(user) |> Enum.map(&StatusView.render("attachment.json", %{attachment: &1})), params: %{ in_reply_to_id: to_string(activity.id), diff --git a/test/pleroma/web/pleroma_api/views/chat_message_reference_view_test.exs b/test/pleroma/web/pleroma_api/views/chat_message_reference_view_test.exs index 017c9c5c0..7ab3f5acd 100644 --- a/test/pleroma/web/pleroma_api/views/chat_message_reference_view_test.exs +++ b/test/pleroma/web/pleroma_api/views/chat_message_reference_view_test.exs @@ -24,7 +24,7 @@ test "it displays a chat message" do filename: "an_image.jpg" } - {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) + {:ok, upload} = ActivityPub.upload(file, actor: recipient.ap_id) {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "kippis :firefox:", idempotency_key: "123") From 385492577d11e9667064d7f7e0dacdc00457064a Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Fri, 23 Dec 2022 18:46:14 +0100 Subject: [PATCH 123/199] mix: version 2.5.5 --- CHANGELOG.md | 7 ++++++- mix.exs | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d9aadc6e..32ec440de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Removed -## 2.5.54 +## 2.5.5 + +## Security +- Prevent users from accessing media of other users by creating a status with reused attachment ID + +## 2.5.4 ## Security - Fix XML External Entity (XXE) loading vulnerability allowing to fetch arbitary files from the server's filesystem diff --git a/mix.exs b/mix.exs index 12f721364..e2aac0fc5 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do def project do [ app: :pleroma, - version: version("2.5.4"), + version: version("2.5.5"), elixir: "~> 1.11", elixirc_paths: elixirc_paths(Mix.env()), compilers: [:phoenix, :gettext] ++ Mix.compilers(), From 31eb3dc24587ad3715da9fe00886867a6c0bf0c4 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 16:41:30 -0600 Subject: [PATCH 124/199] ObjectValidators: accept "quoteUrl" field --- .../web/activity_pub/object_validators/common_fields.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex index d580208df..835ed97b7 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex @@ -27,7 +27,7 @@ defmacro activity_fields do end end - # All objects except Answer and CHatMessage + # All objects except Answer and ChatMessage defmacro object_fields do quote bind_quoted: binding() do field(:content, :string) @@ -58,6 +58,7 @@ defmacro status_object_fields do field(:like_count, :integer, default: 0) field(:announcement_count, :integer, default: 0) field(:inReplyTo, ObjectValidators.ObjectID) + field(:quoteUrl, ObjectValidators.ObjectID) field(:url, ObjectValidators.BareUri) field(:likes, {:array, ObjectValidators.ObjectID}, default: []) From 7deda1fa18cb95a588ba66950ac45262c467a7f4 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 17:30:49 -0600 Subject: [PATCH 125/199] Quote post: add fixtures --- .../quote_post/fedibird_quote_post.json | 52 +++++++++++++++++++ .../quote_post/misskey_quote_post.json | 46 ++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 test/fixtures/quote_post/fedibird_quote_post.json create mode 100644 test/fixtures/quote_post/misskey_quote_post.json diff --git a/test/fixtures/quote_post/fedibird_quote_post.json b/test/fixtures/quote_post/fedibird_quote_post.json new file mode 100644 index 000000000..ebf383356 --- /dev/null +++ b/test/fixtures/quote_post/fedibird_quote_post.json @@ -0,0 +1,52 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + { + "ostatus": "http://ostatus.org#", + "atomUri": "ostatus:atomUri", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "conversation": "ostatus:conversation", + "sensitive": "as:sensitive", + "toot": "http://joinmastodon.org/ns#", + "votersCount": "toot:votersCount", + "expiry": "toot:expiry" + } + ], + "id": "https://fedibird.com/users/noellabo/statuses/107663670404015196", + "type": "Note", + "summary": null, + "inReplyTo": null, + "published": "2022-01-22T02:07:16Z", + "url": "https://fedibird.com/@noellabo/107663670404015196", + "attributedTo": "https://fedibird.com/users/noellabo", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "https://fedibird.com/users/noellabo/followers" + ], + "sensitive": false, + "atomUri": "https://fedibird.com/users/noellabo/statuses/107663670404015196", + "inReplyToAtomUri": null, + "conversation": "tag:fedibird.com,2022-01-22:objectId=107663670404038002:objectType=Conversation", + "context": "https://fedibird.com/contexts/107663670404038002", + "quoteURL": "https://misskey.io/notes/8vsn2izjwh", + "_misskey_quote": "https://misskey.io/notes/8vsn2izjwh", + "_misskey_content": "いつの生まれだシトリン", + "content": "

いつの生まれだシトリン
QT: https://misskey.io/notes/8vsn2izjwh

", + "contentMap": { + "ja": "

いつの生まれだシトリン
QT: https://misskey.io/notes/8vsn2izjwh

" + }, + "attachment": [], + "tag": [], + "replies": { + "id": "https://fedibird.com/users/noellabo/statuses/107663670404015196/replies", + "type": "Collection", + "first": { + "type": "CollectionPage", + "next": "https://fedibird.com/users/noellabo/statuses/107663670404015196/replies?only_other_accounts=true&page=true", + "partOf": "https://fedibird.com/users/noellabo/statuses/107663670404015196/replies", + "items": [] + } + } +} diff --git a/test/fixtures/quote_post/misskey_quote_post.json b/test/fixtures/quote_post/misskey_quote_post.json new file mode 100644 index 000000000..59f677ca9 --- /dev/null +++ b/test/fixtures/quote_post/misskey_quote_post.json @@ -0,0 +1,46 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "sensitive": "as:sensitive", + "Hashtag": "as:Hashtag", + "quoteUrl": "as:quoteUrl", + "toot": "http://joinmastodon.org/ns#", + "Emoji": "toot:Emoji", + "featured": "toot:featured", + "discoverable": "toot:discoverable", + "schema": "http://schema.org#", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value", + "misskey": "https://misskey.io/ns#", + "_misskey_content": "misskey:_misskey_content", + "_misskey_quote": "misskey:_misskey_quote", + "_misskey_reaction": "misskey:_misskey_reaction", + "_misskey_votes": "misskey:_misskey_votes", + "_misskey_talk": "misskey:_misskey_talk", + "isCat": "misskey:isCat", + "vcard": "http://www.w3.org/2006/vcard/ns#" + } + ], + "id": "https://misskey.io/notes/8vs6ylpfez", + "type": "Note", + "attributedTo": "https://misskey.io/users/7rkrarq81i", + "summary": null, + "content": "

投稿者の設定によるね
Fanboxについても投稿者によっては過去の投稿は高額なプランに移動してることがある

RE:
https://misskey.io/notes/8vs6wxufd0

", + "_misskey_content": "投稿者の設定によるね\nFanboxについても投稿者によっては過去の投稿は高額なプランに移動してることがある", + "_misskey_quote": "https://misskey.io/notes/8vs6wxufd0", + "quoteUrl": "https://misskey.io/notes/8vs6wxufd0", + "published": "2022-01-21T16:38:30.243Z", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "https://misskey.io/users/7rkrarq81i/followers" + ], + "inReplyTo": null, + "attachment": [], + "sensitive": false, + "tag": [] +} From 795736af16dca77929725e7dd55f5de04a796fdb Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 18:03:22 -0600 Subject: [PATCH 126/199] ObjectValidators: improve quoteUrl compatibility --- .../article_note_page_validator.ex | 16 +++++++++++++++ .../article_note_page_validator_test.exs | 20 +++++++++++++++++++ 2 files changed, 36 insertions(+) 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 2670e3f17..40bb67934 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 @@ -76,6 +76,21 @@ def fix_attachments(%{"attachment" => attachment} = data) when is_map(attachment def fix_attachments(data), do: data + defp fix_quote_url(%{"quoteUrl" => _quote_url} = data), do: data + + # Fix for Fedibird + # https://github.com/fedibird/mastodon/issues/9 + defp fix_quote_url(%{"quoteURL" => quote_url} = data) do + Map.put(data, "quoteUrl", quote_url) + end + + # Misskey fallback + defp fix_quote_url(%{"_misskey_quote" => quote_url} = data) do + Map.put(data, "quoteUrl", quote_url) + end + + defp fix_quote_url(data), do: data + defp fix(data) do data |> CommonFixes.fix_actor() @@ -84,6 +99,7 @@ defp fix(data) do |> fix_tag() |> fix_replies() |> fix_attachments() + |> fix_quote_url() |> Transmogrifier.fix_emoji() |> Transmogrifier.fix_content_map() end diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs index c7a62be18..c3cde00b5 100644 --- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs @@ -116,4 +116,24 @@ test "a Note without replies/first/items validates" do %{valid?: true} = ArticleNotePageValidator.cast_and_validate(note) end + + test "Fedibird quote post" do + insert(:user, ap_id: "https://fedibird.com/users/noellabo") + + data = File.read!("test/fixtures/quote_post/fedibird_quote_post.json") |> Jason.decode!() + chg = ArticleNotePageValidator.cast_and_validate(data) + + assert chg.valid? + assert chg.changes.quoteUrl == "https://misskey.io/notes/8vsn2izjwh" + end + + test "Misskey quote post" do + insert(:user, ap_id: "https://misskey.io/users/7rkrarq81i") + + data = File.read!("test/fixtures/quote_post/misskey_quote_post.json") |> Jason.decode!() + chg = ArticleNotePageValidator.cast_and_validate(data) + + assert chg.valid? + assert chg.changes.quoteUrl == "https://misskey.io/notes/8vs6wxufd0" + end end From b022d6635dad4b2769fbf1fd4b97f77a4cc646b4 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 18:46:58 -0600 Subject: [PATCH 127/199] Transmogrifier: fetch quoted post --- .../web/activity_pub/transmogrifier.ex | 17 +++++ test/fixtures/tesla_mock/aimu@misskey.io.json | 64 +++++++++++++++++++ .../tesla_mock/misskey.io_8vs6wxufd0.json | 44 +++++++++++++ .../web/activity_pub/transmogrifier_test.exs | 22 +++++++ test/support/http_request_mock.ex | 18 ++++++ 5 files changed, 165 insertions(+) create mode 100644 test/fixtures/tesla_mock/aimu@misskey.io.json create mode 100644 test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 0e6c429f9..c466271ca 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -166,6 +166,22 @@ def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options) def fix_in_reply_to(object, _options), do: object + def fix_quote(object, options \\ []) + + def fix_quote(%{"quoteUrl" => quote_url} = object, options) + when not is_nil(quote_url) do + with {:ok, quoted_object} <- get_obj_helper(quote_url, options), + %Activity{} <- Activity.get_create_by_object_ap_id(quoted_object.data["id"]) do + Map.put(object, "quoteUrl", quoted_object.data["id"]) + else + e -> + Logger.warn("Couldn't fetch #{inspect(quote_url)}, error: #{inspect(e)}") + object + end + end + + def fix_quote(object, _options), do: object + defp prepare_in_reply_to(in_reply_to) do cond do is_bitstring(in_reply_to) -> @@ -454,6 +470,7 @@ def handle_incoming( |> strip_internal_fields() |> fix_type(fetch_options) |> fix_in_reply_to(fetch_options) + |> fix_quote(fetch_options) data = Map.put(data, "object", object) options = Keyword.put(options, :local, false) diff --git a/test/fixtures/tesla_mock/aimu@misskey.io.json b/test/fixtures/tesla_mock/aimu@misskey.io.json new file mode 100644 index 000000000..9ff4cb6d0 --- /dev/null +++ b/test/fixtures/tesla_mock/aimu@misskey.io.json @@ -0,0 +1,64 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "sensitive": "as:sensitive", + "Hashtag": "as:Hashtag", + "quoteUrl": "as:quoteUrl", + "toot": "http://joinmastodon.org/ns#", + "Emoji": "toot:Emoji", + "featured": "toot:featured", + "discoverable": "toot:discoverable", + "schema": "http://schema.org#", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value", + "misskey": "https://misskey.io/ns#", + "_misskey_content": "misskey:_misskey_content", + "_misskey_quote": "misskey:_misskey_quote", + "_misskey_reaction": "misskey:_misskey_reaction", + "_misskey_votes": "misskey:_misskey_votes", + "_misskey_talk": "misskey:_misskey_talk", + "isCat": "misskey:isCat", + "vcard": "http://www.w3.org/2006/vcard/ns#" + } + ], + "type": "Person", + "id": "https://misskey.io/users/83ssedkv53", + "inbox": "https://misskey.io/users/83ssedkv53/inbox", + "outbox": "https://misskey.io/users/83ssedkv53/outbox", + "followers": "https://misskey.io/users/83ssedkv53/followers", + "following": "https://misskey.io/users/83ssedkv53/following", + "sharedInbox": "https://misskey.io/inbox", + "endpoints": { + "sharedInbox": "https://misskey.io/inbox" + }, + "url": "https://misskey.io/@aimu", + "preferredUsername": "aimu", + "name": "あいむ", + "summary": "

わずかな作曲要素 巣穴で独り言
Twitter
https://twitter.com/aimu_53
Soundcloud
https://soundcloud.com/aimu-53

", + "icon": { + "type": "Image", + "url": "https://s3.arkjp.net/misskey/webpublic-3f7e93c0-34f5-443c-acc0-f415cb2342b4.jpg", + "sensitive": false, + "name": null + }, + "image": { + "type": "Image", + "url": "https://s3.arkjp.net/misskey/webpublic-2db63d1d-490b-488b-ab62-c93c285f26b6.png", + "sensitive": false, + "name": null + }, + "tag": [], + "manuallyApprovesFollowers": false, + "discoverable": true, + "publicKey": { + "id": "https://misskey.io/users/83ssedkv53#main-key", + "type": "Key", + "owner": "https://misskey.io/users/83ssedkv53", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1ylhePJ6qGHmwHSBP17b\nIosxGaiFKvgDBgZdm8vzvKeRSqJV9uLHfZL3pO/Zt02EwaZd2GohZAtBZEF8DbMA\n3s93WAesvyGF9mjGrYYKlhp/glwyrrrbf+RdD0DLtyDwRRlrxp3pS2lLmv5Tp1Zl\npH+UKpOnNrpQqjHI5P+lEc9bnflzbRrX+UiyLNsVAP80v4wt7SZfT/telrU6mDru\n998UdfhUo7bDKeDsHG1PfLpyhhtfdoZub4kBpkyacHiwAd+CdCjR54Eu7FDwVK3p\nY3JcrT2q5stgMqN1m4QgSL4XAADIotWwDYttTJejM1n9dr+6VWv5bs0F2Q/6gxOp\nu5DQZLk4Q+64U4LWNox6jCMOq3fYe0g7QalJIHnanYQQo+XjoH6S1Aw64gQ3Ip2Y\nZBmZREAOR7GMFVDPFnVnsbCHnIAv16TdgtLgQBAihkWEUuPqITLi8PMu6kMr3uyq\nYkObEfH0TNTcqaiVpoXv791GZLEUV5ROl0FSUANLNkHZZv29xZ5JDOBOR1rNBLyH\ngVtW8rpszYqOXwzX23hh4WsVXfB7YgNvIijwjiaWbzsecleaENGEnLNMiVKVumTj\nmtyTeFJpH0+OaSrUYpemRRJizmqIjklKsNwUEwUb2WcUUg92o56T2obrBkooabZe\nwgSXSKTOcjsR/ju7+AuIyvkCAwEAAQ==\n-----END PUBLIC KEY-----\n" + }, + "isCat": true, + "vcard:bday": "5353-05-03" +} diff --git a/test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json b/test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json new file mode 100644 index 000000000..323ca10ed --- /dev/null +++ b/test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json @@ -0,0 +1,44 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "sensitive": "as:sensitive", + "Hashtag": "as:Hashtag", + "quoteUrl": "as:quoteUrl", + "toot": "http://joinmastodon.org/ns#", + "Emoji": "toot:Emoji", + "featured": "toot:featured", + "discoverable": "toot:discoverable", + "schema": "http://schema.org#", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value", + "misskey": "https://misskey.io/ns#", + "_misskey_content": "misskey:_misskey_content", + "_misskey_quote": "misskey:_misskey_quote", + "_misskey_reaction": "misskey:_misskey_reaction", + "_misskey_votes": "misskey:_misskey_votes", + "_misskey_talk": "misskey:_misskey_talk", + "isCat": "misskey:isCat", + "vcard": "http://www.w3.org/2006/vcard/ns#" + } + ], + "id": "https://misskey.io/notes/8vs6wxufd0", + "type": "Note", + "attributedTo": "https://misskey.io/users/83ssedkv53", + "summary": null, + "content": "

Fantiaこれできないように過去のやつは従量課金だった気がする

", + "_misskey_content": "Fantiaこれできないように過去のやつは従量課金だった気がする", + "published": "2022-01-21T16:37:12.663Z", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "https://misskey.io/users/83ssedkv53/followers" + ], + "inReplyTo": null, + "attachment": [], + "sensitive": false, + "tag": [] +} diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs index 3e0c8dc65..2c8e5ba21 100644 --- a/test/pleroma/web/activity_pub/transmogrifier_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs @@ -136,6 +136,28 @@ test "it drops link tags" do tag = object.data["tag"] |> List.first() assert tag["type"] == "Mention" end + + test "it accepts quote posts" do + insert(:user, ap_id: "https://misskey.io/users/7rkrarq81i") + + object = File.read!("test/fixtures/quote_post/misskey_quote_post.json") |> Jason.decode!() + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Create", + "actor" => "https://misskey.io/users/7rkrarq81i", + "object" => object + } + + assert {:ok, activity} = Transmogrifier.handle_incoming(message) + + # Object was created in the database + object = Object.normalize(activity) + assert object.data["quoteUrl"] == "https://misskey.io/notes/8vs6wxufd0" + + # It fetched the quoted post + assert Object.normalize("https://misskey.io/notes/8vs6wxufd0") + end end describe "prepare outgoing" do diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index b0cf613ac..78a367024 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -1380,6 +1380,15 @@ def get("https://gleasonator.com/objects/102eb097-a18b-4cd5-abfc-f952efcb70bb", }} end + def get("https://misskey.io/users/83ssedkv53", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/aimu@misskey.io.json"), + headers: activitypub_object_headers() + }} + end + def get("https://gleasonator.com/users/macgirvin", _, _, _) do {:ok, %Tesla.Env{ @@ -1446,6 +1455,15 @@ def get("https://p.helene.moe/objects/fd5910ac-d9dc-412e-8d1d-914b203296c4", _, }} end + def get("https://misskey.io/notes/8vs6wxufd0", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json"), + headers: activitypub_object_headers() + }} + end + def get(url, query, body, headers) do {:error, "Mock response not implemented for GET #{inspect(url)}, #{query}, #{inspect(body)}, #{inspect(headers)}"} From cc4badaf60462fdb8bb57225437e3dd360ee0dfb Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 19:14:39 -0600 Subject: [PATCH 128/199] Transmogrifier: fix quoteUrl here too --- .../web/activity_pub/transmogrifier.ex | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index c466271ca..f5771e75e 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -166,9 +166,9 @@ def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options) def fix_in_reply_to(object, _options), do: object - def fix_quote(object, options \\ []) + def fix_quote_url(object, options \\ []) - def fix_quote(%{"quoteUrl" => quote_url} = object, options) + def fix_quote_url(%{"quoteUrl" => quote_url} = object, options) when not is_nil(quote_url) do with {:ok, quoted_object} <- get_obj_helper(quote_url, options), %Activity{} <- Activity.get_create_by_object_ap_id(quoted_object.data["id"]) do @@ -180,7 +180,22 @@ def fix_quote(%{"quoteUrl" => quote_url} = object, options) end end - def fix_quote(object, _options), do: object + # Fix for Fedibird + # https://github.com/fedibird/mastodon/issues/9 + def fix_quote_url(%{"quoteURL" => quote_url} = object, options) do + object + |> Map.put("quoteUrl", quote_url) + |> fix_quote_url(options) + end + + # Misskey fallback + def fix_quote_url(%{"_misskey_quote" => quote_url} = object, options) do + object + |> Map.put("quoteUrl", quote_url) + |> fix_quote_url(options) + end + + def fix_quote_url(object, _options), do: object defp prepare_in_reply_to(in_reply_to) do cond do @@ -470,7 +485,7 @@ def handle_incoming( |> strip_internal_fields() |> fix_type(fetch_options) |> fix_in_reply_to(fetch_options) - |> fix_quote(fetch_options) + |> fix_quote_url(fetch_options) data = Map.put(data, "object", object) options = Keyword.put(options, :local, false) From ce5eb3172321f0ef2ae85d7819b44cc8241a5581 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 19:47:08 -0600 Subject: [PATCH 129/199] StatusView: show quoted posts through the API, probably --- .../web/mastodon_api/views/status_view.ex | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index dea22f9c2..b966a84d0 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -57,6 +57,27 @@ defp get_replied_to_activities(activities) do end) end + defp get_quoted_activities([]), do: %{} + + defp get_quoted_activities(activities) do + activities + |> Enum.map(fn + %{data: %{"type" => "Create"}} = activity -> + object = Object.normalize(activity, fetch: false) + object && object.data["quoteUrl"] != "" && object.data["quoteUrl"] + + _ -> + nil + end) + |> Enum.filter(& &1) + |> Activity.create_by_object_ap_id_with_object() + |> Repo.all() + |> Enum.reduce(%{}, fn activity, acc -> + object = Object.normalize(activity, fetch: false) + if object, do: Map.put(acc, object.data["id"], activity), else: acc + end) + end + # DEPRECATED This field seems to be a left-over from the StatusNet era. # If your application uses `pleroma.conversation_id`: this field is deprecated. # It is currently stubbed instead by doing a CRC32 of the context, and @@ -97,6 +118,7 @@ def render("index.json", opts) do # length(activities_with_links) * timeout fetch_rich_media_for_activities(activities) replied_to_activities = get_replied_to_activities(activities) + quoted_activities = get_quoted_activities(activities) parent_activities = activities @@ -129,6 +151,7 @@ def render("index.json", opts) do opts = opts |> Map.put(:replied_to_activities, replied_to_activities) + |> Map.put(:quoted_activities, quoted_activities) |> Map.put(:parent_activities, parent_activities) |> Map.put(:relationships, relationships_opt) @@ -277,7 +300,6 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} end reply_to = get_reply_to(activity, opts) - reply_to_user = reply_to && CommonAPI.get_user(reply_to.data["actor"]) history_len = @@ -290,6 +312,8 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} # Here the implicit index of the current content is 0 chrono_order = history_len - 1 + quote_activity = get_quote(activity, opts) + content = object |> render_content() @@ -398,6 +422,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} conversation_id: get_context_id(activity), context: object.data["context"], in_reply_to_account_acct: reply_to_user && reply_to_user.nickname, + quote_id: quote_activity && to_string(quote_activity.id), content: %{"text/plain" => content_plaintext}, spoiler_text: %{"text/plain" => summary}, expires_at: expires_at, @@ -633,6 +658,21 @@ def get_reply_to(%{data: %{"object" => _object}} = activity, _) do end end + def get_quote(activity, %{quoted_activities: quoted_activities}) do + object = Object.normalize(activity, fetch: false) + quoted_activities[object.data["quoteUrl"]] + end + + def get_quote(%{data: %{"object" => _object}} = activity, _) do + object = Object.normalize(activity, fetch: false) + + if object.data["quoteUrl"] && object.data["quoteUrl"] != "" do + Activity.get_create_by_object_ap_id(object.data["quoteUrl"]) + else + nil + end + end + def render_content(%{data: %{"name" => name}} = object) when not is_nil(name) and name != "" do url = object.data["url"] || object.data["id"] From 0d9c443e51c85d9ded3e20954c9620f7a9d2361e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 20:05:58 -0600 Subject: [PATCH 130/199] StatusView: render the whole quoted status --- lib/pleroma/web/api_spec/schemas/status.ex | 5 +++++ lib/pleroma/web/mastodon_api/views/status_view.ex | 10 +++++++++- .../web/mastodon_api/views/status_view_test.exs | 1 + 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex index bc29cf4a6..39241aa39 100644 --- a/lib/pleroma/web/api_spec/schemas/status.ex +++ b/lib/pleroma/web/api_spec/schemas/status.ex @@ -193,6 +193,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do nullable: true, description: "The `acct` property of User entity for replied user (if any)" }, + quote: %Schema{ + allOf: [%OpenApiSpex.Reference{"$ref": "#/components/schemas/Status"}], + nullable: true, + description: "Quoted status (if any)" + }, local: %Schema{ type: :boolean, description: "`true` if the post was made on the local instance" diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index b966a84d0..5bde1ce04 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -314,6 +314,14 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} quote_activity = get_quote(activity, opts) + quote_post = + if quote_activity do + quote_rendering_opts = Map.put(opts, :activity, quote_activity) + render("show.json", quote_rendering_opts) + else + nil + end + content = object |> render_content() @@ -422,7 +430,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} conversation_id: get_context_id(activity), context: object.data["context"], in_reply_to_account_acct: reply_to_user && reply_to_user.nickname, - quote_id: quote_activity && to_string(quote_activity.id), + quote: quote_post, content: %{"text/plain" => content_plaintext}, spoiler_text: %{"text/plain" => summary}, expires_at: expires_at, diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs index b93335190..b10b0f0b9 100644 --- a/test/pleroma/web/mastodon_api/views/status_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs @@ -326,6 +326,7 @@ test "a note activity" do conversation_id: convo_id, context: object_data["context"], in_reply_to_account_acct: nil, + quote: nil, content: %{"text/plain" => HTML.strip_tags(object_data["content"])}, spoiler_text: %{"text/plain" => HTML.strip_tags(object_data["summary"])}, expires_at: nil, From 6ac19c3999c543e5a26bbf04932a6a7aaa447b99 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 21:27:05 -0600 Subject: [PATCH 131/199] ActivityDraft: create quote posts --- lib/pleroma/web/common_api/activity_draft.ex | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index 63ed48a27..375aabc91 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -22,6 +22,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do attachments: [], in_reply_to: nil, in_reply_to_conversation: nil, + quote_post: nil, visibility: nil, expires_at: nil, extra: nil, @@ -53,6 +54,7 @@ def create(user, params) do |> poll() |> with_valid(&in_reply_to/1) |> with_valid(&in_reply_to_conversation/1) + |> with_valid("e_post/1) |> with_valid(&visibility/1) |> content() |> with_valid(&to_and_cc/1) @@ -132,6 +134,18 @@ defp in_reply_to(%{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} defp in_reply_to(draft), do: draft + defp quote_post(%{params: %{quote_id: ""}} = draft), do: draft + + defp quote_post(%{params: %{quote_id: id}} = draft) when is_binary(id) do + %__MODULE__{draft | quote_post: Activity.get_by_id(id)} + end + + defp quote_post(%{params: %{quote_id: %Activity{} = quote_post}} = draft) do + %__MODULE__{draft | quote_post: quote_post} + end + + defp quote_post(draft), do: draft + defp in_reply_to_conversation(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} From d4fea8b5595e9e6cd37bdb1cee21285f905693f1 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 22:15:54 -0600 Subject: [PATCH 132/199] ActivityDraft: allow quoting --- lib/pleroma/web/activity_pub/builder.ex | 11 +++++++++++ .../web/api_spec/operations/status_operation.ex | 7 ++++++- test/pleroma/web/common_api_test.exs | 12 ++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index 8eab3a241..eb0bb0e33 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -217,6 +217,7 @@ def note(%ActivityDraft{} = draft) do "tag" => Keyword.values(draft.tags) |> Enum.uniq() } |> add_in_reply_to(draft.in_reply_to) + |> add_quote(draft.quote_post) |> Map.merge(draft.extra) {:ok, data, []} @@ -232,6 +233,16 @@ defp add_in_reply_to(object, in_reply_to) do end end + defp add_quote(object, nil), do: object + + defp add_quote(object, quote_post) do + with %Object{} = quote_object <- Object.normalize(quote_post, fetch: false) do + Map.put(object, "quoteUrl", quote_object.data["id"]) + else + _ -> object + end + end + def chat_message(actor, recipient, content, opts \\ []) do basic = %{ "id" => Utils.generate_object_id(), diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex index 5d6e82f3c..8fa3b0890 100644 --- a/lib/pleroma/web/api_spec/operations/status_operation.ex +++ b/lib/pleroma/web/api_spec/operations/status_operation.ex @@ -581,7 +581,12 @@ defp create_request do type: :string, description: "Will reply to a given conversation, addressing only the people who are part of the recipient set of that conversation. Sets the visibility to `direct`." - } + }, + quote_id: %Schema{ + nullable: true, + allOf: [FlakeID], + description: "ID of the status being quoted, if any" + }, }, example: %{ "status" => "What time is it?", diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index 0d76d6581..09df27acb 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -796,6 +796,18 @@ test "it can handle activities that expire" do scheduled_at: expires_at ) end + + test "it allows allows quote posting" do + user = insert(:user) + + {:ok, quoted} = CommonAPI.post(user, %{status: "Hello world"}) + {:ok, quote_post} = CommonAPI.post(user, %{status: "nice post", quote_id: quoted.id}) + + quoted = Object.normalize(quoted) + quote_post = Object.normalize(quote_post) + + assert quote_post.data["quoteUrl"] == quoted.data["id"] + end end describe "reactions" do From c20e90e898affc9d00d8b7d6b71f11157f5c7837 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 22:29:13 -0600 Subject: [PATCH 133/199] BuilderTest: build quote post --- .../pleroma/web/activity_pub/builder_test.exs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/pleroma/web/activity_pub/builder_test.exs b/test/pleroma/web/activity_pub/builder_test.exs index eb175a1be..52058a0a3 100644 --- a/test/pleroma/web/activity_pub/builder_test.exs +++ b/test/pleroma/web/activity_pub/builder_test.exs @@ -44,5 +44,34 @@ test "returns note data" do assert {:ok, ^expected, []} = Builder.note(draft) end + + test "quote post" do + user = insert(:user) + note = insert(:note) + + draft = %ActivityDraft{ + user: user, + context: "2hu", + content_html: "

This is :moominmamma: note

", + quote_post: note, + extra: %{} + } + + expected = %{ + "actor" => user.ap_id, + "attachment" => [], + "content" => "

This is :moominmamma: note

", + "context" => "2hu", + "sensitive" => false, + "type" => "Note", + "quoteUrl" => note.data["id"], + "cc" => [], + "summary" => nil, + "tag" => [], + "to" => [] + } + + assert {:ok, ^expected, []} = Builder.note(draft) + end end end From 3a8b5d90df6de502debaee4d670211bcf64ad1db Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 22:35:08 -0600 Subject: [PATCH 134/199] StatusControllerTest: test creating a quote post --- .../controllers/status_controller_test.exs | 19 +++++++++++++++++++ 1 file changed, 19 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 76c289ee7..ba49256b5 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -125,6 +125,25 @@ test "posting a status", %{conn: conn} do ) end + test "posting a quote post", %{conn: conn} do + user = insert(:user) + + {:ok, %{id: activity_id}} = CommonAPI.post(user, %{status: "yolo"}) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "indeed", + "quote_id" => activity_id + }) + + assert %{"id" => id, "pleroma" => %{"quote" => %{"id" => ^activity_id}}} = + json_response_and_validate_schema(conn, 200) + + assert Activity.get_by_id(id) + end + test "it fails to create a status if `expires_in` is less or equal than an hour", %{ conn: conn } do From cbd1760efac872c00edad15f352ffe4d2e0e1e12 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 22:41:57 -0600 Subject: [PATCH 135/199] TransmogrifierTest: prepare an outgoing quote post --- .../pleroma/web/activity_pub/transmogrifier_test.exs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs index 2c8e5ba21..824398e38 100644 --- a/test/pleroma/web/activity_pub/transmogrifier_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs @@ -372,6 +372,18 @@ test "Updates of Notes are handled" do } } = prepared["object"] end + + test "it prepares a quote post" do + user = insert(:user) + + {:ok, quoted_post} = CommonAPI.post(user, %{status: "hey"}) + {:ok, quote_post} = CommonAPI.post(user, %{status: "hey", quote_id: quoted_post.id}) + + {:ok, modified} = Transmogrifier.prepare_outgoing(quote_post.data) + + quoted_post = Object.normalize(quoted_post) + assert modified["object"]["quoteUrl"] == quoted_post.data["id"] + end end describe "actor rewriting" do From 96009739173e5e48a636bb964855eb7aea11c828 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 22:57:42 -0600 Subject: [PATCH 136/199] mix format --- lib/pleroma/web/api_spec/operations/status_operation.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex index 8fa3b0890..c133a3aac 100644 --- a/lib/pleroma/web/api_spec/operations/status_operation.ex +++ b/lib/pleroma/web/api_spec/operations/status_operation.ex @@ -586,7 +586,7 @@ defp create_request do nullable: true, allOf: [FlakeID], description: "ID of the status being quoted, if any" - }, + } }, example: %{ "status" => "What time is it?", From f4ccdfd5033e7b1136ae0fe4e41dba78d83e80cf Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 23:02:44 -0600 Subject: [PATCH 137/199] Fix typos --- .../article_note_page_validator_test.exs | 12 ++++++------ test/pleroma/web/common_api_test.exs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs index c3cde00b5..dec2e28c9 100644 --- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs @@ -121,19 +121,19 @@ test "Fedibird quote post" do insert(:user, ap_id: "https://fedibird.com/users/noellabo") data = File.read!("test/fixtures/quote_post/fedibird_quote_post.json") |> Jason.decode!() - chg = ArticleNotePageValidator.cast_and_validate(data) + cng = ArticleNotePageValidator.cast_and_validate(data) - assert chg.valid? - assert chg.changes.quoteUrl == "https://misskey.io/notes/8vsn2izjwh" + assert cng.valid? + assert cng.changes.quoteUrl == "https://misskey.io/notes/8vsn2izjwh" end test "Misskey quote post" do insert(:user, ap_id: "https://misskey.io/users/7rkrarq81i") data = File.read!("test/fixtures/quote_post/misskey_quote_post.json") |> Jason.decode!() - chg = ArticleNotePageValidator.cast_and_validate(data) + cng = ArticleNotePageValidator.cast_and_validate(data) - assert chg.valid? - assert chg.changes.quoteUrl == "https://misskey.io/notes/8vs6wxufd0" + assert cng.valid? + assert cng.changes.quoteUrl == "https://misskey.io/notes/8vs6wxufd0" end end diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index 09df27acb..734e6dd82 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -797,7 +797,7 @@ test "it can handle activities that expire" do ) end - test "it allows allows quote posting" do + test "it allows quote posting" do user = insert(:user) {:ok, quoted} = CommonAPI.post(user, %{status: "Hello world"}) From 5716f88a1d8424cf7c62a0491b3bf9607dc9aa3f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 23:09:33 -0600 Subject: [PATCH 138/199] InstanceView: add "quote_posting" feature --- lib/pleroma/web/mastodon_api/views/instance_view.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index efd2a0af6..1b01d7371 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -69,6 +69,7 @@ def features do "multifetch", "pleroma:api/v1/notifications:include_types_filter", "editing", + "quote_posting", if Config.get([:activitypub, :blockers_visible]) do "blockers_visible" end, From db46abce477b83b252e0ad87f74ac9266e9cb118 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 23:29:55 -0600 Subject: [PATCH 139/199] @context: add quoteUrl --- priv/static/schemas/litepub-0.1.jsonld | 1 + 1 file changed, 1 insertion(+) diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld index 650118475..5d8244a11 100644 --- a/priv/static/schemas/litepub-0.1.jsonld +++ b/priv/static/schemas/litepub-0.1.jsonld @@ -26,6 +26,7 @@ "@id": "litepub:listMessage", "@type": "@id" }, + "quoteUrl": "as:quoteUrl", "oauthRegistrationEndpoint": { "@id": "litepub:oauthRegistrationEndpoint", "@type": "@id" From 80ab2572a4d5590d738cc763a87156b3f79362fb Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 23 Jan 2022 13:55:25 -0600 Subject: [PATCH 140/199] Return quote_url through the API, don't render quotes more than 1 level deep --- lib/pleroma/web/api_spec/schemas/status.ex | 6 +++++ .../web/mastodon_api/views/status_view.ex | 7 +++++- .../controllers/status_controller_test.exs | 9 +++++--- .../mastodon_api/views/status_view_test.exs | 23 +++++++++++++++++++ 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex index 39241aa39..f4ee9b38c 100644 --- a/lib/pleroma/web/api_spec/schemas/status.ex +++ b/lib/pleroma/web/api_spec/schemas/status.ex @@ -198,6 +198,12 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do nullable: true, description: "Quoted status (if any)" }, + quote_url: %Schema{ + type: :string, + format: :uri, + nullable: true, + description: "URL of the quoted status" + }, local: %Schema{ type: :boolean, description: "`true` if the post was made on the local instance" diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 5bde1ce04..06adfb221 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -316,7 +316,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} quote_post = if quote_activity do - quote_rendering_opts = Map.put(opts, :activity, quote_activity) + quote_rendering_opts = Map.merge(opts, %{activity: quote_activity, show_quote: false}) render("show.json", quote_rendering_opts) else nil @@ -431,6 +431,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} context: object.data["context"], in_reply_to_account_acct: reply_to_user && reply_to_user.nickname, quote: quote_post, + quote_url: object.data["quoteUrl"], content: %{"text/plain" => content_plaintext}, spoiler_text: %{"text/plain" => summary}, expires_at: expires_at, @@ -666,6 +667,10 @@ def get_reply_to(%{data: %{"object" => _object}} = activity, _) do end end + def get_quote(_activity, %{show_quote: false}) do + nil + end + def get_quote(activity, %{quoted_activities: quoted_activities}) do object = Object.normalize(activity, fetch: false) quoted_activities[object.data["quoteUrl"]] 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 ba49256b5..de3b52e26 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -128,7 +128,8 @@ test "posting a status", %{conn: conn} do test "posting a quote post", %{conn: conn} do user = insert(:user) - {:ok, %{id: activity_id}} = CommonAPI.post(user, %{status: "yolo"}) + {:ok, %{id: activity_id} = activity} = CommonAPI.post(user, %{status: "yolo"}) + %{data: %{"id" => quote_url}} = Object.normalize(activity) conn = conn @@ -138,8 +139,10 @@ test "posting a quote post", %{conn: conn} do "quote_id" => activity_id }) - assert %{"id" => id, "pleroma" => %{"quote" => %{"id" => ^activity_id}}} = - json_response_and_validate_schema(conn, 200) + assert %{ + "id" => id, + "pleroma" => %{"quote" => %{"id" => ^activity_id}, "quote_url" => ^quote_url} + } = json_response_and_validate_schema(conn, 200) assert Activity.get_by_id(id) end diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs index b10b0f0b9..f50b02799 100644 --- a/test/pleroma/web/mastodon_api/views/status_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs @@ -327,6 +327,7 @@ test "a note activity" do context: object_data["context"], in_reply_to_account_acct: nil, quote: nil, + quote_url: nil, content: %{"text/plain" => HTML.strip_tags(object_data["content"])}, spoiler_text: %{"text/plain" => HTML.strip_tags(object_data["summary"])}, expires_at: nil, @@ -423,6 +424,28 @@ test "a reply" do assert status.in_reply_to_id == to_string(note.id) end + test "a quote post" do + post = insert(:note_activity) + user = insert(:user) + + {:ok, quote_post} = CommonAPI.post(user, %{status: "he", quote_id: post.id}) + {:ok, quoted_quote_post} = CommonAPI.post(user, %{status: "yo", quote_id: quote_post.id}) + + status = StatusView.render("show.json", %{activity: quoted_quote_post}) + + assert status.pleroma.quote.id == to_string(quote_post.id) + assert status.pleroma.quote_url == Object.normalize(quote_post).data["id"] + + # Quotes don't go more than one level deep + refute status.pleroma.quote.pleroma.quote + assert status.pleroma.quote.pleroma.quote_url == Object.normalize(post).data["id"] + + # In an index + [status] = StatusView.render("index.json", %{activities: [quoted_quote_post], as: :activity}) + + assert status.pleroma.quote.id == to_string(quote_post.id) + end + test "contains mentions" do user = insert(:user) mentioned = insert(:user) From 54a989793878c63900d2c6de7b4ffc8fbd8fe8c6 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 23 Jan 2022 15:46:44 -0600 Subject: [PATCH 141/199] ActivityDraft: mention the OP of a quoted post --- lib/pleroma/web/common_api/activity_draft.ex | 19 +++++++++++-------- test/pleroma/web/common_api_test.exs | 12 ++++++++++++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index 375aabc91..588aba55e 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -137,11 +137,11 @@ defp in_reply_to(draft), do: draft defp quote_post(%{params: %{quote_id: ""}} = draft), do: draft defp quote_post(%{params: %{quote_id: id}} = draft) when is_binary(id) do - %__MODULE__{draft | quote_post: Activity.get_by_id(id)} - end - - defp quote_post(%{params: %{quote_id: %Activity{} = quote_post}} = draft) do - %__MODULE__{draft | quote_post: quote_post} + with %Activity{actor: actor_ap_id} = activity <- Activity.get_by_id(id) do + %__MODULE__{draft | quote_post: activity, mentions: [actor_ap_id]} + else + _ -> draft + end end defp quote_post(draft), do: draft @@ -178,12 +178,15 @@ defp poll(draft) do end end - defp content(draft) do + defp content(%{mentions: mentions} = draft) do {content_html, mentioned_users, tags} = Utils.make_content_html(draft) + mentioned_ap_ids = + Enum.map(mentioned_users, fn {_, mentioned_user} -> mentioned_user.ap_id end) + mentions = - mentioned_users - |> Enum.map(fn {_, mentioned_user} -> mentioned_user.ap_id end) + mentions + |> Kernel.++(mentioned_ap_ids) |> Utils.get_addressed_users(draft.params[:to]) %__MODULE__{draft | content_html: content_html, mentions: mentions, tags: tags} diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index 734e6dd82..051e770d7 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -807,6 +807,18 @@ test "it allows quote posting" do quote_post = Object.normalize(quote_post) assert quote_post.data["quoteUrl"] == quoted.data["id"] + + # The OP is mentioned + assert quoted.data["actor"] in quote_post.data["to"] + end + + test "quote posting with explicit addressing doesn't mention the OP" do + user = insert(:user) + + {:ok, quoted} = CommonAPI.post(user, %{status: "Hello world"}) + {:ok, quote_post} = CommonAPI.post(user, %{status: "nice post", quote_id: quoted.id, to: []}) + + assert Object.normalize(quote_post).data["to"] == [Pleroma.Constants.as_public()] end end From 1f19dd76f66ca657ddfe79a51e59b8997a4c6321 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 23 Jan 2022 16:03:46 -0600 Subject: [PATCH 142/199] ActivityDraft: mix format, defensive actor ID --- lib/pleroma/web/common_api/activity_draft.ex | 16 ++++++++++------ test/pleroma/web/common_api_test.exs | 4 +++- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index 588aba55e..95534f3cb 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -11,6 +11,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do alias Pleroma.Web.CommonAPI.Utils import Pleroma.Web.Gettext + import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1] defstruct valid?: true, errors: [], @@ -134,13 +135,16 @@ defp in_reply_to(%{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} defp in_reply_to(draft), do: draft - defp quote_post(%{params: %{quote_id: ""}} = draft), do: draft + defp quote_post(%{params: %{quote_id: id}} = draft) when not_empty_string(id) do + case Activity.get_by_id(id) do + %Activity{actor: actor_ap_id} = activity when not_empty_string(actor_ap_id) -> + %__MODULE__{draft | quote_post: activity, mentions: [actor_ap_id]} - defp quote_post(%{params: %{quote_id: id}} = draft) when is_binary(id) do - with %Activity{actor: actor_ap_id} = activity <- Activity.get_by_id(id) do - %__MODULE__{draft | quote_post: activity, mentions: [actor_ap_id]} - else - _ -> draft + %Activity{} = activity -> + %__MODULE__{draft | quote_post: activity} + + _ -> + draft end end diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index 051e770d7..960d0cf16 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -816,7 +816,9 @@ test "quote posting with explicit addressing doesn't mention the OP" do user = insert(:user) {:ok, quoted} = CommonAPI.post(user, %{status: "Hello world"}) - {:ok, quote_post} = CommonAPI.post(user, %{status: "nice post", quote_id: quoted.id, to: []}) + + {:ok, quote_post} = + CommonAPI.post(user, %{status: "nice post", quote_id: quoted.id, to: []}) assert Object.normalize(quote_post).data["to"] == [Pleroma.Constants.as_public()] end From 36a5578d2b16ba9f771fff55daafa85ec606a6be Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 24 Jan 2022 15:34:23 -0600 Subject: [PATCH 143/199] Scrubber.Default: allow span.quote-inline for quote post compatibility --- priv/scrubbers/default.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/priv/scrubbers/default.ex b/priv/scrubbers/default.ex index d1215d2e0..56324a9fa 100644 --- a/priv/scrubbers/default.ex +++ b/priv/scrubbers/default.ex @@ -60,7 +60,7 @@ defmodule Pleroma.HTML.Scrubber.Default do Meta.allow_tag_with_these_attributes(:u, ["lang"]) Meta.allow_tag_with_these_attributes(:ul, ["lang"]) - Meta.allow_tag_with_this_attribute_values(:span, "class", ["h-card", "recipients-inline"]) + Meta.allow_tag_with_this_attribute_values(:span, "class", ["h-card", "recipients-inline", "quote-inline"]) Meta.allow_tag_with_these_attributes(:span, ["lang"]) Meta.allow_tag_with_this_attribute_values(:code, "class", ["inline"]) From 57ef1d121101d785c043ef6aaf2d33bb9be3ec3b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 24 Jan 2022 16:44:35 -0600 Subject: [PATCH 144/199] Add InlineQuotePolicy to force quote URLs inline --- config/config.exs | 2 + docs/configuration/cheatsheet.md | 4 ++ .../activity_pub/mrf/inline_quote_policy.ex | 53 ++++++++++++++++++ .../mrf/inline_quote_policy_test.exs | 56 +++++++++++++++++++ 4 files changed, 115 insertions(+) create mode 100644 lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex create mode 100644 test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs diff --git a/config/config.exs b/config/config.exs index ebcbf8b49..56cc34db5 100644 --- a/config/config.exs +++ b/config/config.exs @@ -434,6 +434,8 @@ config :pleroma, :mrf_follow_bot, follower_nickname: nil +config :pleroma, :mrf_inline_quote, prefix: "RT" + config :pleroma, :rich_media, enabled: true, ignore_hosts: [], diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index f43cde114..32cc5811a 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -160,6 +160,7 @@ To add configuration to your config file, you can copy it from the base config. * `Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy`: Drops follow requests from followbots. Users can still allow bots to follow them by first following the bot. * `Pleroma.Web.ActivityPub.MRF.KeywordPolicy`: Rejects or removes from the federated timeline or replaces keywords. (See [`:mrf_keyword`](#mrf_keyword)). * `Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent`: Forces every mentioned user to be reflected in the post content. + * `Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy`: Forces quote post URLs to be reflected in the message content inline. * `transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo). * `transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value. @@ -267,6 +268,9 @@ Notes: * `federated_timeline_removal_url`: A list of patterns which result in message with emojis whose URLs match being removed from federated timelines (a.k.a unlisted). This will apply only to statuses. Each pattern can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html). * `federated_timeline_removal_shortcode`: A list of patterns which result in message with emojis whose shortcodes match being removed from federated timelines (a.k.a unlisted). This will apply only to statuses. Each pattern can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html). +#### :mrf_inline_quote +* `prefix`: Prefix before the link (default: `RT`) + ### :activitypub * `unfollow_blocked`: Whether blocks result in people getting unfollowed * `outgoing_blocks`: Whether to federate blocks to other instances diff --git a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex new file mode 100644 index 000000000..0f1dc9f42 --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex @@ -0,0 +1,53 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do + @moduledoc "Force a quote line into the message content." + @behaviour Pleroma.Web.ActivityPub.MRF.Policy + + defp build_inline_quote(prefix, url) do + "

#{prefix}: #{url}
" + end + + defp filter_object(%{"quoteUrl" => quote_url} = object) do + content = object["content"] || "" + + if content =~ quote_url do + object + else + prefix = Pleroma.Config.get([:mrf_inline_quote, :prefix]) + content = content <> build_inline_quote(prefix, quote_url) + Map.put(object, "content", content) + end + end + + @impl true + def filter(%{"object" => %{"quoteUrl" => _} = object} = activity) do + {:ok, Map.put(activity, "object", filter_object(object))} + end + + @impl true + def filter(object), do: {:ok, object} + + @impl true + def describe, do: {:ok, %{}} + + @impl true + def config_description do + %{ + key: :mrf_inline_quote, + related_policy: "Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy", + label: "MRF Inline Quote", + description: "Force quote post URLs inline", + children: [ + %{ + key: :prefix, + type: :string, + description: "Prefix before the link", + suggestions: ["RT", "QT", "RE", "RN"] + } + ] + } + end +end diff --git a/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs new file mode 100644 index 000000000..81dc06dda --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs @@ -0,0 +1,56 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicyTest do + alias Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy + use Pleroma.DataCase + + test "adds quote URL to post content" do + quote_url = "https://gleasonator.com/objects/1234" + + activity = %{ + "type" => "Create", + "actor" => "https://gleasonator.com/users/alex", + "object" => %{ + "type" => "Note", + "content" => "

Nice post

", + "quoteUrl" => quote_url + } + } + + {:ok, %{"object" => %{"content" => filtered}}} = InlineQuotePolicy.filter(activity) + + assert filtered == + "

Nice post



RT: https://gleasonator.com/objects/1234
" + end + + test "ignores Misskey quote posts" do + object = File.read!("test/fixtures/quote_post/misskey_quote_post.json") |> Jason.decode!() + + activity = %{ + "type" => "Create", + "actor" => "https://misskey.io/users/7rkrarq81i", + "object" => object + } + + {:ok, filtered} = InlineQuotePolicy.filter(activity) + assert filtered == activity + end + + test "ignores Fedibird quote posts" do + object = File.read!("test/fixtures/quote_post/fedibird_quote_post.json") |> Jason.decode!() + + # Normally the ObjectValidator will fix this before it reaches MRF + object = Map.put(object, "quoteUrl", object["quoteURL"]) + + activity = %{ + "type" => "Create", + "actor" => "https://fedibird.com/users/noellabo", + "object" => object + } + + {:ok, filtered} = InlineQuotePolicy.filter(activity) + assert filtered == activity + end +end From 59326247aa754991add9170e204257a8bf94c40f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 26 Jan 2022 11:21:49 -0600 Subject: [PATCH 145/199] CommonAPI: disallow quoting private posts through the API --- lib/pleroma/web/common_api/activity_draft.ex | 15 ++++++++++- .../web/common_api/activity_draft_test.exs | 26 +++++++++++++++++++ test/pleroma/web/common_api_test.exs | 14 ++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 test/pleroma/web/common_api/activity_draft_test.exs diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index 95534f3cb..d4875765c 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do alias Pleroma.Conversation.Participation alias Pleroma.Object alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI.Utils @@ -57,6 +58,7 @@ def create(user, params) do |> with_valid(&in_reply_to_conversation/1) |> with_valid("e_post/1) |> with_valid(&visibility/1) + |> with_valid("ing_visibility/1) |> content() |> with_valid(&to_and_cc/1) |> with_valid(&context/1) @@ -136,7 +138,7 @@ defp in_reply_to(%{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} defp in_reply_to(draft), do: draft defp quote_post(%{params: %{quote_id: id}} = draft) when not_empty_string(id) do - case Activity.get_by_id(id) do + case Activity.get_by_id_with_object(id) do %Activity{actor: actor_ap_id} = activity when not_empty_string(actor_ap_id) -> %__MODULE__{draft | quote_post: activity, mentions: [actor_ap_id]} @@ -165,6 +167,17 @@ defp visibility(%{params: params} = draft) do end end + defp quoting_visibility(%{quote_post: %Activity{}} = draft) do + with %Object{} = object <- Object.normalize(draft.quote_post, fetch: false), + visibility when visibility in ~w(public unlisted) <- Visibility.get_visibility(object) do + draft + else + _ -> add_error(draft, dgettext("errors", "Cannot quote private message")) + end + end + + defp quoting_visibility(draft), do: draft + defp expires_at(draft) do case CommonAPI.check_expiry_date(draft.params[:expires_in]) do {:ok, expires_at} -> %__MODULE__{draft | expires_at: expires_at} diff --git a/test/pleroma/web/common_api/activity_draft_test.exs b/test/pleroma/web/common_api/activity_draft_test.exs new file mode 100644 index 000000000..8a09fc710 --- /dev/null +++ b/test/pleroma/web/common_api/activity_draft_test.exs @@ -0,0 +1,26 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.CommonAPI.ActivityDraftTest do + use Pleroma.DataCase + + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.CommonAPI.ActivityDraft + + import Pleroma.Factory + + test "create/2 with a quote post" do + user = insert(:user) + + {:ok, direct} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) + {:ok, private} = CommonAPI.post(user, %{status: ".", visibility: "private"}) + {:ok, unlisted} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"}) + {:ok, public} = CommonAPI.post(user, %{status: ".", visibility: "public"}) + + {:error, _} = ActivityDraft.create(user, %{status: "nice", quote_id: direct.id}) + {:error, _} = ActivityDraft.create(user, %{status: "nice", quote_id: private.id}) + {:ok, _} = ActivityDraft.create(user, %{status: "nice", quote_id: unlisted.id}) + {:ok, _} = ActivityDraft.create(user, %{status: "nice", quote_id: public.id}) + end +end diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index 960d0cf16..c4eba8b9c 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -822,6 +822,20 @@ test "quote posting with explicit addressing doesn't mention the OP" do assert Object.normalize(quote_post).data["to"] == [Pleroma.Constants.as_public()] end + + test "quote posting visibility" do + user = insert(:user) + + {:ok, direct} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) + {:ok, private} = CommonAPI.post(user, %{status: ".", visibility: "private"}) + {:ok, unlisted} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"}) + {:ok, public} = CommonAPI.post(user, %{status: ".", visibility: "public"}) + + {:error, _} = CommonAPI.post(user, %{status: "nice", quote_id: direct.id}) + {:error, _} = CommonAPI.post(user, %{status: "nice", quote_id: private.id}) + {:ok, _} = CommonAPI.post(user, %{status: "nice", quote_id: unlisted.id}) + {:ok, _} = CommonAPI.post(user, %{status: "nice", quote_id: public.id}) + end end describe "reactions" do From 6f11f11519f9c735f6b059c250f4bf01e09b305f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 26 Jan 2022 11:49:31 -0600 Subject: [PATCH 146/199] StatusView: fix quote visibility --- .../web/mastodon_api/views/status_view.ex | 2 +- .../mastodon_api/views/status_view_test.exs | 41 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 06adfb221..7360d1093 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -315,7 +315,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} quote_activity = get_quote(activity, opts) quote_post = - if quote_activity do + if visible_for_user?(quote_activity, opts[:for]) do quote_rendering_opts = Map.merge(opts, %{activity: quote_activity, show_quote: false}) render("show.json", quote_rendering_opts) else diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs index f50b02799..f41ef580d 100644 --- a/test/pleroma/web/mastodon_api/views/status_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs @@ -446,6 +446,47 @@ test "a quote post" do assert status.pleroma.quote.id == to_string(quote_post.id) end + test "quoted private post" do + user = insert(:user) + + # Insert a private post + private = insert(:followers_only_note_activity, user: user) + private_object = Object.normalize(private) + + # Create a public post quoting the private post + quote_private = + insert(:note_activity, note: insert(:note, data: %{"quoteUrl" => private_object.data["id"]})) + + status = StatusView.render("show.json", %{activity: quote_private}) + + # The quote isn't rendered + refute status.pleroma.quote + assert status.pleroma.quote_url == private_object.data["id"] + + # After following the user, the quote is rendered + follower = insert(:user) + CommonAPI.follow(follower, user) + + status = StatusView.render("show.json", %{activity: quote_private, for: follower}) + assert status.pleroma.quote.id == to_string(private.id) + end + + test "quoted direct message" do + # Insert a direct message + direct = insert(:direct_note_activity) + direct_object = Object.normalize(direct) + + # Create a public post quoting the direct message + quote_direct = + insert(:note_activity, note: insert(:note, data: %{"quoteUrl" => direct_object.data["id"]})) + + status = StatusView.render("show.json", %{activity: quote_direct}) + + # The quote isn't rendered + refute status.pleroma.quote + assert status.pleroma.quote_url == direct_object.data["id"] + end + test "contains mentions" do user = insert(:user) mentioned = insert(:user) From 74e0a4555f583a6962ad116bf6e54f06e42fe465 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 26 Jan 2022 11:52:50 -0600 Subject: [PATCH 147/199] StatusView: add `quote_visible` param --- lib/pleroma/web/api_spec/schemas/status.ex | 4 ++++ lib/pleroma/web/mastodon_api/views/status_view.ex | 1 + test/pleroma/web/mastodon_api/views/status_view_test.exs | 4 ++++ 3 files changed, 9 insertions(+) diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex index f4ee9b38c..5d0eedb08 100644 --- a/lib/pleroma/web/api_spec/schemas/status.ex +++ b/lib/pleroma/web/api_spec/schemas/status.ex @@ -204,6 +204,10 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do nullable: true, description: "URL of the quoted status" }, + quote_visible: %Schema{ + type: :boolean, + description: "`true` if the quoted post is visible to the user" + }, local: %Schema{ type: :boolean, description: "`true` if the post was made on the local instance" diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 7360d1093..2aa44b0f6 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -432,6 +432,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} in_reply_to_account_acct: reply_to_user && reply_to_user.nickname, quote: quote_post, quote_url: object.data["quoteUrl"], + quote_visible: visible_for_user?(quote_activity, opts[:for]), content: %{"text/plain" => content_plaintext}, spoiler_text: %{"text/plain" => summary}, expires_at: expires_at, diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs index f41ef580d..ed0a87558 100644 --- a/test/pleroma/web/mastodon_api/views/status_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs @@ -328,6 +328,7 @@ test "a note activity" do in_reply_to_account_acct: nil, quote: nil, quote_url: nil, + quote_visible: false, content: %{"text/plain" => HTML.strip_tags(object_data["content"])}, spoiler_text: %{"text/plain" => HTML.strip_tags(object_data["summary"])}, expires_at: nil, @@ -462,6 +463,7 @@ test "quoted private post" do # The quote isn't rendered refute status.pleroma.quote assert status.pleroma.quote_url == private_object.data["id"] + refute status.pleroma.quote_visible # After following the user, the quote is rendered follower = insert(:user) @@ -469,6 +471,7 @@ test "quoted private post" do status = StatusView.render("show.json", %{activity: quote_private, for: follower}) assert status.pleroma.quote.id == to_string(private.id) + assert status.pleroma.quote_visible end test "quoted direct message" do @@ -485,6 +488,7 @@ test "quoted direct message" do # The quote isn't rendered refute status.pleroma.quote assert status.pleroma.quote_url == direct_object.data["id"] + refute status.pleroma.quote_visible end test "contains mentions" do From bee7e419597615ac6852942fe563166feba3fe73 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 27 Jan 2022 14:28:06 -0600 Subject: [PATCH 148/199] InlineQuotePolicy: don't add line breaks to markdown posts --- .../activity_pub/mrf/inline_quote_policy.ex | 12 ++++++++--- .../mrf/inline_quote_policy_test.exs | 21 ++++++++++++++++++- 2 files changed, 29 insertions(+), 4 deletions(-) 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 0f1dc9f42..46013fc5e 100644 --- a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex @@ -6,8 +6,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do @moduledoc "Force a quote line into the message content." @behaviour Pleroma.Web.ActivityPub.MRF.Policy - defp build_inline_quote(prefix, url) do - "

#{prefix}: #{url}
" + defp build_inline_quote(prefix, url, br) do + "#{String.duplicate("
", br)}#{prefix}: #{url}
" end defp filter_object(%{"quoteUrl" => quote_url} = object) do @@ -17,7 +17,13 @@ defp filter_object(%{"quoteUrl" => quote_url} = object) do object else prefix = Pleroma.Config.get([:mrf_inline_quote, :prefix]) - content = content <> build_inline_quote(prefix, quote_url) + + inline_quote = + if String.ends_with?(content, "

"), + do: build_inline_quote(prefix, quote_url, 0), + else: build_inline_quote(prefix, quote_url, 2) + + content = content <> inline_quote Map.put(object, "content", content) end end diff --git a/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs index 81dc06dda..8e75aaaab 100644 --- a/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs @@ -9,6 +9,25 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicyTest do test "adds quote URL to post content" do quote_url = "https://gleasonator.com/objects/1234" + activity = %{ + "type" => "Create", + "actor" => "https://gleasonator.com/users/alex", + "object" => %{ + "type" => "Note", + "content" => "Nice post", + "quoteUrl" => quote_url + } + } + + {:ok, %{"object" => %{"content" => filtered}}} = InlineQuotePolicy.filter(activity) + + assert filtered == + "Nice post

RT: https://gleasonator.com/objects/1234
" + end + + test "doesn't add line breaks to markdown posts" do + quote_url = "https://gleasonator.com/objects/1234" + activity = %{ "type" => "Create", "actor" => "https://gleasonator.com/users/alex", @@ -22,7 +41,7 @@ test "adds quote URL to post content" do {:ok, %{"object" => %{"content" => filtered}}} = InlineQuotePolicy.filter(activity) assert filtered == - "

Nice post



RT: https://gleasonator.com/objects/1234
" + "

Nice post

RT: https://gleasonator.com/objects/1234" end test "ignores Misskey quote posts" do From 93e4972b50f9bb0a07a7074fbab2aedbdc0cc4eb Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 27 Jan 2022 15:01:20 -0600 Subject: [PATCH 149/199] Add InlineQuotePolicy as a default MRF --- config/config.exs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/config/config.exs b/config/config.exs index 56cc34db5..9149e925a 100644 --- a/config/config.exs +++ b/config/config.exs @@ -860,7 +860,11 @@ config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: false config :pleroma, :mrf, - policies: [Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy, Pleroma.Web.ActivityPub.MRF.TagPolicy], + policies: [ + Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy, + Pleroma.Web.ActivityPub.MRF.TagPolicy, + Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy + ], transparency: true, transparency_exclusions: [] From cf8e4258830e3f362ab1e54238d622f6b2056502 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 28 Jan 2022 12:33:07 -0600 Subject: [PATCH 150/199] StatusView: return quote post inside a reblog --- lib/pleroma/web/mastodon_api/views/status_view.ex | 10 ++++++---- .../web/mastodon_api/views/status_view_test.exs | 12 ++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 2aa44b0f6..ba4a8f3eb 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -668,13 +668,15 @@ def get_reply_to(%{data: %{"object" => _object}} = activity, _) do end end - def get_quote(_activity, %{show_quote: false}) do - nil - end + def get_quote(_activity, %{show_quote: false}), do: nil def get_quote(activity, %{quoted_activities: quoted_activities}) do object = Object.normalize(activity, fetch: false) - quoted_activities[object.data["quoteUrl"]] + + with nil <- quoted_activities[object.data["quoteUrl"]] do + # For when a quote post is inside an Announce + Activity.get_create_by_object_ap_id_with_object(object.data["quoteUrl"]) + end end def get_quote(%{data: %{"object" => _object}} = activity, _) do diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs index ed0a87558..6d3a72970 100644 --- a/test/pleroma/web/mastodon_api/views/status_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs @@ -491,6 +491,18 @@ test "quoted direct message" do refute status.pleroma.quote_visible end + test "repost of quote post" do + post = insert(:note_activity) + user = insert(:user) + + {:ok, quote_post} = CommonAPI.post(user, %{status: "he", quote_id: post.id}) + {:ok, repost} = CommonAPI.repeat(quote_post.id, user) + + [status] = StatusView.render("index.json", %{activities: [repost], as: :activity}) + + assert status.reblog.pleroma.quote.id == to_string(post.id) + end + test "contains mentions" do user = insert(:user) mentioned = insert(:user) From 3c8319fe9f7a7c793f8fdc347be2015190981e33 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 28 Jan 2022 14:06:32 -0600 Subject: [PATCH 151/199] Transmogrifier: federate quotes with _misskey_quote field --- lib/pleroma/web/activity_pub/transmogrifier.ex | 9 +++++++++ priv/static/schemas/litepub-0.1.jsonld | 2 ++ test/pleroma/web/activity_pub/transmogrifier_test.exs | 4 ++++ 3 files changed, 15 insertions(+) diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index f5771e75e..163ae54fa 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -660,6 +660,14 @@ def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_r def set_reply_to_uri(obj), do: obj + # Misskey quotes + # Despite being underscored, it's potentially more reliable for interop. + def set_quote_url(%{"quoteUrl" => quote_url} = object) when is_binary(quote_url) do + Map.put(object, "_misskey_quote", quote_url) + end + + def set_quote_url(obj), do: obj + @doc """ Serialized Mastodon-compatible `replies` collection containing _self-replies_. Based on Mastodon's ActivityPub::NoteSerializer#replies. @@ -714,6 +722,7 @@ def prepare_object(object) do |> prepare_attachments |> set_conversation |> set_reply_to_uri + |> set_quote_url |> set_replies |> strip_internal_fields |> strip_internal_tags diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld index 5d8244a11..8559e744d 100644 --- a/priv/static/schemas/litepub-0.1.jsonld +++ b/priv/static/schemas/litepub-0.1.jsonld @@ -17,6 +17,7 @@ "ostatus": "http://ostatus.org#", "schema": "http://schema.org#", "toot": "http://joinmastodon.org/ns#", + "misskey": "https://misskey-hub.net/ns#", "value": "schema:value", "sensitive": "as:sensitive", "litepub": "http://litepub.social/ns#", @@ -27,6 +28,7 @@ "@type": "@id" }, "quoteUrl": "as:quoteUrl", + "_misskey_quote": "misskey:_misskey_quote", "oauthRegistrationEndpoint": { "@id": "litepub:oauthRegistrationEndpoint", "@type": "@id" diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs index 824398e38..8c7e0a4c9 100644 --- a/test/pleroma/web/activity_pub/transmogrifier_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs @@ -382,7 +382,11 @@ test "it prepares a quote post" do {:ok, modified} = Transmogrifier.prepare_outgoing(quote_post.data) quoted_post = Object.normalize(quoted_post) + assert modified["object"]["quoteUrl"] == quoted_post.data["id"] + + # Add Misskey's quote as a fallback + assert modified["object"]["_misskey_quote"] == quoted_post.data["id"] end end From 817e308c0d9e42dfc39742c554e4c7a8da6f1c50 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 28 Jan 2022 15:55:52 -0600 Subject: [PATCH 152/199] Handle Fedibird's new quoteUri field --- .../article_note_page_validator.ex | 8 ++- .../web/activity_pub/transmogrifier.ex | 19 +++++-- priv/static/schemas/litepub-0.1.jsonld | 2 + .../quote_post/fedibird_quote_uri.json | 54 +++++++++++++++++++ .../article_note_page_validator_test.exs | 10 ++++ .../web/activity_pub/transmogrifier_test.exs | 9 ++-- 6 files changed, 92 insertions(+), 10 deletions(-) create mode 100644 test/fixtures/quote_post/fedibird_quote_uri.json 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 40bb67934..0b435b251 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 @@ -78,7 +78,13 @@ def fix_attachments(data), do: data defp fix_quote_url(%{"quoteUrl" => _quote_url} = data), do: data - # Fix for Fedibird + # Fedibird + # https://github.com/fedibird/mastodon/commit/dbd7ae6cf58a92ec67c512296b4daaea0d01e6ac + defp fix_quote_url(%{"quoteUri" => quote_url} = data) do + Map.put(data, "quoteUrl", quote_url) + end + + # Old Fedibird (bug) # https://github.com/fedibird/mastodon/issues/9 defp fix_quote_url(%{"quoteURL" => quote_url} = data) do Map.put(data, "quoteUrl", quote_url) diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 163ae54fa..01e135fc1 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -180,7 +180,15 @@ def fix_quote_url(%{"quoteUrl" => quote_url} = object, options) end end - # Fix for Fedibird + # Fedibird + # https://github.com/fedibird/mastodon/commit/dbd7ae6cf58a92ec67c512296b4daaea0d01e6ac + def fix_quote_url(%{"quoteUri" => quote_url} = object, options) do + object + |> Map.put("quoteUrl", quote_url) + |> fix_quote_url(options) + end + + # Old Fedibird (bug) # https://github.com/fedibird/mastodon/issues/9 def fix_quote_url(%{"quoteURL" => quote_url} = object, options) do object @@ -660,10 +668,13 @@ def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_r def set_reply_to_uri(obj), do: obj - # Misskey quotes - # Despite being underscored, it's potentially more reliable for interop. def set_quote_url(%{"quoteUrl" => quote_url} = object) when is_binary(quote_url) do - Map.put(object, "_misskey_quote", quote_url) + Map.merge(object, %{ + # Fedibird quote + "quoteUri" => quote_url, + # Misskey quote + "_misskey_quote" => quote_url + }) end def set_quote_url(obj), do: obj diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld index 8559e744d..3d68e0714 100644 --- a/priv/static/schemas/litepub-0.1.jsonld +++ b/priv/static/schemas/litepub-0.1.jsonld @@ -18,6 +18,7 @@ "schema": "http://schema.org#", "toot": "http://joinmastodon.org/ns#", "misskey": "https://misskey-hub.net/ns#", + "fedibird": "http://fedibird.com/ns#", "value": "schema:value", "sensitive": "as:sensitive", "litepub": "http://litepub.social/ns#", @@ -28,6 +29,7 @@ "@type": "@id" }, "quoteUrl": "as:quoteUrl", + "quoteUri": "fedibird:quoteUri", "_misskey_quote": "misskey:_misskey_quote", "oauthRegistrationEndpoint": { "@id": "litepub:oauthRegistrationEndpoint", diff --git a/test/fixtures/quote_post/fedibird_quote_uri.json b/test/fixtures/quote_post/fedibird_quote_uri.json new file mode 100644 index 000000000..7c328fdb9 --- /dev/null +++ b/test/fixtures/quote_post/fedibird_quote_uri.json @@ -0,0 +1,54 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + { + "ostatus": "http://ostatus.org#", + "atomUri": "ostatus:atomUri", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "conversation": "ostatus:conversation", + "sensitive": "as:sensitive", + "toot": "http://joinmastodon.org/ns#", + "votersCount": "toot:votersCount", + "fedibird": "http://fedibird.com/ns#", + "quoteUri": "fedibird:quoteUri", + "expiry": "fedibird:expiry" + } + ], + "id": "https://fedibird.com/users/noellabo/statuses/107699335988346142", + "type": "Note", + "summary": null, + "inReplyTo": null, + "published": "2022-01-28T09:17:30Z", + "url": "https://fedibird.com/@noellabo/107699335988346142", + "attributedTo": "https://fedibird.com/users/noellabo", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "https://fedibird.com/users/noellabo/followers" + ], + "sensitive": false, + "atomUri": "https://fedibird.com/users/noellabo/statuses/107699335988346142", + "inReplyToAtomUri": null, + "conversation": "tag:fedibird.com,2022-01-28:objectId=107699335988345290:objectType=Conversation", + "context": "https://fedibird.com/contexts/107699335988345290", + "quoteUri": "https://fedibird.com/users/yamako/statuses/107699333438289729", + "_misskey_quote": "https://fedibird.com/users/yamako/statuses/107699333438289729", + "_misskey_content": "美味しそう", + "content": "

美味しそう
QT: https://fedibird.com/@yamako/107699333438289729

", + "contentMap": { + "ja": "

美味しそう
QT: https://fedibird.com/@yamako/107699333438289729

" + }, + "attachment": [], + "tag": [], + "replies": { + "id": "https://fedibird.com/users/noellabo/statuses/107699335988346142/replies", + "type": "Collection", + "first": { + "type": "CollectionPage", + "next": "https://fedibird.com/users/noellabo/statuses/107699335988346142/replies?only_other_accounts=true&page=true", + "partOf": "https://fedibird.com/users/noellabo/statuses/107699335988346142/replies", + "items": [] + } + } +} diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs index dec2e28c9..a4ba38e6a 100644 --- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs @@ -127,6 +127,16 @@ test "Fedibird quote post" do assert cng.changes.quoteUrl == "https://misskey.io/notes/8vsn2izjwh" end + test "Fedibird quote post with quoteUri field" do + insert(:user, ap_id: "https://fedibird.com/users/noellabo") + + data = File.read!("test/fixtures/quote_post/fedibird_quote_uri.json") |> Jason.decode!() + cng = ArticleNotePageValidator.cast_and_validate(data) + + assert cng.valid? + assert cng.changes.quoteUrl == "https://fedibird.com/users/yamako/statuses/107699333438289729" + end + test "Misskey quote post" do insert(:user, ap_id: "https://misskey.io/users/7rkrarq81i") diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs index 8c7e0a4c9..1838f96bb 100644 --- a/test/pleroma/web/activity_pub/transmogrifier_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs @@ -381,12 +381,11 @@ test "it prepares a quote post" do {:ok, modified} = Transmogrifier.prepare_outgoing(quote_post.data) - quoted_post = Object.normalize(quoted_post) + %{data: %{"id" => quote_id}} = Object.normalize(quoted_post) - assert modified["object"]["quoteUrl"] == quoted_post.data["id"] - - # Add Misskey's quote as a fallback - assert modified["object"]["_misskey_quote"] == quoted_post.data["id"] + assert modified["object"]["quoteUrl"] == quote_id + assert modified["object"]["quoteUri"] == quote_id + assert modified["object"]["_misskey_quote"] == quote_id end end From 4075eecca0033e4487fa9d5d5bb75384597c3e79 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 28 Jan 2022 16:07:17 -0600 Subject: [PATCH 153/199] InlineQuotePolicy: improve the way Markdown quotes are displayed by other software --- .../web/activity_pub/mrf/inline_quote_policy.ex | 13 +++++++------ .../activity_pub/mrf/inline_quote_policy_test.exs | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) 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 46013fc5e..7de4935f2 100644 --- a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex @@ -6,8 +6,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do @moduledoc "Force a quote line into the message content." @behaviour Pleroma.Web.ActivityPub.MRF.Policy - defp build_inline_quote(prefix, url, br) do - "#{String.duplicate("
", br)}#{prefix}: #{url}
" + defp build_inline_quote(prefix, url) do + "

#{prefix}: #{url}
" end defp filter_object(%{"quoteUrl" => quote_url} = object) do @@ -18,12 +18,13 @@ defp filter_object(%{"quoteUrl" => quote_url} = object) do else prefix = Pleroma.Config.get([:mrf_inline_quote, :prefix]) - inline_quote = + content = if String.ends_with?(content, "

"), - do: build_inline_quote(prefix, quote_url, 0), - else: build_inline_quote(prefix, quote_url, 2) + do: + String.trim_trailing(content, "

") <> + build_inline_quote(prefix, quote_url) <> "

", + else: content <> build_inline_quote(prefix, quote_url) - content = content <> inline_quote Map.put(object, "content", content) end end diff --git a/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs index 8e75aaaab..2291c1dac 100644 --- a/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs @@ -22,7 +22,7 @@ test "adds quote URL to post content" do {:ok, %{"object" => %{"content" => filtered}}} = InlineQuotePolicy.filter(activity) assert filtered == - "Nice post

RT: https://gleasonator.com/objects/1234
" + "Nice post

RT: https://gleasonator.com/objects/1234
" end test "doesn't add line breaks to markdown posts" do @@ -41,7 +41,7 @@ test "doesn't add line breaks to markdown posts" do {:ok, %{"object" => %{"content" => filtered}}} = InlineQuotePolicy.filter(activity) assert filtered == - "

Nice post

RT: https://gleasonator.com/objects/1234" + "

Nice post

RT: https://gleasonator.com/objects/1234

" end test "ignores Misskey quote posts" do From 79fca39faf6d084eabb6be44a2263431943b8dd4 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 28 Jan 2022 17:53:19 -0600 Subject: [PATCH 154/199] Actually, don't send _misskey_quote anymore --- lib/pleroma/web/activity_pub/transmogrifier.ex | 11 +++++------ priv/static/schemas/litepub-0.1.jsonld | 2 -- test/pleroma/web/activity_pub/transmogrifier_test.exs | 1 - 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 01e135fc1..6c6cd712b 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -668,13 +668,12 @@ def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_r def set_reply_to_uri(obj), do: obj + @doc """ + Fedibird compatibility + https://github.com/fedibird/mastodon/commit/dbd7ae6cf58a92ec67c512296b4daaea0d01e6ac + """ def set_quote_url(%{"quoteUrl" => quote_url} = object) when is_binary(quote_url) do - Map.merge(object, %{ - # Fedibird quote - "quoteUri" => quote_url, - # Misskey quote - "_misskey_quote" => quote_url - }) + Map.put(object, "quoteUri", quote_url) end def set_quote_url(obj), do: obj diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld index 3d68e0714..b499a96f5 100644 --- a/priv/static/schemas/litepub-0.1.jsonld +++ b/priv/static/schemas/litepub-0.1.jsonld @@ -17,7 +17,6 @@ "ostatus": "http://ostatus.org#", "schema": "http://schema.org#", "toot": "http://joinmastodon.org/ns#", - "misskey": "https://misskey-hub.net/ns#", "fedibird": "http://fedibird.com/ns#", "value": "schema:value", "sensitive": "as:sensitive", @@ -30,7 +29,6 @@ }, "quoteUrl": "as:quoteUrl", "quoteUri": "fedibird:quoteUri", - "_misskey_quote": "misskey:_misskey_quote", "oauthRegistrationEndpoint": { "@id": "litepub:oauthRegistrationEndpoint", "@type": "@id" diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs index 1838f96bb..4a192cdc0 100644 --- a/test/pleroma/web/activity_pub/transmogrifier_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs @@ -385,7 +385,6 @@ test "it prepares a quote post" do assert modified["object"]["quoteUrl"] == quote_id assert modified["object"]["quoteUri"] == quote_id - assert modified["object"]["_misskey_quote"] == quote_id end end From f9697e68c2b75a77575b9b7c89d08a5687bfd7b4 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 30 Jan 2022 10:57:29 -0600 Subject: [PATCH 155/199] InlineQuotePolicy: skip objects which already have an .inline-quote span --- .../activity_pub/mrf/inline_quote_policy.ex | 13 ++++- .../quote_post/fedibird_quote_mismatched.json | 54 +++++++++++++++++++ .../mrf/inline_quote_policy_test.exs | 17 ++++++ 3 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/quote_post/fedibird_quote_mismatched.json 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 7de4935f2..c78675caf 100644 --- a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex @@ -10,10 +10,21 @@ defp build_inline_quote(prefix, url) do "

#{prefix}: #{url}
" end + defp has_inline_quote?(content, quote_url) do + cond do + # Does the quote URL exist in the content? + content =~ quote_url -> true + # Does the content already have a .quote-inline span? + content =~ "" -> true + # No inline quote found + true -> false + end + end + defp filter_object(%{"quoteUrl" => quote_url} = object) do content = object["content"] || "" - if content =~ quote_url do + if has_inline_quote?(content, quote_url) do object else prefix = Pleroma.Config.get([:mrf_inline_quote, :prefix]) diff --git a/test/fixtures/quote_post/fedibird_quote_mismatched.json b/test/fixtures/quote_post/fedibird_quote_mismatched.json new file mode 100644 index 000000000..8dee5daff --- /dev/null +++ b/test/fixtures/quote_post/fedibird_quote_mismatched.json @@ -0,0 +1,54 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + { + "ostatus": "http://ostatus.org#", + "atomUri": "ostatus:atomUri", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "conversation": "ostatus:conversation", + "sensitive": "as:sensitive", + "toot": "http://joinmastodon.org/ns#", + "votersCount": "toot:votersCount", + "fedibird": "http://fedibird.com/ns#", + "quoteUri": "fedibird:quoteUri", + "expiry": "fedibird:expiry" + } + ], + "id": "https://fedibird.com/users/noellabo/statuses/107712183700212249", + "type": "Note", + "summary": null, + "inReplyTo": null, + "published": "2022-01-30T15:44:50Z", + "url": "https://fedibird.com/@noellabo/107712183700212249", + "attributedTo": "https://fedibird.com/users/noellabo", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "https://fedibird.com/users/noellabo/followers" + ], + "sensitive": false, + "atomUri": "https://fedibird.com/users/noellabo/statuses/107712183700212249", + "inReplyToAtomUri": null, + "conversation": "tag:fedibird.com,2022-01-30:objectId=107712183700170473:objectType=Conversation", + "context": "https://fedibird.com/contexts/107712183700170473", + "quoteUri": "https://unnerv.jp/users/UN_NERV/statuses/107712176849067434", + "_misskey_quote": "https://unnerv.jp/users/UN_NERV/statuses/107712176849067434", + "_misskey_content": "揺れていたようだ", + "content": "

揺れていたようだ
QT: https://unnerv.jp/@UN_NERV/107712176849067434

", + "contentMap": { + "ja": "

揺れていたようだ
QT: https://unnerv.jp/@UN_NERV/107712176849067434

" + }, + "attachment": [], + "tag": [], + "replies": { + "id": "https://fedibird.com/users/noellabo/statuses/107712183700212249/replies", + "type": "Collection", + "first": { + "type": "CollectionPage", + "next": "https://fedibird.com/users/noellabo/statuses/107712183700212249/replies?only_other_accounts=true&page=true", + "partOf": "https://fedibird.com/users/noellabo/statuses/107712183700212249/replies", + "items": [] + } + } +} diff --git a/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs index 2291c1dac..44ee91d4b 100644 --- a/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs @@ -72,4 +72,21 @@ test "ignores Fedibird quote posts" do {:ok, filtered} = InlineQuotePolicy.filter(activity) assert filtered == activity end + + test "skips objects which already have an .inline-quote span" do + object = + File.read!("test/fixtures/quote_post/fedibird_quote_mismatched.json") |> Jason.decode!() + + # Normally the ObjectValidator will fix this before it reaches MRF + object = Map.put(object, "quoteUrl", object["quoteUri"]) + + activity = %{ + "type" => "Create", + "actor" => "https://fedibird.com/users/noellabo", + "object" => object + } + + {:ok, filtered} = InlineQuotePolicy.filter(activity) + assert filtered == activity + end end From b0a7e795e799d3c8d750ab909657ec7b3d0bfd58 Mon Sep 17 00:00:00 2001 From: tusooa Date: Mon, 10 Jul 2023 17:57:09 -0400 Subject: [PATCH 156/199] Unify logic for normalizing quoteUri --- .../article_note_page_validator.ex | 23 +--------- .../object_validators/common_fixes.ex | 21 ++++++++++ .../web/activity_pub/transmogrifier.ex | 42 ++++++------------- priv/scrubbers/default.ex | 7 +++- 4 files changed, 40 insertions(+), 53 deletions(-) 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 0b435b251..1b5b2e8fb 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 @@ -76,27 +76,6 @@ def fix_attachments(%{"attachment" => attachment} = data) when is_map(attachment def fix_attachments(data), do: data - defp fix_quote_url(%{"quoteUrl" => _quote_url} = data), do: data - - # Fedibird - # https://github.com/fedibird/mastodon/commit/dbd7ae6cf58a92ec67c512296b4daaea0d01e6ac - defp fix_quote_url(%{"quoteUri" => quote_url} = data) do - Map.put(data, "quoteUrl", quote_url) - end - - # Old Fedibird (bug) - # https://github.com/fedibird/mastodon/issues/9 - defp fix_quote_url(%{"quoteURL" => quote_url} = data) do - Map.put(data, "quoteUrl", quote_url) - end - - # Misskey fallback - defp fix_quote_url(%{"_misskey_quote" => quote_url} = data) do - Map.put(data, "quoteUrl", quote_url) - end - - defp fix_quote_url(data), do: data - defp fix(data) do data |> CommonFixes.fix_actor() @@ -105,7 +84,7 @@ defp fix(data) do |> fix_tag() |> fix_replies() |> fix_attachments() - |> fix_quote_url() + |> CommonFixes.fix_quote_url() |> Transmogrifier.fix_emoji() |> Transmogrifier.fix_content_map() end 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 add46d561..cc2ad9116 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex @@ -76,4 +76,25 @@ def fix_object_action_recipients(data, %Object{data: %{"actor" => actor}}) do Map.put(data, "to", to) end + + def fix_quote_url(%{"quoteUrl" => _quote_url} = data), do: data + + # Fedibird + # https://github.com/fedibird/mastodon/commit/dbd7ae6cf58a92ec67c512296b4daaea0d01e6ac + def fix_quote_url(%{"quoteUri" => quote_url} = data) do + Map.put(data, "quoteUrl", quote_url) + 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) + end + + # Misskey fallback + def fix_quote_url(%{"_misskey_quote" => quote_url} = data) do + Map.put(data, "quoteUrl", quote_url) + end + + def fix_quote_url(data), do: data end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 6c6cd712b..86d3ac60f 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -166,45 +166,27 @@ def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options) def fix_in_reply_to(object, _options), do: object - def fix_quote_url(object, options \\ []) + def fix_quote_url_and_maybe_fetch(object, options \\ []) do + quote_url = + case Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes.fix_quote_url(object) do + %{"quoteUrl" => quote_url} -> quote_url + _ -> nil + end - def fix_quote_url(%{"quoteUrl" => quote_url} = object, options) - when not is_nil(quote_url) do - with {:ok, quoted_object} <- get_obj_helper(quote_url, options), + with {:quoting?, true} <- {:quoting?, not is_nil(quote_url)}, + {:ok, quoted_object} <- get_obj_helper(quote_url, options), %Activity{} <- Activity.get_create_by_object_ap_id(quoted_object.data["id"]) do Map.put(object, "quoteUrl", quoted_object.data["id"]) else + {:quoting?, _} -> + object + e -> Logger.warn("Couldn't fetch #{inspect(quote_url)}, error: #{inspect(e)}") object end end - # Fedibird - # https://github.com/fedibird/mastodon/commit/dbd7ae6cf58a92ec67c512296b4daaea0d01e6ac - def fix_quote_url(%{"quoteUri" => quote_url} = object, options) do - object - |> Map.put("quoteUrl", quote_url) - |> fix_quote_url(options) - end - - # Old Fedibird (bug) - # https://github.com/fedibird/mastodon/issues/9 - def fix_quote_url(%{"quoteURL" => quote_url} = object, options) do - object - |> Map.put("quoteUrl", quote_url) - |> fix_quote_url(options) - end - - # Misskey fallback - def fix_quote_url(%{"_misskey_quote" => quote_url} = object, options) do - object - |> Map.put("quoteUrl", quote_url) - |> fix_quote_url(options) - end - - def fix_quote_url(object, _options), do: object - defp prepare_in_reply_to(in_reply_to) do cond do is_bitstring(in_reply_to) -> @@ -493,7 +475,7 @@ def handle_incoming( |> strip_internal_fields() |> fix_type(fetch_options) |> fix_in_reply_to(fetch_options) - |> fix_quote_url(fetch_options) + |> fix_quote_url_and_maybe_fetch(fetch_options) data = Map.put(data, "object", object) options = Keyword.put(options, :local, false) diff --git a/priv/scrubbers/default.ex b/priv/scrubbers/default.ex index 56324a9fa..4e7950547 100644 --- a/priv/scrubbers/default.ex +++ b/priv/scrubbers/default.ex @@ -60,7 +60,12 @@ defmodule Pleroma.HTML.Scrubber.Default do Meta.allow_tag_with_these_attributes(:u, ["lang"]) Meta.allow_tag_with_these_attributes(:ul, ["lang"]) - Meta.allow_tag_with_this_attribute_values(:span, "class", ["h-card", "recipients-inline", "quote-inline"]) + Meta.allow_tag_with_this_attribute_values(:span, "class", [ + "h-card", + "recipients-inline", + "quote-inline" + ]) + Meta.allow_tag_with_these_attributes(:span, ["lang"]) Meta.allow_tag_with_this_attribute_values(:code, "class", ["inline"]) From 9bcec87aba5ce4de6b61b5a95d6832da9dfa0fd8 Mon Sep 17 00:00:00 2001 From: tusooa Date: Mon, 10 Jul 2023 18:27:23 -0400 Subject: [PATCH 157/199] Allow local quote and private self-quote --- lib/pleroma/web/common_api/activity_draft.ex | 14 +++++++++++++- .../pleroma/web/common_api/activity_draft_test.exs | 9 ++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index d4875765c..32921aa5a 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -167,9 +167,21 @@ defp visibility(%{params: params} = draft) do end end + defp can_quote?(_draft, _object, visibility) when visibility in ~w(public unlisted local) do + true + end + + defp can_quote?(draft, object, "private") do + draft.user.ap_id == object.data["actor"] + end + + defp can_quote?(_, _, _) do + false + end + defp quoting_visibility(%{quote_post: %Activity{}} = draft) do with %Object{} = object <- Object.normalize(draft.quote_post, fetch: false), - visibility when visibility in ~w(public unlisted) <- Visibility.get_visibility(object) do + true <- can_quote?(draft, object, Visibility.get_visibility(object)) do draft else _ -> add_error(draft, dgettext("errors", "Cannot quote private message")) diff --git a/test/pleroma/web/common_api/activity_draft_test.exs b/test/pleroma/web/common_api/activity_draft_test.exs index 8a09fc710..02bc6cf3b 100644 --- a/test/pleroma/web/common_api/activity_draft_test.exs +++ b/test/pleroma/web/common_api/activity_draft_test.exs @@ -12,15 +12,22 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraftTest do test "create/2 with a quote post" do user = insert(:user) + another_user = insert(:user) {:ok, direct} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) {:ok, private} = CommonAPI.post(user, %{status: ".", visibility: "private"}) {:ok, unlisted} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"}) + {:ok, local} = CommonAPI.post(user, %{status: ".", visibility: "local"}) {:ok, public} = CommonAPI.post(user, %{status: ".", visibility: "public"}) {:error, _} = ActivityDraft.create(user, %{status: "nice", quote_id: direct.id}) - {:error, _} = ActivityDraft.create(user, %{status: "nice", quote_id: private.id}) + {:ok, _} = ActivityDraft.create(user, %{status: "nice", quote_id: private.id}) + {:error, _} = ActivityDraft.create(another_user, %{status: "nice", quote_id: private.id}) {:ok, _} = ActivityDraft.create(user, %{status: "nice", quote_id: unlisted.id}) + {:ok, _} = ActivityDraft.create(another_user, %{status: "nice", quote_id: unlisted.id}) + {:ok, _} = ActivityDraft.create(user, %{status: "nice", quote_id: local.id}) + {:ok, _} = ActivityDraft.create(another_user, %{status: "nice", quote_id: local.id}) {:ok, _} = ActivityDraft.create(user, %{status: "nice", quote_id: public.id}) + {:ok, _} = ActivityDraft.create(another_user, %{status: "nice", quote_id: public.id}) end end From d244c9d2984d21887f50737597fc03d2d0dd1601 Mon Sep 17 00:00:00 2001 From: tusooa Date: Mon, 10 Jul 2023 18:28:13 -0400 Subject: [PATCH 158/199] Add changelog --- changelog.d/quote.add | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/quote.add diff --git a/changelog.d/quote.add b/changelog.d/quote.add new file mode 100644 index 000000000..1c368ae75 --- /dev/null +++ b/changelog.d/quote.add @@ -0,0 +1 @@ +Implement quotes From 762794eed9e6fc8a03d1416e2a6e080b00df9e10 Mon Sep 17 00:00:00 2001 From: tusooa Date: Mon, 10 Jul 2023 19:43:18 -0400 Subject: [PATCH 159/199] Fix CommonAPITest --- test/pleroma/web/common_api_test.exs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index c4eba8b9c..70a5b6fed 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -825,16 +825,23 @@ test "quote posting with explicit addressing doesn't mention the OP" do test "quote posting visibility" do user = insert(:user) + another_user = insert(:user) {:ok, direct} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) {:ok, private} = CommonAPI.post(user, %{status: ".", visibility: "private"}) {:ok, unlisted} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"}) + {:ok, local} = CommonAPI.post(user, %{status: ".", visibility: "local"}) {:ok, public} = CommonAPI.post(user, %{status: ".", visibility: "public"}) {:error, _} = CommonAPI.post(user, %{status: "nice", quote_id: direct.id}) - {:error, _} = CommonAPI.post(user, %{status: "nice", quote_id: private.id}) + {:ok, _} = CommonAPI.post(user, %{status: "nice", quote_id: private.id}) + {:error, _} = CommonAPI.post(another_user, %{status: "nice", quote_id: private.id}) {:ok, _} = CommonAPI.post(user, %{status: "nice", quote_id: unlisted.id}) + {:ok, _} = CommonAPI.post(another_user, %{status: "nice", quote_id: unlisted.id}) + {:ok, _} = CommonAPI.post(user, %{status: "nice", quote_id: local.id}) + {:ok, _} = CommonAPI.post(another_user, %{status: "nice", quote_id: local.id}) {:ok, _} = CommonAPI.post(user, %{status: "nice", quote_id: public.id}) + {:ok, _} = CommonAPI.post(another_user, %{status: "nice", quote_id: public.id}) end end From 163e5637335f9454688d3cc83530f82fc640a5b9 Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 12 Jul 2023 09:30:43 -0400 Subject: [PATCH 160/199] Allow more flexibility in InlineQuotePolicy --- config/config.exs | 2 +- config/description.exs | 18 ++++++++++++++ docs/configuration/cheatsheet.md | 2 +- .../activity_pub/mrf/inline_quote_policy.ex | 12 ++++++---- priv/scrubbers/default.ex | 1 + .../mrf/inline_quote_policy_test.exs | 24 +++++++++++++++++-- 6 files changed, 50 insertions(+), 9 deletions(-) diff --git a/config/config.exs b/config/config.exs index 9149e925a..e8ae31542 100644 --- a/config/config.exs +++ b/config/config.exs @@ -434,7 +434,7 @@ config :pleroma, :mrf_follow_bot, follower_nickname: nil -config :pleroma, :mrf_inline_quote, prefix: "RT" +config :pleroma, :mrf_inline_quote, template: "RT: {url}" config :pleroma, :rich_media, enabled: true, diff --git a/config/description.exs b/config/description.exs index d18649ae8..079d187d5 100644 --- a/config/description.exs +++ b/config/description.exs @@ -2994,6 +2994,24 @@ } ] }, + %{ + group: :pleroma, + key: :mrf_inline_quote, + tab: :mrf, + related_policy: "Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy", + label: "MRF Inline Quote Policy", + type: :group, + description: "Force quote url to appear in post content.", + children: [ + %{ + key: :template, + type: :string, + description: + "The template to append to the post. `{url}` will be replaced with the actual link to the quoted post.", + suggestions: ["RT: {url}"] + } + ] + }, %{ group: :pleroma, key: :modules, diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 32cc5811a..a17f8735a 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -269,7 +269,7 @@ Notes: * `federated_timeline_removal_shortcode`: A list of patterns which result in message with emojis whose shortcodes match being removed from federated timelines (a.k.a unlisted). This will apply only to statuses. Each pattern can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html). #### :mrf_inline_quote -* `prefix`: Prefix before the link (default: `RT`) +* `template`: The template to append to the post. `{url}` will be replaced with the actual link to the quoted post. Default: `RT: {url}` ### :activitypub * `unfollow_blocked`: Whether blocks result in people getting unfollowed 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 c78675caf..a0eefefc0 100644 --- a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex @@ -6,8 +6,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do @moduledoc "Force a quote line into the message content." @behaviour Pleroma.Web.ActivityPub.MRF.Policy - defp build_inline_quote(prefix, url) do - "

#{prefix}: #{url}
" + defp build_inline_quote(template, url) do + quote_line = String.replace(template, "{url}", "#{url}") + + "

#{quote_line}
" end defp has_inline_quote?(content, quote_url) do @@ -27,14 +29,14 @@ defp filter_object(%{"quoteUrl" => quote_url} = object) do if has_inline_quote?(content, quote_url) do object else - prefix = Pleroma.Config.get([:mrf_inline_quote, :prefix]) + template = Pleroma.Config.get([:mrf_inline_quote, :template]) content = if String.ends_with?(content, "

"), do: String.trim_trailing(content, "

") <> - build_inline_quote(prefix, quote_url) <> "

", - else: content <> build_inline_quote(prefix, quote_url) + build_inline_quote(template, quote_url) <> "

", + else: content <> build_inline_quote(template, quote_url) Map.put(object, "content", content) end diff --git a/priv/scrubbers/default.ex b/priv/scrubbers/default.ex index 4e7950547..24a76263b 100644 --- a/priv/scrubbers/default.ex +++ b/priv/scrubbers/default.ex @@ -38,6 +38,7 @@ defmodule Pleroma.HTML.Scrubber.Default do Meta.allow_tag_with_these_attributes(:abbr, ["title", "lang"]) Meta.allow_tag_with_these_attributes(:b, ["lang"]) + Meta.allow_tag_with_these_attributes(:bdi, []) Meta.allow_tag_with_these_attributes(:blockquote, ["lang"]) Meta.allow_tag_with_these_attributes(:br, ["lang"]) Meta.allow_tag_with_these_attributes(:code, ["lang"]) diff --git a/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs index 44ee91d4b..d5762766f 100644 --- a/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs @@ -22,7 +22,27 @@ test "adds quote URL to post content" do {:ok, %{"object" => %{"content" => filtered}}} = InlineQuotePolicy.filter(activity) assert filtered == - "Nice post

RT: https://gleasonator.com/objects/1234
" + "Nice post

RT: https://gleasonator.com/objects/1234
" + end + + test "adds quote URL to post content, custom template" do + clear_config([:mrf_inline_quote, :template], "{url}'s quoting") + quote_url = "https://gleasonator.com/objects/1234" + + activity = %{ + "type" => "Create", + "actor" => "https://gleasonator.com/users/alex", + "object" => %{ + "type" => "Note", + "content" => "Nice post", + "quoteUrl" => quote_url + } + } + + {:ok, %{"object" => %{"content" => filtered}}} = InlineQuotePolicy.filter(activity) + + assert filtered == + "Nice post

https://gleasonator.com/objects/1234's quoting
" end test "doesn't add line breaks to markdown posts" do @@ -41,7 +61,7 @@ test "doesn't add line breaks to markdown posts" do {:ok, %{"object" => %{"content" => filtered}}} = InlineQuotePolicy.filter(activity) assert filtered == - "

Nice post

RT: https://gleasonator.com/objects/1234

" + "

Nice post

RT: https://gleasonator.com/objects/1234

" end test "ignores Misskey quote posts" do From e9cd004ba1b904d92b1c07446bbf03dc070cce6a Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 12 Jul 2023 11:09:10 -0400 Subject: [PATCH 161/199] Parse object link as quoteUrl --- lib/pleroma/constants.ex | 7 +++++ .../audio_image_video_validator.ex | 1 + .../object_validators/common_fixes.ex | 27 +++++++++++++++++++ .../object_validators/question_validator.ex | 1 + .../quote_post/fep-e232-tag-example.json | 17 ++++++++++++ .../article_note_page_validator_test.exs | 12 +++++++++ 6 files changed, 65 insertions(+) create mode 100644 test/fixtures/quote_post/fep-e232-tag-example.json diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex index 6befc6897..9d764ec25 100644 --- a/lib/pleroma/constants.ex +++ b/lib/pleroma/constants.ex @@ -83,4 +83,11 @@ defmodule Pleroma.Constants do ) const(upload_object_types, do: ["Document", "Image"]) + + const(activity_json_mime_types, + do: [ + "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", + "application/activity+json" + ] + ) end diff --git a/lib/pleroma/web/activity_pub/object_validators/audio_image_video_validator.ex b/lib/pleroma/web/activity_pub/object_validators/audio_image_video_validator.ex index 79ff76104..65ac6bb93 100644 --- a/lib/pleroma/web/activity_pub/object_validators/audio_image_video_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/audio_image_video_validator.ex @@ -99,6 +99,7 @@ defp fix(data) do data |> CommonFixes.fix_actor() |> CommonFixes.fix_object_defaults() + |> CommonFixes.fix_quote_url() |> Transmogrifier.fix_emoji() |> fix_url() |> fix_content() 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 cc2ad9116..65b8d9a2c 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex @@ -10,6 +10,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Utils + require Pleroma.Constants + def cast_and_filter_recipients(message, field, follower_collection, field_fallback \\ []) do {:ok, data} = ObjectValidators.Recipients.cast(message[field] || field_fallback) @@ -96,5 +98,30 @@ def fix_quote_url(%{"_misskey_quote" => quote_url} = data) do Map.put(data, "quoteUrl", quote_url) end + def fix_quote_url(%{"tag" => [_ | _] = tags} = data) do + tag = Enum.find(tags, &is_object_link_tag/1) + + if not is_nil(tag) do + data + |> Map.put("quoteUrl", tag["href"]) + else + data + end + end + def fix_quote_url(data), do: data + + # https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md + defp is_object_link_tag( + %{ + "type" => "Link", + "mediaType" => media_type, + "href" => href + } = tag + ) + when media_type in Pleroma.Constants.activity_json_mime_types() and is_binary(href) do + true + end + + defp is_object_link_tag(_), do: false end diff --git a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex index ce3305142..621085e6c 100644 --- a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex @@ -62,6 +62,7 @@ defp fix(data) do data |> CommonFixes.fix_actor() |> CommonFixes.fix_object_defaults() + |> CommonFixes.fix_quote_url() |> Transmogrifier.fix_emoji() |> fix_closed() end diff --git a/test/fixtures/quote_post/fep-e232-tag-example.json b/test/fixtures/quote_post/fep-e232-tag-example.json new file mode 100644 index 000000000..23c7fb5ac --- /dev/null +++ b/test/fixtures/quote_post/fep-e232-tag-example.json @@ -0,0 +1,17 @@ +{ + "@context": "https://www.w3.org/ns/activitystreams", + "type": "Note", + "content": "This is a quote:
RE: https://server.example/objects/123", + "tag": [ + { + "type": "Link", + "mediaType": "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", + "href": "https://server.example/objects/123", + "name": "RE: https://server.example/objects/123" + } + ], + "id": "https://server.example/objects/1", + "to": "https://server.example/users/1", + "attributedTo": "https://server.example/users/1", + "actor": "https://server.example/users/1" +} diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs index a4ba38e6a..73141cac1 100644 --- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs @@ -146,4 +146,16 @@ test "Misskey quote post" do assert cng.valid? assert cng.changes.quoteUrl == "https://misskey.io/notes/8vs6wxufd0" end + + test "Parse tag as quote" do + # https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md + + insert(:user, ap_id: "https://server.example/users/1") + + data = File.read!("test/fixtures/quote_post/fep-e232-tag-example.json") |> Jason.decode!() + cng = ArticleNotePageValidator.cast_and_validate(data) + + assert cng.valid? + assert cng.changes.quoteUrl == "https://server.example/objects/123" + end end From 479a6f11dbbba0c945c08883956ffab198f91688 Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 12 Jul 2023 14:08:24 -0400 Subject: [PATCH 162/199] Keep incoming Link tag --- .../object_validators/tag_validator.ex | 14 +++++++++++++- .../article_note_page_validator_test.exs | 7 +++++++ 2 files changed, 20 insertions(+), 1 deletion(-) 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 cfd510c19..47cf7b415 100644 --- a/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex @@ -9,15 +9,20 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.TagValidator do import Ecto.Changeset + require Pleroma.Constants + @primary_key false embedded_schema do # Common field(:type, :string) field(:name, :string) - # Mention, Hashtag + # Mention, Hashtag, Link field(:href, ObjectValidators.Uri) + # Link + field(:mediaType, :string) + # Emoji embeds_one :icon, IconObjectValidator, primary_key: false do field(:type, :string) @@ -68,6 +73,13 @@ def changeset(struct, %{"type" => "Emoji"} = data) do |> validate_required([:type, :name, :icon]) end + def changeset(struct, %{"type" => "Link"} = data) do + struct + |> cast(data, [:type, :name, :mediaType, :href]) + |> validate_inclusion(:mediaType, Pleroma.Constants.activity_json_mime_types()) + |> validate_required([:type, :href, :mediaType]) + end + def changeset(struct, %{"type" => _} = data) do struct |> cast(data, []) diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs index 73141cac1..4703c3801 100644 --- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs @@ -157,5 +157,12 @@ test "Parse tag as quote" do assert cng.valid? assert cng.changes.quoteUrl == "https://server.example/objects/123" + + assert Enum.at(cng.changes.tag, 0).changes == %{ + type: "Link", + mediaType: "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", + href: "https://server.example/objects/123", + name: "RE: https://server.example/objects/123" + } end end From e349e92a441840bbbdbf13cacd307e65f85a38ff Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 12 Jul 2023 14:27:29 -0400 Subject: [PATCH 163/199] Add mrf to force link tag of quoting posts --- docs/configuration/cheatsheet.md | 1 + lib/pleroma/constants.ex | 4 + .../mrf/quote_to_link_tag_policy.ex | 49 +++++++++++++ .../object_validators/common_fixes.ex | 16 ++-- .../mrf/quote_to_link_tag_policy_test.exs | 73 +++++++++++++++++++ 5 files changed, 134 insertions(+), 9 deletions(-) create mode 100644 lib/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy.ex create mode 100644 test/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy_test.exs diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index a17f8735a..a4cae4dbb 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -161,6 +161,7 @@ To add configuration to your config file, you can copy it from the base config. * `Pleroma.Web.ActivityPub.MRF.KeywordPolicy`: Rejects or removes from the federated timeline or replaces keywords. (See [`:mrf_keyword`](#mrf_keyword)). * `Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent`: Forces every mentioned user to be reflected in the post content. * `Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy`: Forces quote post URLs to be reflected in the message content inline. + * `Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicy`: Force a Link tag for posts quoting another post. (may break outgoing federation of quote posts with older Pleroma versions) * `transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo). * `transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value. diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex index 9d764ec25..ed60bcc37 100644 --- a/lib/pleroma/constants.ex +++ b/lib/pleroma/constants.ex @@ -84,6 +84,10 @@ defmodule Pleroma.Constants do const(upload_object_types, do: ["Document", "Image"]) + const(activity_json_canonical_mime_type, + do: "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" + ) + const(activity_json_mime_types, do: [ "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", 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 new file mode 100644 index 000000000..f1c573d1b --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy.ex @@ -0,0 +1,49 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +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 + + alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes + + require Pleroma.Constants + + @impl Pleroma.Web.ActivityPub.MRF.Policy + def filter(%{"object" => %{"quoteUrl" => _} = object} = activity) do + {:ok, Map.put(activity, "object", filter_object(object))} + end + + @impl Pleroma.Web.ActivityPub.MRF.Policy + def filter(object), do: {:ok, object} + + @impl Pleroma.Web.ActivityPub.MRF.Policy + def describe, do: {:ok, %{}} + + @impl Pleroma.Web.ActivityPub.MRF.Policy + def history_awareness, do: :auto + + defp filter_object(%{"quoteUrl" => quote_url} = object) do + tags = object["tag"] || [] + + if Enum.any?(tags, fn tag -> + CommonFixes.is_object_link_tag(tag) and tag["href"] == quote_url + end) do + object + else + object + |> Map.put( + "tag", + tags ++ + [ + %{ + "type" => "Link", + "mediaType" => Pleroma.Constants.activity_json_canonical_mime_type(), + "href" => quote_url + } + ] + ) + end + end +end 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 65b8d9a2c..4d9be0bdd 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex @@ -112,16 +112,14 @@ def fix_quote_url(%{"tag" => [_ | _] = tags} = data) do def fix_quote_url(data), do: data # https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md - defp is_object_link_tag( - %{ - "type" => "Link", - "mediaType" => media_type, - "href" => href - } = tag - ) - when media_type in Pleroma.Constants.activity_json_mime_types() and is_binary(href) do + def is_object_link_tag(%{ + "type" => "Link", + "mediaType" => media_type, + "href" => href + }) + when media_type in Pleroma.Constants.activity_json_mime_types() and is_binary(href) do true end - defp is_object_link_tag(_), do: false + def is_object_link_tag(_), do: false end diff --git a/test/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy_test.exs b/test/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy_test.exs new file mode 100644 index 000000000..96b49b6a0 --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy_test.exs @@ -0,0 +1,73 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicyTest do + alias Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicy + + use Pleroma.DataCase + + require Pleroma.Constants + + test "Add quote url to Link tag" do + quote_url = "https://gleasonator.com/objects/1234" + + activity = %{ + "type" => "Create", + "actor" => "https://gleasonator.com/users/alex", + "object" => %{ + "type" => "Note", + "content" => "Nice post", + "quoteUrl" => quote_url + } + } + + {:ok, %{"object" => object}} = QuoteToLinkTagPolicy.filter(activity) + + assert object["tag"] == [ + %{ + "type" => "Link", + "href" => quote_url, + "mediaType" => Pleroma.Constants.activity_json_canonical_mime_type() + } + ] + end + + test "Add quote url to Link tag, append to the end" do + quote_url = "https://gleasonator.com/objects/1234" + + activity = %{ + "type" => "Create", + "actor" => "https://gleasonator.com/users/alex", + "object" => %{ + "type" => "Note", + "content" => "Nice post", + "quoteUrl" => quote_url, + "tag" => [%{"type" => "Hashtag", "name" => "#foo"}] + } + } + + {:ok, %{"object" => object}} = QuoteToLinkTagPolicy.filter(activity) + + assert [_, tag] = object["tag"] + + assert tag == %{ + "type" => "Link", + "href" => quote_url, + "mediaType" => Pleroma.Constants.activity_json_canonical_mime_type() + } + end + + test "Bypass posts without quoteUrl" do + activity = %{ + "type" => "Create", + "actor" => "https://gleasonator.com/users/alex", + "object" => %{ + "type" => "Note", + "content" => "Nice post" + } + } + + assert {:ok, ^activity} = QuoteToLinkTagPolicy.filter(activity) + end +end From 8b98a98dfb4ab3db9b4f2347713d86ed9c87b61b Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 12 Jul 2023 14:37:12 -0400 Subject: [PATCH 164/199] Make InlineQuotePolicy history aware --- lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex | 3 +++ 1 file changed, 3 insertions(+) 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 a0eefefc0..aaa209aa1 100644 --- a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex @@ -53,6 +53,9 @@ def filter(object), do: {:ok, object} @impl true def describe, do: {:ok, %{}} + @impl Pleroma.Web.ActivityPub.MRF.Policy + def history_awareness, do: :auto + @impl true def config_description do %{ From 8596f926543126efdb4b8cfe70beab6812824398 Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 12 Jul 2023 14:58:20 -0400 Subject: [PATCH 165/199] Fix TransmogrifierTest --- test/pleroma/web/activity_pub/transmogrifier_test.exs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs index 4a192cdc0..5e58d75db 100644 --- a/test/pleroma/web/activity_pub/transmogrifier_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs @@ -123,7 +123,7 @@ test "it fixes both the Create and object contexts in a reply" do assert activity.data["context"] == object.data["context"] end - test "it drops link tags" do + test "it keeps link tags" do insert(:user, ap_id: "https://example.org/users/alice") message = File.read!("test/fixtures/fep-e232.json") |> Jason.decode!() @@ -131,10 +131,7 @@ test "it drops link tags" do assert {:ok, activity} = Transmogrifier.handle_incoming(message) object = Object.normalize(activity) - assert length(object.data["tag"]) == 1 - - tag = object.data["tag"] |> List.first() - assert tag["type"] == "Mention" + assert [%{"type" => "Mention"}, %{"type" => "Link"}] = object.data["tag"] end test "it accepts quote posts" do From 87353e5ad12799d12507253fe9a0363fd9f0c817 Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 12 Jul 2023 22:07:16 -0400 Subject: [PATCH 166/199] Fix config descriptions for mrf inline quote --- config/description.exs | 18 ------------------ .../activity_pub/mrf/inline_quote_policy.ex | 12 +++++++----- 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/config/description.exs b/config/description.exs index 079d187d5..d18649ae8 100644 --- a/config/description.exs +++ b/config/description.exs @@ -2994,24 +2994,6 @@ } ] }, - %{ - group: :pleroma, - key: :mrf_inline_quote, - tab: :mrf, - related_policy: "Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy", - label: "MRF Inline Quote Policy", - type: :group, - description: "Force quote url to appear in post content.", - children: [ - %{ - key: :template, - type: :string, - description: - "The template to append to the post. `{url}` will be replaced with the actual link to the quoted post.", - suggestions: ["RT: {url}"] - } - ] - }, %{ group: :pleroma, key: :modules, 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 aaa209aa1..171b22c5e 100644 --- a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex @@ -61,14 +61,16 @@ def config_description do %{ key: :mrf_inline_quote, related_policy: "Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy", - label: "MRF Inline Quote", - description: "Force quote post URLs inline", + label: "MRF Inline Quote Policy", + type: :group, + description: "Force quote url to appear in post content.", children: [ %{ - key: :prefix, + key: :template, type: :string, - description: "Prefix before the link", - suggestions: ["RT", "QT", "RE", "RN"] + description: + "The template to append to the post. `{url}` will be replaced with the actual link to the quoted post.", + suggestions: ["RT: {url}"] } ] } From 875b46d97d910ffd2c33ac26ed8dfe38f7672f52 Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 12 Jul 2023 23:29:23 -0400 Subject: [PATCH 167/199] Do not mention original poster when quoting --- lib/pleroma/web/common_api/activity_draft.ex | 3 --- test/pleroma/web/common_api_test.exs | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index 32921aa5a..ca1329284 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -139,9 +139,6 @@ defp in_reply_to(draft), do: draft defp quote_post(%{params: %{quote_id: id}} = draft) when not_empty_string(id) do case Activity.get_by_id_with_object(id) do - %Activity{actor: actor_ap_id} = activity when not_empty_string(actor_ap_id) -> - %__MODULE__{draft | quote_post: activity, mentions: [actor_ap_id]} - %Activity{} = activity -> %__MODULE__{draft | quote_post: activity} diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index 70a5b6fed..a98b16d4b 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -808,8 +808,8 @@ test "it allows quote posting" do assert quote_post.data["quoteUrl"] == quoted.data["id"] - # The OP is mentioned - assert quoted.data["actor"] in quote_post.data["to"] + # The OP is not mentioned + refute quoted.data["actor"] in quote_post.data["to"] end test "quote posting with explicit addressing doesn't mention the OP" do From a8b2f9205d16465a3b11d3802c966db3da908c5d Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 12 Jul 2023 23:47:31 -0400 Subject: [PATCH 168/199] Expose quote_id parameter on the api --- lib/pleroma/web/api_spec/schemas/status.ex | 5 +++++ lib/pleroma/web/mastodon_api/views/status_view.ex | 10 ++++++++++ .../web/mastodon_api/views/status_view_test.exs | 3 +++ 3 files changed, 18 insertions(+) diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex index 5d0eedb08..07f03134a 100644 --- a/lib/pleroma/web/api_spec/schemas/status.ex +++ b/lib/pleroma/web/api_spec/schemas/status.ex @@ -198,6 +198,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do nullable: true, description: "Quoted status (if any)" }, + quote_id: %Schema{ + nullable: true, + allOf: [FlakeID], + description: "ID of the status being quoted, if any" + }, quote_url: %Schema{ type: :string, format: :uri, diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index ba4a8f3eb..3d3039751 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -312,6 +312,8 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} # Here the implicit index of the current content is 0 chrono_order = history_len - 1 + quote_id = get_quote_id(activity) + quote_activity = get_quote(activity, opts) quote_post = @@ -431,6 +433,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} context: object.data["context"], in_reply_to_account_acct: reply_to_user && reply_to_user.nickname, quote: quote_post, + quote_id: quote_id, quote_url: object.data["quoteUrl"], quote_visible: visible_for_user?(quote_activity, opts[:for]), content: %{"text/plain" => content_plaintext}, @@ -689,6 +692,13 @@ def get_quote(%{data: %{"object" => _object}} = activity, _) do end end + defp get_quote_id(activity) do + case get_quote(activity, %{}) do + %Activity{id: id} -> id + _ -> nil + end + end + def render_content(%{data: %{"name" => name}} = object) when not is_nil(name) and name != "" do url = object.data["url"] || object.data["id"] diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs index 6d3a72970..221244d4e 100644 --- a/test/pleroma/web/mastodon_api/views/status_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs @@ -327,6 +327,7 @@ test "a note activity" do context: object_data["context"], in_reply_to_account_acct: nil, quote: nil, + quote_id: nil, quote_url: nil, quote_visible: false, content: %{"text/plain" => HTML.strip_tags(object_data["content"])}, @@ -435,10 +436,12 @@ test "a quote post" do status = StatusView.render("show.json", %{activity: quoted_quote_post}) assert status.pleroma.quote.id == to_string(quote_post.id) + assert status.pleroma.quote_id == to_string(quote_post.id) assert status.pleroma.quote_url == Object.normalize(quote_post).data["id"] # Quotes don't go more than one level deep refute status.pleroma.quote.pleroma.quote + assert status.pleroma.quote.pleroma.quote_id == to_string(post.id) assert status.pleroma.quote.pleroma.quote_url == Object.normalize(post).data["id"] # In an index From 08608afca5566f712acdc14b7c43976d6d071106 Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 12 Jul 2023 23:56:54 -0400 Subject: [PATCH 169/199] Fix quote_visible attribute --- .../web/mastodon_api/views/status_view.ex | 19 +++++++------------ .../mastodon_api/views/status_view_test.exs | 2 ++ 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 3d3039751..d070262cc 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -312,12 +312,16 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} # Here the implicit index of the current content is 0 chrono_order = history_len - 1 - quote_id = get_quote_id(activity) - quote_activity = get_quote(activity, opts) + quote_id = + case quote_activity do + %Activity{id: id} -> id + _ -> nil + end + quote_post = - if visible_for_user?(quote_activity, opts[:for]) do + if visible_for_user?(quote_activity, opts[:for]) and opts[:show_quote] != false do quote_rendering_opts = Map.merge(opts, %{activity: quote_activity, show_quote: false}) render("show.json", quote_rendering_opts) else @@ -671,8 +675,6 @@ def get_reply_to(%{data: %{"object" => _object}} = activity, _) do end end - def get_quote(_activity, %{show_quote: false}), do: nil - def get_quote(activity, %{quoted_activities: quoted_activities}) do object = Object.normalize(activity, fetch: false) @@ -692,13 +694,6 @@ def get_quote(%{data: %{"object" => _object}} = activity, _) do end end - defp get_quote_id(activity) do - case get_quote(activity, %{}) do - %Activity{id: id} -> id - _ -> nil - end - end - def render_content(%{data: %{"name" => name}} = object) when not is_nil(name) and name != "" do url = object.data["url"] || object.data["id"] diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs index 221244d4e..baa9b32f5 100644 --- a/test/pleroma/web/mastodon_api/views/status_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs @@ -438,11 +438,13 @@ test "a quote post" do assert status.pleroma.quote.id == to_string(quote_post.id) assert status.pleroma.quote_id == to_string(quote_post.id) assert status.pleroma.quote_url == Object.normalize(quote_post).data["id"] + assert status.pleroma.quote_visible # Quotes don't go more than one level deep refute status.pleroma.quote.pleroma.quote assert status.pleroma.quote.pleroma.quote_id == to_string(post.id) assert status.pleroma.quote.pleroma.quote_url == Object.normalize(post).data["id"] + assert status.pleroma.quote.pleroma.quote_visible # In an index [status] = StatusView.render("index.json", %{activities: [quoted_quote_post], as: :activity}) From 2b5636bf127b1987736c281d346a5771c8168c09 Mon Sep 17 00:00:00 2001 From: tusooa Date: Fri, 31 Mar 2023 21:47:37 -0400 Subject: [PATCH 170/199] Allow unified streaming endpoint --- .../web/mastodon_api/websocket_handler.ex | 17 ++++++++++++----- lib/pleroma/web/streamer.ex | 8 ++++++-- .../integration/mastodon_websocket_test.exs | 5 ++++- test/pleroma/web/streamer_test.exs | 4 ++++ 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex index 88444106d..97a1f1401 100644 --- a/lib/pleroma/web/mastodon_api/websocket_handler.ex +++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex @@ -32,8 +32,15 @@ def init(%{qs: qs} = req, state) do req end + topics = + if topic do + [topic] + else + [] + end + {:cowboy_websocket, req, - %{user: user, topic: topic, oauth_token: oauth_token, count: 0, timer: nil}, + %{user: user, topics: topics, oauth_token: oauth_token, count: 0, timer: nil}, %{idle_timeout: @timeout}} else {:error, :bad_topic} -> @@ -50,10 +57,10 @@ def init(%{qs: qs} = req, state) do def websocket_init(state) do Logger.debug( - "#{__MODULE__} accepted websocket connection for user #{(state.user || %{id: "anonymous"}).id}, topic #{state.topic}" + "#{__MODULE__} accepted websocket connection for user #{(state.user || %{id: "anonymous"}).id}, topics #{state.topics}" ) - Streamer.add_socket(state.topic, state.oauth_token) + Enum.each(state.topics, fn topic -> Streamer.add_socket(topic, state.oauth_token) end) {:ok, %{state | timer: timer()}} end @@ -109,10 +116,10 @@ def terminate(_reason, _req, []), do: :ok def terminate(reason, _req, state) do Logger.debug( - "#{__MODULE__} terminating websocket connection for user #{(state.user || %{id: "anonymous"}).id}, topic #{state.topic || "?"}: #{inspect(reason)}" + "#{__MODULE__} terminating websocket connection for user #{(state.user || %{id: "anonymous"}).id}, topics #{state.topics || "?"}: #{inspect(reason)}" ) - Streamer.remove_socket(state.topic) + Enum.each(state.topics, fn topic -> Streamer.remove_socket(topic) end) :ok end diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex index b9a04cc76..6b5fdbf0c 100644 --- a/lib/pleroma/web/streamer.ex +++ b/lib/pleroma/web/streamer.ex @@ -59,10 +59,14 @@ defp can_access_stream(user, oauth_token, kind) do end @doc "Expand and authorizes a stream" - @spec get_topic(stream :: String.t(), User.t() | nil, Token.t() | nil, Map.t()) :: - {:ok, topic :: String.t()} | {:error, :bad_topic} + @spec get_topic(stream :: String.t() | nil, User.t() | nil, Token.t() | nil, Map.t()) :: + {:ok, topic :: String.t() | nil} | {:error, :bad_topic} def get_topic(stream, user, oauth_token, params \\ %{}) + def get_topic(nil = _stream, _user, _oauth_token, _params) do + {:ok, nil} + end + # Allow all public steams if the instance allows unauthenticated access. # Otherwise, only allow users with valid oauth tokens. def get_topic(stream, user, oauth_token, _params) when stream in @public_streams do diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs index 9be0445c0..3b120c10e 100644 --- a/test/pleroma/integration/mastodon_websocket_test.exs +++ b/test/pleroma/integration/mastodon_websocket_test.exs @@ -33,7 +33,6 @@ def start_socket(qs \\ nil, headers \\ []) do test "refuses invalid requests" do capture_log(fn -> - assert {:error, %WebSockex.RequestError{code: 404}} = start_socket() assert {:error, %WebSockex.RequestError{code: 404}} = start_socket("?stream=ncjdk") Process.sleep(30) end) @@ -49,6 +48,10 @@ test "requires authentication and a valid token for protected streams" do end) end + test "allows unified stream" do + assert {:ok, _} = start_socket() + end + test "allows public streams without authentication" do assert {:ok, _} = start_socket("?stream=public") assert {:ok, _} = start_socket("?stream=public:local") diff --git a/test/pleroma/web/streamer_test.exs b/test/pleroma/web/streamer_test.exs index 7ab0e379b..da97b87d8 100644 --- a/test/pleroma/web/streamer_test.exs +++ b/test/pleroma/web/streamer_test.exs @@ -22,6 +22,10 @@ defmodule Pleroma.Web.StreamerTest do setup do: clear_config([:instance, :skip_thread_containment]) describe "get_topic/_ (unauthenticated)" do + test "allows no stream" do + assert {:ok, nil} = Streamer.get_topic(nil, nil, nil) + end + test "allows public" do assert {:ok, "public"} = Streamer.get_topic("public", nil, nil) assert {:ok, "public:local"} = Streamer.get_topic("public:local", nil, nil) From 273cda63ad79b61f4d37e4b7603694908e894e4f Mon Sep 17 00:00:00 2001 From: tusooa Date: Fri, 31 Mar 2023 22:55:52 -0400 Subject: [PATCH 171/199] Allow subscribing to streams --- .../web/mastodon_api/websocket_handler.ex | 74 +++++++++++++++ lib/pleroma/web/views/streamer_view.ex | 18 ++++ .../integration/mastodon_websocket_test.exs | 90 +++++++++++++++++++ 3 files changed, 182 insertions(+) diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex index 97a1f1401..a42a9a63c 100644 --- a/lib/pleroma/web/mastodon_api/websocket_handler.ex +++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do alias Pleroma.User alias Pleroma.Web.OAuth.Token alias Pleroma.Web.Streamer + alias Pleroma.Web.StreamerView @behaviour :cowboy_websocket @@ -73,6 +74,16 @@ def websocket_handle(:pong, state) do # We only receive pings for now def websocket_handle(:ping, state), do: {:ok, state} + def websocket_handle({:text, text}, state) do + with {:ok, %{} = event} <- Jason.decode(text) do + handle_client_event(event, state) + else + _ -> + Logger.error("#{__MODULE__} received non-JSON event: #{inspect(text)}") + {:ok, state} + end + end + def websocket_handle(frame, state) do Logger.error("#{__MODULE__} received frame: #{inspect(frame)}") {:ok, state} @@ -144,4 +155,67 @@ defp authenticate_request(access_token, sec_websocket) do defp timer do Process.send_after(self(), :tick, @tick) end + + defp handle_client_event(%{"type" => "subscribe", "stream" => _topic} = params, state) do + with {_, {:ok, topic}} <- + {:topic, Streamer.get_topic(params["stream"], state.user, state.oauth_token, params)}, + {_, false} <- {:subscribed, topic in state.topics} do + Streamer.add_socket(topic, state.oauth_token) + + {[ + {:text, + StreamerView.render("pleroma_respond.json", %{type: "subscribe", result: "success"})} + ], %{state | topics: [topic | state.topics]}} + else + {:subscribed, true} -> + {[ + {:text, + StreamerView.render("pleroma_respond.json", %{type: "subscribe", result: "ignored"})} + ], state} + + {:topic, {:error, error}} -> + {[ + {:text, + StreamerView.render("pleroma_respond.json", %{ + type: "subscribe", + result: "error", + error: error + })} + ], state} + end + end + + defp handle_client_event(%{"type" => "unsubscribe", "stream" => _topic} = params, state) do + with {_, {:ok, topic}} <- + {:topic, Streamer.get_topic(params["stream"], state.user, state.oauth_token, params)}, + {_, true} <- {:subscribed, topic in state.topics} do + Streamer.remove_socket(topic) + + {[ + {:text, + StreamerView.render("pleroma_respond.json", %{type: "unsubscribe", result: "success"})} + ], %{state | topics: List.delete(state.topics, topic)}} + else + {:subscribed, false} -> + {[ + {:text, + StreamerView.render("pleroma_respond.json", %{type: "unsubscribe", result: "ignored"})} + ], state} + + {:topic, {:error, error}} -> + {[ + {:text, + StreamerView.render("pleroma_respond.json", %{ + type: "unsubscribe", + result: "error", + error: error + })} + ], state} + end + end + + defp handle_client_event(params, state) do + Logger.error("#{__MODULE__} received unknown event: #{inspect(params)}") + {[], state} + end end diff --git a/lib/pleroma/web/views/streamer_view.ex b/lib/pleroma/web/views/streamer_view.ex index 6a55242b0..19f098783 100644 --- a/lib/pleroma/web/views/streamer_view.ex +++ b/lib/pleroma/web/views/streamer_view.ex @@ -135,4 +135,22 @@ def render("conversation.json", %Participation{} = participation) do } |> Jason.encode!() end + + def render("pleroma_respond.json", %{type: type, result: result} = params) do + %{ + event: "pleroma.respond", + payload: + %{ + result: result, + type: type + } + |> Map.merge(maybe_error(params)) + |> Jason.encode!() + } + |> Jason.encode!() + end + + defp maybe_error(%{error: :bad_topic}), do: %{error: "bad_topic"} + defp maybe_error(%{error: :unauthorized}), do: %{error: "unauthorized"} + defp maybe_error(_), do: %{} end diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs index 3b120c10e..9db0f714f 100644 --- a/test/pleroma/integration/mastodon_websocket_test.exs +++ b/test/pleroma/integration/mastodon_websocket_test.exs @@ -31,6 +31,13 @@ def start_socket(qs \\ nil, headers \\ []) do WebsocketClient.start_link(self(), path, headers) end + defp decode_json(json) do + with {:ok, %{"event" => event, "payload" => payload_text}} <- Jason.decode(json), + {:ok, payload} <- Jason.decode(payload_text) do + {:ok, %{"event" => event, "payload" => payload}} + end + end + test "refuses invalid requests" do capture_log(fn -> assert {:error, %WebSockex.RequestError{code: 404}} = start_socket("?stream=ncjdk") @@ -79,6 +86,89 @@ test "receives well formatted events" do assert json == view_json end + describe "subscribing via WebSocket" do + test "can subscribe" do + user = insert(:user) + {:ok, pid} = start_socket() + WebsocketClient.send_text(pid, %{type: "subscribe", stream: "public"} |> Jason.encode!()) + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "pleroma.respond", + "payload" => %{"type" => "subscribe", "result" => "success"} + }} = decode_json(raw_json) + + {:ok, activity} = CommonAPI.post(user, %{status: "nice echo chamber"}) + + assert_receive {:text, raw_json}, 1_000 + assert {:ok, json} = Jason.decode(raw_json) + + assert "update" == json["event"] + assert json["payload"] + assert {:ok, json} = Jason.decode(json["payload"]) + + view_json = + Pleroma.Web.MastodonAPI.StatusView.render("show.json", activity: activity, for: nil) + |> Jason.encode!() + |> Jason.decode!() + + assert json == view_json + end + + test "won't double subscribe" do + user = insert(:user) + {:ok, pid} = start_socket() + WebsocketClient.send_text(pid, %{type: "subscribe", stream: "public"} |> Jason.encode!()) + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "pleroma.respond", + "payload" => %{"type" => "subscribe", "result" => "success"} + }} = decode_json(raw_json) + + WebsocketClient.send_text(pid, %{type: "subscribe", stream: "public"} |> Jason.encode!()) + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "pleroma.respond", + "payload" => %{"type" => "subscribe", "result" => "ignored"} + }} = decode_json(raw_json) + + {:ok, _activity} = CommonAPI.post(user, %{status: "nice echo chamber"}) + + assert_receive {:text, _}, 1_000 + refute_receive {:text, _}, 1_000 + end + + test "can unsubscribe" do + user = insert(:user) + {:ok, pid} = start_socket() + WebsocketClient.send_text(pid, %{type: "subscribe", stream: "public"} |> Jason.encode!()) + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "pleroma.respond", + "payload" => %{"type" => "subscribe", "result" => "success"} + }} = decode_json(raw_json) + + WebsocketClient.send_text(pid, %{type: "unsubscribe", stream: "public"} |> Jason.encode!()) + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "pleroma.respond", + "payload" => %{"type" => "unsubscribe", "result" => "success"} + }} = decode_json(raw_json) + + {:ok, _activity} = CommonAPI.post(user, %{status: "nice echo chamber"}) + refute_receive {:text, _}, 1_000 + end + end + describe "with a valid user token" do setup do {:ok, app} = From 21395aa5090f2a53bdbe0ef5fac46693d16025ed Mon Sep 17 00:00:00 2001 From: tusooa Date: Fri, 31 Mar 2023 23:19:57 -0400 Subject: [PATCH 172/199] Allow authenticating via client-sent events --- .../web/mastodon_api/websocket_handler.ex | 36 +++++++++ lib/pleroma/web/views/streamer_view.ex | 1 + .../integration/mastodon_websocket_test.exs | 81 +++++++++++++++++++ 3 files changed, 118 insertions(+) diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex index a42a9a63c..6233c3340 100644 --- a/lib/pleroma/web/mastodon_api/websocket_handler.ex +++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex @@ -214,6 +214,42 @@ defp handle_client_event(%{"type" => "unsubscribe", "stream" => _topic} = params end end + defp handle_client_event( + %{"type" => "pleroma.authenticate", "token" => access_token} = _params, + state + ) do + with {:auth, nil, nil} <- {:auth, state.user, state.oauth_token}, + {:ok, user, oauth_token} <- authenticate_request(access_token, nil) do + {[ + {:text, + StreamerView.render("pleroma_respond.json", %{ + type: "pleroma.authenticate", + result: "success" + })} + ], %{state | user: user, oauth_token: oauth_token}} + else + {:auth, _, _} -> + {[ + {:text, + StreamerView.render("pleroma_respond.json", %{ + type: "pleroma.authenticate", + result: "error", + error: :already_authenticated + })} + ], state} + + _ -> + {[ + {:text, + StreamerView.render("pleroma_respond.json", %{ + type: "pleroma.authenticate", + result: "error", + error: :unauthorized + })} + ], state} + end + end + defp handle_client_event(params, state) do Logger.error("#{__MODULE__} received unknown event: #{inspect(params)}") {[], state} diff --git a/lib/pleroma/web/views/streamer_view.ex b/lib/pleroma/web/views/streamer_view.ex index 19f098783..0cdcb1918 100644 --- a/lib/pleroma/web/views/streamer_view.ex +++ b/lib/pleroma/web/views/streamer_view.ex @@ -152,5 +152,6 @@ def render("pleroma_respond.json", %{type: type, result: result} = params) do defp maybe_error(%{error: :bad_topic}), do: %{error: "bad_topic"} defp maybe_error(%{error: :unauthorized}), do: %{error: "unauthorized"} + defp maybe_error(%{error: :already_authenticated}), do: %{error: "already_authenticated"} defp maybe_error(_), do: %{} end diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs index 9db0f714f..827c7b5b0 100644 --- a/test/pleroma/integration/mastodon_websocket_test.exs +++ b/test/pleroma/integration/mastodon_websocket_test.exs @@ -224,6 +224,87 @@ test "accepts valid token on Sec-WebSocket-Protocol header", %{token: token} do end) end + test "accepts valid token on client-sent event", %{token: token} do + assert {:ok, pid} = start_socket() + + WebsocketClient.send_text( + pid, + %{type: "pleroma.authenticate", token: token.token} |> Jason.encode!() + ) + + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "pleroma.respond", + "payload" => %{"type" => "pleroma.authenticate", "result" => "success"} + }} = decode_json(raw_json) + + WebsocketClient.send_text(pid, %{type: "subscribe", stream: "user"} |> Jason.encode!()) + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "pleroma.respond", + "payload" => %{"type" => "subscribe", "result" => "success"} + }} = decode_json(raw_json) + end + + test "rejects invalid token on client-sent event" do + assert {:ok, pid} = start_socket() + + WebsocketClient.send_text( + pid, + %{type: "pleroma.authenticate", token: "Something else"} |> Jason.encode!() + ) + + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "pleroma.respond", + "payload" => %{ + "type" => "pleroma.authenticate", + "result" => "error", + "error" => "unauthorized" + } + }} = decode_json(raw_json) + end + + test "rejects new authenticate request if already logged-in", %{token: token} do + assert {:ok, pid} = start_socket() + + WebsocketClient.send_text( + pid, + %{type: "pleroma.authenticate", token: token.token} |> Jason.encode!() + ) + + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "pleroma.respond", + "payload" => %{"type" => "pleroma.authenticate", "result" => "success"} + }} = decode_json(raw_json) + + WebsocketClient.send_text( + pid, + %{type: "pleroma.authenticate", token: "Something else"} |> Jason.encode!() + ) + + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "pleroma.respond", + "payload" => %{ + "type" => "pleroma.authenticate", + "result" => "error", + "error" => "already_authenticated" + } + }} = decode_json(raw_json) + end + test "disconnect when token is revoked", %{app: app, user: user, token: token} do assert {:ok, _} = start_socket("?stream=user:notification&access_token=#{token.token}") assert {:ok, _} = start_socket("?stream=user&access_token=#{token.token}") From 7d005e8c93b22dc3d7be1a66dd2d404b7f54306a Mon Sep 17 00:00:00 2001 From: tusooa Date: Sat, 1 Apr 2023 01:25:13 -0400 Subject: [PATCH 173/199] Return stream attribute in server-sent events --- lib/pleroma/constants.ex | 4 ++ .../web/mastodon_api/websocket_handler.ex | 4 +- lib/pleroma/web/streamer.ex | 27 +++++---- lib/pleroma/web/views/streamer_view.ex | 42 +++++++++++--- .../integration/mastodon_websocket_test.exs | 33 +++++++++++ test/pleroma/web/streamer_test.exs | 58 +++++++++---------- 6 files changed, 119 insertions(+), 49 deletions(-) diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex index ed60bcc37..77bc4bfac 100644 --- a/lib/pleroma/constants.ex +++ b/lib/pleroma/constants.ex @@ -94,4 +94,8 @@ defmodule Pleroma.Constants do "application/activity+json" ] ) + + const(public_streams, + do: ["public", "public:local", "public:media", "public:local:media"] + ) end diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex index 6233c3340..2707673ba 100644 --- a/lib/pleroma/web/mastodon_api/websocket_handler.ex +++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex @@ -89,11 +89,11 @@ def websocket_handle(frame, state) do {:ok, state} end - def websocket_info({:render_with_user, view, template, item}, state) do + def websocket_info({:render_with_user, view, template, item, topic}, state) do user = %User{} = User.get_cached_by_ap_id(state.user.ap_id) unless Streamer.filtered_by_user?(user, item) do - websocket_info({:text, view.render(template, item, user)}, %{state | user: user}) + websocket_info({:text, view.render(template, item, user, topic)}, %{state | user: user}) else {:ok, state} end diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex index 6b5fdbf0c..70e46617a 100644 --- a/lib/pleroma/web/streamer.ex +++ b/lib/pleroma/web/streamer.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.Streamer do require Logger + require Pleroma.Constants alias Pleroma.Activity alias Pleroma.Chat.MessageReference @@ -24,7 +25,7 @@ defmodule Pleroma.Web.Streamer do def registry, do: @registry - @public_streams ["public", "public:local", "public:media", "public:local:media"] + @public_streams Pleroma.Constants.public_streams() @local_streams ["public:local", "public:local:media"] @user_streams ["user", "user:notification", "direct", "user:pleroma_chat"] @@ -223,8 +224,8 @@ defp do_stream("direct", item) do end defp do_stream("follow_relationship", item) do - text = StreamerView.render("follow_relationships_update.json", item) user_topic = "user:#{item.follower.id}" + text = StreamerView.render("follow_relationships_update.json", item, user_topic) Logger.debug("Trying to push follow relationship update to #{user_topic}\n\n") @@ -270,9 +271,11 @@ defp do_stream("list", item) do defp do_stream(topic, %Notification{} = item) when topic in ["user", "user:notification"] do - Registry.dispatch(@registry, "#{topic}:#{item.user_id}", fn list -> + user_topic = "#{topic}:#{item.user_id}" + + Registry.dispatch(@registry, user_topic, fn list -> Enum.each(list, fn {pid, _auth} -> - send(pid, {:render_with_user, StreamerView, "notification.json", item}) + send(pid, {:render_with_user, StreamerView, "notification.json", item, user_topic}) end) end) end @@ -281,7 +284,7 @@ defp do_stream(topic, {user, %MessageReference{} = cm_ref}) when topic in ["user", "user:pleroma_chat"] do topic = "#{topic}:#{user.id}" - text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}) + text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}, topic) Registry.dispatch(@registry, topic, fn list -> Enum.each(list, fn {pid, _auth} -> @@ -309,7 +312,7 @@ defp do_stream(topic, item) do end defp push_to_socket(topic, %Participation{} = participation) do - rendered = StreamerView.render("conversation.json", participation) + rendered = StreamerView.render("conversation.json", participation, topic) Registry.dispatch(@registry, topic, fn list -> Enum.each(list, fn {pid, _} -> @@ -337,12 +340,15 @@ defp push_to_socket(topic, %Activity{data: %{"type" => "Update"}} = item) do Pleroma.Activity.get_create_by_object_ap_id(item.object.data["id"]) |> Map.put(:object, item.object) - anon_render = StreamerView.render("status_update.json", create_activity) + anon_render = StreamerView.render("status_update.json", create_activity, topic) Registry.dispatch(@registry, topic, fn list -> Enum.each(list, fn {pid, auth?} -> if auth? do - send(pid, {:render_with_user, StreamerView, "status_update.json", create_activity}) + send( + pid, + {:render_with_user, StreamerView, "status_update.json", create_activity, topic} + ) else send(pid, {:text, anon_render}) end @@ -351,12 +357,13 @@ defp push_to_socket(topic, %Activity{data: %{"type" => "Update"}} = item) do end defp push_to_socket(topic, item) do - anon_render = StreamerView.render("update.json", item) + Logger.debug("topic=#{topic}") + anon_render = StreamerView.render("update.json", item, topic) Registry.dispatch(@registry, topic, fn list -> Enum.each(list, fn {pid, auth?} -> if auth? do - send(pid, {:render_with_user, StreamerView, "update.json", item}) + send(pid, {:render_with_user, StreamerView, "update.json", item, topic}) else send(pid, {:text, anon_render}) end diff --git a/lib/pleroma/web/views/streamer_view.ex b/lib/pleroma/web/views/streamer_view.ex index 0cdcb1918..f591da9a6 100644 --- a/lib/pleroma/web/views/streamer_view.ex +++ b/lib/pleroma/web/views/streamer_view.ex @@ -11,8 +11,11 @@ defmodule Pleroma.Web.StreamerView do alias Pleroma.User alias Pleroma.Web.MastodonAPI.NotificationView - def render("update.json", %Activity{} = activity, %User{} = user) do + require Pleroma.Constants + + def render("update.json", %Activity{} = activity, %User{} = user, topic) do %{ + stream: render("stream.json", %{topic: topic}), event: "update", payload: Pleroma.Web.MastodonAPI.StatusView.render( @@ -25,8 +28,9 @@ def render("update.json", %Activity{} = activity, %User{} = user) do |> Jason.encode!() end - def render("status_update.json", %Activity{} = activity, %User{} = user) do + def render("status_update.json", %Activity{} = activity, %User{} = user, topic) do %{ + stream: render("stream.json", %{topic: topic}), event: "status.update", payload: Pleroma.Web.MastodonAPI.StatusView.render( @@ -39,8 +43,9 @@ def render("status_update.json", %Activity{} = activity, %User{} = user) do |> Jason.encode!() end - def render("notification.json", %Notification{} = notify, %User{} = user) do + def render("notification.json", %Notification{} = notify, %User{} = user, topic) do %{ + stream: render("stream.json", %{topic: topic}), event: "notification", payload: NotificationView.render( @@ -52,8 +57,9 @@ def render("notification.json", %Notification{} = notify, %User{} = user) do |> Jason.encode!() end - def render("update.json", %Activity{} = activity) do + def render("update.json", %Activity{} = activity, topic) do %{ + stream: render("stream.json", %{topic: topic}), event: "update", payload: Pleroma.Web.MastodonAPI.StatusView.render( @@ -65,8 +71,9 @@ def render("update.json", %Activity{} = activity) do |> Jason.encode!() end - def render("status_update.json", %Activity{} = activity) do + def render("status_update.json", %Activity{} = activity, topic) do %{ + stream: render("stream.json", %{topic: topic}), event: "status.update", payload: Pleroma.Web.MastodonAPI.StatusView.render( @@ -78,7 +85,7 @@ def render("status_update.json", %Activity{} = activity) do |> Jason.encode!() end - def render("chat_update.json", %{chat_message_reference: cm_ref}) do + def render("chat_update.json", %{chat_message_reference: cm_ref}, topic) do # Explicitly giving the cmr for the object here, so we don't accidentally # send a later 'last_message' that was inserted between inserting this and # streaming it out @@ -93,6 +100,7 @@ def render("chat_update.json", %{chat_message_reference: cm_ref}) do ) %{ + stream: render("stream.json", %{topic: topic}), event: "pleroma:chat_update", payload: representation @@ -101,8 +109,9 @@ def render("chat_update.json", %{chat_message_reference: cm_ref}) do |> Jason.encode!() end - def render("follow_relationships_update.json", item) do + def render("follow_relationships_update.json", item, topic) do %{ + stream: render("stream.json", %{topic: topic}), event: "pleroma:follow_relationships_update", payload: %{ @@ -123,8 +132,9 @@ def render("follow_relationships_update.json", item) do |> Jason.encode!() end - def render("conversation.json", %Participation{} = participation) do + def render("conversation.json", %Participation{} = participation, topic) do %{ + stream: render("stream.json", %{topic: topic}), event: "conversation", payload: Pleroma.Web.MastodonAPI.ConversationView.render("participation.json", %{ @@ -150,6 +160,22 @@ def render("pleroma_respond.json", %{type: type, result: result} = params) do |> Jason.encode!() end + def render("stream.json", %{topic: "user:pleroma_chat:" <> _}), do: ["user:pleroma_chat"] + def render("stream.json", %{topic: "user:notification:" <> _}), do: ["user:notification"] + def render("stream.json", %{topic: "user:" <> _}), do: ["user"] + def render("stream.json", %{topic: "direct:" <> _}), do: ["direct"] + def render("stream.json", %{topic: "list:" <> id}), do: ["list", id] + def render("stream.json", %{topic: "hashtag:" <> tag}), do: ["hashtag", tag] + + def render("stream.json", %{topic: "public:remote:media:" <> instance}), + do: ["public:remote:media", instance] + + def render("stream.json", %{topic: "public:remote:" <> instance}), + do: ["public:remote", instance] + + def render("stream.json", %{topic: stream}) when stream in Pleroma.Constants.public_streams(), + do: [stream] + defp maybe_error(%{error: :bad_topic}), do: %{error: "bad_topic"} defp maybe_error(%{error: :unauthorized}), do: %{error: "unauthorized"} defp maybe_error(%{error: :already_authenticated}), do: %{error: "already_authenticated"} diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs index 827c7b5b0..d2e98df3b 100644 --- a/test/pleroma/integration/mastodon_websocket_test.exs +++ b/test/pleroma/integration/mastodon_websocket_test.exs @@ -116,6 +116,39 @@ test "can subscribe" do assert json == view_json end + test "can subscribe to multiple streams" do + user = insert(:user) + {:ok, pid} = start_socket() + WebsocketClient.send_text(pid, %{type: "subscribe", stream: "public"} |> Jason.encode!()) + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "pleroma.respond", + "payload" => %{"type" => "subscribe", "result" => "success"} + }} = decode_json(raw_json) + + WebsocketClient.send_text(pid, %{type: "subscribe", stream: "hashtag", tag: "mew"} |> Jason.encode!()) + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "pleroma.respond", + "payload" => %{"type" => "subscribe", "result" => "success"} + }} = decode_json(raw_json) + + {:ok, _activity} = CommonAPI.post(user, %{status: "nice echo chamber #mew"}) + + assert_receive {:text, raw_json}, 1_000 + assert {:ok, %{"stream" => stream1}} = Jason.decode(raw_json) + assert_receive {:text, raw_json}, 1_000 + assert {:ok, %{"stream" => stream2}} = Jason.decode(raw_json) + + streams = [stream1, stream2] + assert ["hashtag", "mew"] in streams + assert ["public"] in streams + end + test "won't double subscribe" do user = insert(:user) {:ok, pid} = start_socket() diff --git a/test/pleroma/web/streamer_test.exs b/test/pleroma/web/streamer_test.exs index da97b87d8..cc57a2989 100644 --- a/test/pleroma/web/streamer_test.exs +++ b/test/pleroma/web/streamer_test.exs @@ -246,7 +246,7 @@ test "it streams the user's post in the 'user' stream", %{user: user, token: oau Streamer.get_topic_and_add_socket("user", user, oauth_token) {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) - assert_receive {:render_with_user, _, _, ^activity} + assert_receive {:render_with_user, _, _, ^activity, _} refute Streamer.filtered_by_user?(user, activity) end @@ -257,7 +257,7 @@ test "it streams boosts of the user in the 'user' stream", %{user: user, token: {:ok, activity} = CommonAPI.post(other_user, %{status: "hey"}) {:ok, announce} = CommonAPI.repeat(activity.id, user) - assert_receive {:render_with_user, Pleroma.Web.StreamerView, "update.json", ^announce} + assert_receive {:render_with_user, Pleroma.Web.StreamerView, "update.json", ^announce, _} refute Streamer.filtered_by_user?(user, announce) end @@ -310,7 +310,7 @@ test "it streams boosts of mastodon user in the 'user' stream", %{ {:ok, %Pleroma.Activity{data: _data, local: false} = announce} = Pleroma.Web.ActivityPub.Transmogrifier.handle_incoming(data) - assert_receive {:render_with_user, Pleroma.Web.StreamerView, "update.json", ^announce} + assert_receive {:render_with_user, Pleroma.Web.StreamerView, "update.json", ^announce, _} refute Streamer.filtered_by_user?(user, announce) end @@ -322,7 +322,7 @@ test "it sends notify to in the 'user' stream", %{ Streamer.get_topic_and_add_socket("user", user, oauth_token) Streamer.stream("user", notify) - assert_receive {:render_with_user, _, _, ^notify} + assert_receive {:render_with_user, _, _, ^notify, _} refute Streamer.filtered_by_user?(user, notify) end @@ -334,7 +334,7 @@ test "it sends notify to in the 'user:notification' stream", %{ Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) Streamer.stream("user:notification", notify) - assert_receive {:render_with_user, _, _, ^notify} + assert_receive {:render_with_user, _, _, ^notify, _} refute Streamer.filtered_by_user?(user, notify) end @@ -355,7 +355,7 @@ test "it sends chat messages to the 'user:pleroma_chat' stream", %{ Streamer.get_topic_and_add_socket("user:pleroma_chat", user, oauth_token) Streamer.stream("user:pleroma_chat", {user, cm_ref}) - text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}) + text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}, "user:pleroma_chat:#{user.id}") assert text =~ "hey cirno" assert_receive {:text, ^text} @@ -373,7 +373,7 @@ test "it sends chat messages to the 'user' stream", %{user: user, token: oauth_t Streamer.get_topic_and_add_socket("user", user, oauth_token) Streamer.stream("user", {user, cm_ref}) - text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}) + text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}, "user:#{user.id}") assert text =~ "hey cirno" assert_receive {:text, ^text} @@ -394,7 +394,7 @@ test "it sends chat message notifications to the 'user:notification' stream", %{ Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) Streamer.stream("user:notification", notify) - assert_receive {:render_with_user, _, _, ^notify} + assert_receive {:render_with_user, _, _, ^notify, _} refute Streamer.filtered_by_user?(user, notify) end @@ -440,7 +440,7 @@ test "it sends favorite to 'user:notification' stream'", %{ Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) {:ok, favorite_activity} = CommonAPI.favorite(user2, activity.id) - assert_receive {:render_with_user, _, "notification.json", notif} + assert_receive {:render_with_user, _, "notification.json", notif, _} assert notif.activity.id == favorite_activity.id refute Streamer.filtered_by_user?(user, notif) end @@ -469,7 +469,7 @@ test "it sends follow activities to the 'user:notification' stream", %{ Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) {:ok, _follower, _followed, follow_activity} = CommonAPI.follow(user2, user) - assert_receive {:render_with_user, _, "notification.json", notif} + assert_receive {:render_with_user, _, "notification.json", notif, _} assert notif.activity.id == follow_activity.id refute Streamer.filtered_by_user?(user, notif) end @@ -534,7 +534,7 @@ test "it streams edits in the 'user' stream", %{user: user, token: oauth_token} {:ok, edited} = CommonAPI.update(sender, activity, %{status: "mew mew"}) create = Pleroma.Activity.get_create_by_object_ap_id_with_object(activity.object.data["id"]) - assert_receive {:render_with_user, _, "status_update.json", ^create} + assert_receive {:render_with_user, _, "status_update.json", ^create, _} refute Streamer.filtered_by_user?(user, edited) end @@ -545,7 +545,7 @@ test "it streams own edits in the 'user' stream", %{user: user, token: oauth_tok {:ok, edited} = CommonAPI.update(user, activity, %{status: "mew mew"}) create = Pleroma.Activity.get_create_by_object_ap_id_with_object(activity.object.data["id"]) - assert_receive {:render_with_user, _, "status_update.json", ^create} + assert_receive {:render_with_user, _, "status_update.json", ^create, _} refute Streamer.filtered_by_user?(user, edited) end end @@ -558,7 +558,7 @@ test "it sends to public (authenticated)" do Streamer.get_topic_and_add_socket("public", user, oauth_token) {:ok, activity} = CommonAPI.post(other_user, %{status: "Test"}) - assert_receive {:render_with_user, _, _, ^activity} + assert_receive {:render_with_user, _, _, ^activity, _} refute Streamer.filtered_by_user?(other_user, activity) end @@ -658,7 +658,7 @@ test "it filters to user if recipients invalid and thread containment is enabled Streamer.get_topic_and_add_socket("public", user, oauth_token) Streamer.stream("public", activity) - assert_receive {:render_with_user, _, _, ^activity} + assert_receive {:render_with_user, _, _, ^activity, _} assert Streamer.filtered_by_user?(user, activity) end @@ -680,7 +680,7 @@ test "it sends message if recipients invalid and thread containment is disabled" Streamer.get_topic_and_add_socket("public", user, oauth_token) Streamer.stream("public", activity) - assert_receive {:render_with_user, _, _, ^activity} + assert_receive {:render_with_user, _, _, ^activity, _} refute Streamer.filtered_by_user?(user, activity) end @@ -703,7 +703,7 @@ test "it sends message if recipients invalid and thread containment is enabled b Streamer.get_topic_and_add_socket("public", user, oauth_token) Streamer.stream("public", activity) - assert_receive {:render_with_user, _, _, ^activity} + assert_receive {:render_with_user, _, _, ^activity, _} refute Streamer.filtered_by_user?(user, activity) end end @@ -717,7 +717,7 @@ test "it filters messages involving blocked users", %{user: user, token: oauth_t Streamer.get_topic_and_add_socket("public", user, oauth_token) {:ok, activity} = CommonAPI.post(blocked_user, %{status: "Test"}) - assert_receive {:render_with_user, _, _, ^activity} + assert_receive {:render_with_user, _, _, ^activity, _} assert Streamer.filtered_by_user?(user, activity) end @@ -734,17 +734,17 @@ test "it filters messages transitively involving blocked users", %{ {:ok, activity_one} = CommonAPI.post(friend, %{status: "hey! @#{blockee.nickname}"}) - assert_receive {:render_with_user, _, _, ^activity_one} + assert_receive {:render_with_user, _, _, ^activity_one, _} assert Streamer.filtered_by_user?(blocker, activity_one) {:ok, activity_two} = CommonAPI.post(blockee, %{status: "hey! @#{friend.nickname}"}) - assert_receive {:render_with_user, _, _, ^activity_two} + assert_receive {:render_with_user, _, _, ^activity_two, _} assert Streamer.filtered_by_user?(blocker, activity_two) {:ok, activity_three} = CommonAPI.post(blockee, %{status: "hey! @#{blocker.nickname}"}) - assert_receive {:render_with_user, _, _, ^activity_three} + assert_receive {:render_with_user, _, _, ^activity_three, _} assert Streamer.filtered_by_user?(blocker, activity_three) end end @@ -805,7 +805,7 @@ test "it sends wanted private posts to list", %{user: user_a, token: user_a_toke visibility: "private" }) - assert_receive {:render_with_user, _, _, ^activity} + assert_receive {:render_with_user, _, _, ^activity, _} refute Streamer.filtered_by_user?(user_a, activity) end end @@ -823,7 +823,7 @@ test "it filters muted reblogs", %{user: user1, token: user1_token} do Streamer.get_topic_and_add_socket("user", user1, user1_token) {:ok, announce_activity} = CommonAPI.repeat(create_activity.id, user2) - assert_receive {:render_with_user, _, _, ^announce_activity} + assert_receive {:render_with_user, _, _, ^announce_activity, _} assert Streamer.filtered_by_user?(user1, announce_activity) end @@ -839,7 +839,7 @@ test "it filters reblog notification for reblog-muted actors", %{ Streamer.get_topic_and_add_socket("user", user1, user1_token) {:ok, _announce_activity} = CommonAPI.repeat(create_activity.id, user2) - assert_receive {:render_with_user, _, "notification.json", notif} + assert_receive {:render_with_user, _, "notification.json", notif, _} assert Streamer.filtered_by_user?(user1, notif) end @@ -855,7 +855,7 @@ test "it send non-reblog notification for reblog-muted actors", %{ Streamer.get_topic_and_add_socket("user", user1, user1_token) {:ok, _favorite_activity} = CommonAPI.favorite(user2, create_activity.id) - assert_receive {:render_with_user, _, "notification.json", notif} + assert_receive {:render_with_user, _, "notification.json", notif, _} refute Streamer.filtered_by_user?(user1, notif) end end @@ -870,7 +870,7 @@ test "it filters posts from muted threads" do {:ok, activity} = CommonAPI.post(user, %{status: "super hot take"}) {:ok, _} = CommonAPI.add_mute(user2, activity) - assert_receive {:render_with_user, _, _, ^activity} + assert_receive {:render_with_user, _, _, ^activity, _} assert Streamer.filtered_by_user?(user2, activity) end end @@ -912,7 +912,7 @@ test "it doesn't send conversation update to the 'direct' stream when the last m }) create_activity_id = create_activity.id - assert_receive {:render_with_user, _, _, ^create_activity} + assert_receive {:render_with_user, _, _, ^create_activity, _} assert_receive {:text, received_conversation1} assert %{"event" => "conversation", "payload" => _} = Jason.decode!(received_conversation1) @@ -947,8 +947,8 @@ test "it sends conversation update to the 'direct' stream when a message is dele visibility: "direct" }) - assert_receive {:render_with_user, _, _, ^create_activity} - assert_receive {:render_with_user, _, _, ^create_activity2} + assert_receive {:render_with_user, _, _, ^create_activity, _} + assert_receive {:render_with_user, _, _, ^create_activity2, _} assert_receive {:text, received_conversation1} assert %{"event" => "conversation", "payload" => _} = Jason.decode!(received_conversation1) assert_receive {:text, received_conversation1} @@ -977,7 +977,7 @@ test "it sends conversation update to the 'direct' stream when a message is dele receive do {StreamerTest, :ready} -> - assert_receive {:render_with_user, _, "update.json", _} + assert_receive {:render_with_user, _, "update.json", _, _} receive do {StreamerTest, :revoked} -> finalize.() From a348c2e4dd0551a603509501d718d9e0b995be90 Mon Sep 17 00:00:00 2001 From: tusooa Date: Sat, 1 Apr 2023 01:29:11 -0400 Subject: [PATCH 174/199] Use pleroma: instead of pleroma. for ws events --- .../web/mastodon_api/websocket_handler.ex | 8 ++-- lib/pleroma/web/views/streamer_view.ex | 2 +- .../integration/mastodon_websocket_test.exs | 46 ++++++++++--------- test/pleroma/web/streamer_test.exs | 14 +++++- 4 files changed, 42 insertions(+), 28 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex index 2707673ba..07c2b62e3 100644 --- a/lib/pleroma/web/mastodon_api/websocket_handler.ex +++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex @@ -215,7 +215,7 @@ defp handle_client_event(%{"type" => "unsubscribe", "stream" => _topic} = params end defp handle_client_event( - %{"type" => "pleroma.authenticate", "token" => access_token} = _params, + %{"type" => "pleroma:authenticate", "token" => access_token} = _params, state ) do with {:auth, nil, nil} <- {:auth, state.user, state.oauth_token}, @@ -223,7 +223,7 @@ defp handle_client_event( {[ {:text, StreamerView.render("pleroma_respond.json", %{ - type: "pleroma.authenticate", + type: "pleroma:authenticate", result: "success" })} ], %{state | user: user, oauth_token: oauth_token}} @@ -232,7 +232,7 @@ defp handle_client_event( {[ {:text, StreamerView.render("pleroma_respond.json", %{ - type: "pleroma.authenticate", + type: "pleroma:authenticate", result: "error", error: :already_authenticated })} @@ -242,7 +242,7 @@ defp handle_client_event( {[ {:text, StreamerView.render("pleroma_respond.json", %{ - type: "pleroma.authenticate", + type: "pleroma:authenticate", result: "error", error: :unauthorized })} diff --git a/lib/pleroma/web/views/streamer_view.ex b/lib/pleroma/web/views/streamer_view.ex index f591da9a6..f97570b0a 100644 --- a/lib/pleroma/web/views/streamer_view.ex +++ b/lib/pleroma/web/views/streamer_view.ex @@ -148,7 +148,7 @@ def render("conversation.json", %Participation{} = participation, topic) do def render("pleroma_respond.json", %{type: type, result: result} = params) do %{ - event: "pleroma.respond", + event: "pleroma:respond", payload: %{ result: result, diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs index d2e98df3b..21e7ca2b0 100644 --- a/test/pleroma/integration/mastodon_websocket_test.exs +++ b/test/pleroma/integration/mastodon_websocket_test.exs @@ -95,7 +95,7 @@ test "can subscribe" do assert {:ok, %{ - "event" => "pleroma.respond", + "event" => "pleroma:respond", "payload" => %{"type" => "subscribe", "result" => "success"} }} = decode_json(raw_json) @@ -124,16 +124,20 @@ test "can subscribe to multiple streams" do assert {:ok, %{ - "event" => "pleroma.respond", + "event" => "pleroma:respond", "payload" => %{"type" => "subscribe", "result" => "success"} }} = decode_json(raw_json) - WebsocketClient.send_text(pid, %{type: "subscribe", stream: "hashtag", tag: "mew"} |> Jason.encode!()) + WebsocketClient.send_text( + pid, + %{type: "subscribe", stream: "hashtag", tag: "mew"} |> Jason.encode!() + ) + assert_receive {:text, raw_json}, 1_000 assert {:ok, %{ - "event" => "pleroma.respond", + "event" => "pleroma:respond", "payload" => %{"type" => "subscribe", "result" => "success"} }} = decode_json(raw_json) @@ -157,7 +161,7 @@ test "won't double subscribe" do assert {:ok, %{ - "event" => "pleroma.respond", + "event" => "pleroma:respond", "payload" => %{"type" => "subscribe", "result" => "success"} }} = decode_json(raw_json) @@ -166,7 +170,7 @@ test "won't double subscribe" do assert {:ok, %{ - "event" => "pleroma.respond", + "event" => "pleroma:respond", "payload" => %{"type" => "subscribe", "result" => "ignored"} }} = decode_json(raw_json) @@ -184,7 +188,7 @@ test "can unsubscribe" do assert {:ok, %{ - "event" => "pleroma.respond", + "event" => "pleroma:respond", "payload" => %{"type" => "subscribe", "result" => "success"} }} = decode_json(raw_json) @@ -193,7 +197,7 @@ test "can unsubscribe" do assert {:ok, %{ - "event" => "pleroma.respond", + "event" => "pleroma:respond", "payload" => %{"type" => "unsubscribe", "result" => "success"} }} = decode_json(raw_json) @@ -262,15 +266,15 @@ test "accepts valid token on client-sent event", %{token: token} do WebsocketClient.send_text( pid, - %{type: "pleroma.authenticate", token: token.token} |> Jason.encode!() + %{type: "pleroma:authenticate", token: token.token} |> Jason.encode!() ) assert_receive {:text, raw_json}, 1_000 assert {:ok, %{ - "event" => "pleroma.respond", - "payload" => %{"type" => "pleroma.authenticate", "result" => "success"} + "event" => "pleroma:respond", + "payload" => %{"type" => "pleroma:authenticate", "result" => "success"} }} = decode_json(raw_json) WebsocketClient.send_text(pid, %{type: "subscribe", stream: "user"} |> Jason.encode!()) @@ -278,7 +282,7 @@ test "accepts valid token on client-sent event", %{token: token} do assert {:ok, %{ - "event" => "pleroma.respond", + "event" => "pleroma:respond", "payload" => %{"type" => "subscribe", "result" => "success"} }} = decode_json(raw_json) end @@ -288,16 +292,16 @@ test "rejects invalid token on client-sent event" do WebsocketClient.send_text( pid, - %{type: "pleroma.authenticate", token: "Something else"} |> Jason.encode!() + %{type: "pleroma:authenticate", token: "Something else"} |> Jason.encode!() ) assert_receive {:text, raw_json}, 1_000 assert {:ok, %{ - "event" => "pleroma.respond", + "event" => "pleroma:respond", "payload" => %{ - "type" => "pleroma.authenticate", + "type" => "pleroma:authenticate", "result" => "error", "error" => "unauthorized" } @@ -309,29 +313,29 @@ test "rejects new authenticate request if already logged-in", %{token: token} do WebsocketClient.send_text( pid, - %{type: "pleroma.authenticate", token: token.token} |> Jason.encode!() + %{type: "pleroma:authenticate", token: token.token} |> Jason.encode!() ) assert_receive {:text, raw_json}, 1_000 assert {:ok, %{ - "event" => "pleroma.respond", - "payload" => %{"type" => "pleroma.authenticate", "result" => "success"} + "event" => "pleroma:respond", + "payload" => %{"type" => "pleroma:authenticate", "result" => "success"} }} = decode_json(raw_json) WebsocketClient.send_text( pid, - %{type: "pleroma.authenticate", token: "Something else"} |> Jason.encode!() + %{type: "pleroma:authenticate", token: "Something else"} |> Jason.encode!() ) assert_receive {:text, raw_json}, 1_000 assert {:ok, %{ - "event" => "pleroma.respond", + "event" => "pleroma:respond", "payload" => %{ - "type" => "pleroma.authenticate", + "type" => "pleroma:authenticate", "result" => "error", "error" => "already_authenticated" } diff --git a/test/pleroma/web/streamer_test.exs b/test/pleroma/web/streamer_test.exs index cc57a2989..d85358fd4 100644 --- a/test/pleroma/web/streamer_test.exs +++ b/test/pleroma/web/streamer_test.exs @@ -355,7 +355,12 @@ test "it sends chat messages to the 'user:pleroma_chat' stream", %{ Streamer.get_topic_and_add_socket("user:pleroma_chat", user, oauth_token) Streamer.stream("user:pleroma_chat", {user, cm_ref}) - text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}, "user:pleroma_chat:#{user.id}") + text = + StreamerView.render( + "chat_update.json", + %{chat_message_reference: cm_ref}, + "user:pleroma_chat:#{user.id}" + ) assert text =~ "hey cirno" assert_receive {:text, ^text} @@ -373,7 +378,12 @@ test "it sends chat messages to the 'user' stream", %{user: user, token: oauth_t Streamer.get_topic_and_add_socket("user", user, oauth_token) Streamer.stream("user", {user, cm_ref}) - text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}, "user:#{user.id}") + text = + StreamerView.render( + "chat_update.json", + %{chat_message_reference: cm_ref}, + "user:#{user.id}" + ) assert text =~ "hey cirno" assert_receive {:text, ^text} From 2d430679468a4c6a9b5c365a53f007cfa28679d9 Mon Sep 17 00:00:00 2001 From: tusooa Date: Sat, 1 Apr 2023 02:24:30 -0400 Subject: [PATCH 175/199] Document the streaming endpoint --- .../API/differences_in_mastoapi_responses.md | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/docs/development/API/differences_in_mastoapi_responses.md b/docs/development/API/differences_in_mastoapi_responses.md index 4007c63c8..ef6b3b3be 100644 --- a/docs/development/API/differences_in_mastoapi_responses.md +++ b/docs/development/API/differences_in_mastoapi_responses.md @@ -357,6 +357,122 @@ The message payload consist of: - `follower_count`: follower count - `following_count`: following count +### Authenticating via `sec-websocket-protocol` header + +Pleroma allows to authenticate via the `sec-websocket-protocol` header, for example, if your access token is `your-access-token`, you can authenticate using the following: + +``` +sec-websocket-protocol: your-access-token +``` + +### Authenticating after connection via `pleroma:authenticate` event + +Pleroma allows to authenticate after connection is established, via the `pleroma:authenticate` event. For example, if your access token is `your-access-token`, you can send the following after the connection is established: + +``` +{"type": "pleroma:authenticate", "token": "your-access-token"} +``` + +### Response to client-sent events + +Pleroma will respond to client-sent events that it recognizes. Supported event types are: + +- `subscribe` +- `unsubscribe` +- `pleroma:authenticate` + +The reply will be in the following format: + +``` +{ + "event": "pleroma:respond", + "payload": "{\"type\": \"\", \"result\": \"\", \"error\": \"\"}" +} +``` + +Result of the action can be either `success`, `ignored` or `error`. If it is `error`, the `error` property will contain the error code. Otherwise, the `error` property will not be present. Below are some examples: + +``` +{ + "event": "pleroma:respond", + "payload": "{\"type\": \"pleroma:authenticate\", \"result\": \"success\"}" +} + +{ + "event": "pleroma:respond", + "payload": "{\"type\": \"subscribe\", \"result\": \"ignored\"}" +} + +{ + "event": "pleroma:respond", + "payload": "{\"type\": \"unsubscribe\", \"result\": \"error\", \"error\": \"bad_topic\"}" +} +``` + +If the sent event is not of a type that Pleroma supports, it will not reply. + +### The `stream` attribute of a server-sent event + +Technically, this is in Mastodon, but its documentation does nothing to specify its format. + +This attribute appears on every event type except `pleroma:respond` and `delete`. It helps clients determine where they should display the new statuses. + +The value of the attribute is an array containing one or two elements. The first element is the type of the stream. The second is the identifier related to that specific stream, if applicable. + +For the following stream types, there is a second element in the array: + +- `list`: The second element is the id of the list. +- `hashtag`: The second element is the name of the hashtag. +- `public:remote:media` and `public:remote`: The second element is the domain of the corresponding instance. + +For all other stream types, there is no second element. + +Some examples of valid `stream` values: + +- `list:1`: List of id 1. +- `hashtag:mew`: The hashtag #mew. +- `user:notifications`: Notifications for the current user. +- `user`: Home timeline. +- `public:remote:mew.moe`: Public posts from the instance mew.moe . + +### The unified streaming endpoint + +If you do not specify a stream to connect to when requesting `/api/v1/streaming`, you will enter a connection that subscribes to no streams. After the connection is established, you can authenticate and then subscribe to different streams. + +### List of supported streams + +Below is a list of supported streams by Pleroma. To make a single-stream WebSocket connection, append the string specified in "Query style" to the streaming endpoint url. +To subscribe to a stream after the connection is established, merge the JSON object specified in "Subscribe style" with `{"type": "subscribe"}`. To unsubscribe, merge it with `{"type": "unsubscribe"}`. + +For example, to receive updates on the list 1, you can connect to `/api/v1/streaming/?stream=list&list=1`, or send + +``` +{"type": "subscribe", "stream": "list", "list": 1} +``` + +upon establishing the websocket connection. + +To unsubscribe to list 1, send + +``` +{"type": "unsubscribe", "stream": "list", "list": 1} +``` + +Note that if you specify a stream that requires a logged-in user in the query string (for example, `user` or `list`), you have to specify the access token when you are trying to establish the connection, i.e. in the query string or via the `sec-websocket-protocol` header. + +- `list` + - Query style: `?stream=list&list=` + - Subscribe style: `{"stream": "list", "list": }` +- `public`, `public:local`, `public:media`, `public:local:media`, `user`, `user:pleroma_chat`, `user:notifications`, `direct` + - Query style: `?stream=` + - Subscribe style: `{"stream": ""}` +- `hashtag` + - Query style: `?stream=hashtag&tag=` + - Subscribe style: `{"stream": "hashtag", "tag": ""}` +- `public:remote`, `public:remote:media` + - Query style: `?stream=&instance=` + - Subscribe style: `{"stream": "", "instance": ""}` + ## User muting and thread muting Both user muting and thread muting can be done for only a certain time by adding an `expires_in` parameter to the API calls and giving the expiration time in seconds. From 9572be1e5fe0201cd069271c7025ef233e8ddf00 Mon Sep 17 00:00:00 2001 From: tusooa Date: Sat, 1 Apr 2023 02:35:12 -0400 Subject: [PATCH 176/199] Add tests for list streams --- .../integration/mastodon_websocket_test.exs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs index 21e7ca2b0..8129645ec 100644 --- a/test/pleroma/integration/mastodon_websocket_test.exs +++ b/test/pleroma/integration/mastodon_websocket_test.exs @@ -342,6 +342,43 @@ test "rejects new authenticate request if already logged-in", %{token: token} do }} = decode_json(raw_json) end + test "accepts the 'list' stream", %{token: token, user: user} do + posting_user = insert(:user) + + {:ok, list} = Pleroma.List.create("test", user) + Pleroma.List.follow(list, posting_user) + + assert {:ok, _} = start_socket("?stream=list&access_token=#{token.token}&list=#{list.id}") + + assert {:ok, pid} = start_socket("?access_token=#{token.token}") + + WebsocketClient.send_text( + pid, + %{type: "subscribe", stream: "list", list: list.id} |> Jason.encode!() + ) + + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "pleroma:respond", + "payload" => %{"type" => "subscribe", "result" => "success"} + }} = decode_json(raw_json) + + WebsocketClient.send_text( + pid, + %{type: "subscribe", stream: "list", list: to_string(list.id)} |> Jason.encode!() + ) + + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "pleroma:respond", + "payload" => %{"type" => "subscribe", "result" => "ignored"} + }} = decode_json(raw_json) + end + test "disconnect when token is revoked", %{app: app, user: user, token: token} do assert {:ok, _} = start_socket("?stream=user:notification&access_token=#{token.token}") assert {:ok, _} = start_socket("?stream=user&access_token=#{token.token}") From 7f12ad6dccfe4c81fa7e0d4e66c43bedadbf4c6a Mon Sep 17 00:00:00 2001 From: tusooa Date: Sat, 1 Apr 2023 02:37:19 -0400 Subject: [PATCH 177/199] Fix docs wording --- .../API/differences_in_mastoapi_responses.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/development/API/differences_in_mastoapi_responses.md b/docs/development/API/differences_in_mastoapi_responses.md index ef6b3b3be..48a9c104c 100644 --- a/docs/development/API/differences_in_mastoapi_responses.md +++ b/docs/development/API/differences_in_mastoapi_responses.md @@ -421,7 +421,7 @@ The value of the attribute is an array containing one or two elements. The first For the following stream types, there is a second element in the array: -- `list`: The second element is the id of the list. +- `list`: The second element is the id of the list, as a string. - `hashtag`: The second element is the name of the hashtag. - `public:remote:media` and `public:remote`: The second element is the domain of the corresponding instance. @@ -429,11 +429,11 @@ For all other stream types, there is no second element. Some examples of valid `stream` values: -- `list:1`: List of id 1. -- `hashtag:mew`: The hashtag #mew. -- `user:notifications`: Notifications for the current user. -- `user`: Home timeline. -- `public:remote:mew.moe`: Public posts from the instance mew.moe . +- `["list", "1"]`: List of id 1. +- `["hashtag", "mew"]`: The hashtag #mew. +- `["user:notifications"]`: Notifications for the current user. +- `["user"]`: Home timeline. +- `["public:remote", "mew.moe"]`: Public posts from the instance mew.moe . ### The unified streaming endpoint @@ -447,7 +447,7 @@ To subscribe to a stream after the connection is established, merge the JSON obj For example, to receive updates on the list 1, you can connect to `/api/v1/streaming/?stream=list&list=1`, or send ``` -{"type": "subscribe", "stream": "list", "list": 1} +{"type": "subscribe", "stream": "list", "list": "1"} ``` upon establishing the websocket connection. @@ -455,14 +455,14 @@ upon establishing the websocket connection. To unsubscribe to list 1, send ``` -{"type": "unsubscribe", "stream": "list", "list": 1} +{"type": "unsubscribe", "stream": "list", "list": "1"} ``` Note that if you specify a stream that requires a logged-in user in the query string (for example, `user` or `list`), you have to specify the access token when you are trying to establish the connection, i.e. in the query string or via the `sec-websocket-protocol` header. - `list` - Query style: `?stream=list&list=` - - Subscribe style: `{"stream": "list", "list": }` + - Subscribe style: `{"stream": "list", "list": ""}` - `public`, `public:local`, `public:media`, `public:local:media`, `user`, `user:pleroma_chat`, `user:notifications`, `direct` - Query style: `?stream=` - Subscribe style: `{"stream": ""}` From 949c4f01c6d5686ff1852e6439616c2069557ef0 Mon Sep 17 00:00:00 2001 From: tusooa Date: Sat, 1 Apr 2023 02:38:16 -0400 Subject: [PATCH 178/199] Fix NotificationTest --- test/pleroma/notification_test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/pleroma/notification_test.exs b/test/pleroma/notification_test.exs index e55aa3a08..71af9acb8 100644 --- a/test/pleroma/notification_test.exs +++ b/test/pleroma/notification_test.exs @@ -252,7 +252,7 @@ test "it creates a notification for user and send to the 'user' and the 'user:no task = Task.async(fn -> {:ok, _topic} = Streamer.get_topic_and_add_socket("user", user, oauth_token) - assert_receive {:render_with_user, _, _, _}, 4_000 + assert_receive {:render_with_user, _, _, _, _}, 4_000 end) task_user_notification = @@ -260,7 +260,7 @@ test "it creates a notification for user and send to the 'user' and the 'user:no {:ok, _topic} = Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) - assert_receive {:render_with_user, _, _, _}, 4_000 + assert_receive {:render_with_user, _, _, _, _}, 4_000 end) activity = insert(:note_activity) From eebc605bc25deead55c305d703c06ddb9d9b1107 Mon Sep 17 00:00:00 2001 From: tusooa Date: Sat, 1 Apr 2023 08:27:43 -0400 Subject: [PATCH 179/199] Clear up debug statement --- lib/pleroma/web/streamer.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex index 70e46617a..48ca82421 100644 --- a/lib/pleroma/web/streamer.ex +++ b/lib/pleroma/web/streamer.ex @@ -357,7 +357,6 @@ defp push_to_socket(topic, %Activity{data: %{"type" => "Update"}} = item) do end defp push_to_socket(topic, item) do - Logger.debug("topic=#{topic}") anon_render = StreamerView.render("update.json", item, topic) Registry.dispatch(@registry, topic, fn list -> From 050227f11898f402f5888d53e6460b704bcd0a8b Mon Sep 17 00:00:00 2001 From: tusooa Date: Sat, 1 Apr 2023 08:39:38 -0400 Subject: [PATCH 180/199] Add test to cover error: bad_topic --- test/pleroma/integration/mastodon_websocket_test.exs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs index 8129645ec..0e8413749 100644 --- a/test/pleroma/integration/mastodon_websocket_test.exs +++ b/test/pleroma/integration/mastodon_websocket_test.exs @@ -180,6 +180,18 @@ test "won't double subscribe" do refute_receive {:text, _}, 1_000 end + test "rejects invalid streams" do + {:ok, pid} = start_socket() + WebsocketClient.send_text(pid, %{type: "subscribe", stream: "nonsense"} |> Jason.encode!()) + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "pleroma:respond", + "payload" => %{"type" => "subscribe", "result" => "error", "error" => "bad_topic"} + }} = decode_json(raw_json) + end + test "can unsubscribe" do user = insert(:user) {:ok, pid} = start_socket() From 4cf109d3c4093d788e6b2de229a9e4034146a5be Mon Sep 17 00:00:00 2001 From: tusooa Date: Sat, 1 Apr 2023 08:47:46 -0400 Subject: [PATCH 181/199] Add test to cover rendering update with user --- .../integration/mastodon_websocket_test.exs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs index 0e8413749..5ec681ce3 100644 --- a/test/pleroma/integration/mastodon_websocket_test.exs +++ b/test/pleroma/integration/mastodon_websocket_test.exs @@ -406,5 +406,32 @@ test "disconnect when token is revoked", %{app: app, user: user, token: token} d assert_receive {:close, _} refute_receive {:close, _} end + + test "receives private statuses", %{user: reading_user, token: token} do + user = insert(:user) + CommonAPI.follow(reading_user, user) + + {:ok, _} = start_socket("?stream=user&access_token=#{token.token}") + + {:ok, activity} = + CommonAPI.post(user, %{status: "nice echo chamber", visibility: "private"}) + + assert_receive {:text, raw_json}, 1_000 + assert {:ok, json} = Jason.decode(raw_json) + + assert "update" == json["event"] + assert json["payload"] + assert {:ok, json} = Jason.decode(json["payload"]) + + view_json = + Pleroma.Web.MastodonAPI.StatusView.render("show.json", + activity: activity, + for: reading_user + ) + |> Jason.encode!() + |> Jason.decode!() + + assert json == view_json + end end end From 26f5caebae1581f1dcdc6d1352840d27009e061c Mon Sep 17 00:00:00 2001 From: tusooa Date: Sat, 1 Apr 2023 08:56:53 -0400 Subject: [PATCH 182/199] Add test to cover notifications streaming --- .../integration/mastodon_websocket_test.exs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs index 5ec681ce3..96648264e 100644 --- a/test/pleroma/integration/mastodon_websocket_test.exs +++ b/test/pleroma/integration/mastodon_websocket_test.exs @@ -433,5 +433,30 @@ test "receives private statuses", %{user: reading_user, token: token} do assert json == view_json end + + test "receives notifications", %{user: reading_user, token: token} do + user = insert(:user) + CommonAPI.follow(reading_user, user) + + {:ok, _} = start_socket("?stream=user:notification&access_token=#{token.token}") + + {:ok, %Pleroma.Activity{id: activity_id} = _activity} = + CommonAPI.post(user, %{ + status: "nice echo chamber @#{reading_user.nickname}", + visibility: "private" + }) + + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "notification", + "payload" => %{ + "status" => %{ + "id" => ^activity_id + } + } + }} = decode_json(raw_json) + end end end From 314360e5e3a4bdd13f152e94050cb8562e2ea0ed Mon Sep 17 00:00:00 2001 From: tusooa Date: Sat, 1 Apr 2023 09:02:43 -0400 Subject: [PATCH 183/199] Add test to cover edit streaming --- .../integration/mastodon_websocket_test.exs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs index 96648264e..7d5dfc255 100644 --- a/test/pleroma/integration/mastodon_websocket_test.exs +++ b/test/pleroma/integration/mastodon_websocket_test.exs @@ -434,6 +434,34 @@ test "receives private statuses", %{user: reading_user, token: token} do assert json == view_json end + test "receives edits", %{user: reading_user, token: token} do + user = insert(:user) + CommonAPI.follow(reading_user, user) + + {:ok, _} = start_socket("?stream=user&access_token=#{token.token}") + + {:ok, activity} = + CommonAPI.post(user, %{status: "nice echo chamber", visibility: "private"}) + + assert_receive {:text, _raw_json}, 1_000 + + {:ok, _} = CommonAPI.update(user, activity, %{status: "mew mew", visibility: "private"}) + + assert_receive {:text, raw_json}, 1_000 + + activity = Pleroma.Activity.normalize(activity) + + view_json = + Pleroma.Web.MastodonAPI.StatusView.render("show.json", + activity: activity, + for: reading_user + ) + |> Jason.encode!() + |> Jason.decode!() + + assert {:ok, %{"event" => "status.update", "payload" => ^view_json}} = decode_json(raw_json) + end + test "receives notifications", %{user: reading_user, token: token} do user = insert(:user) CommonAPI.follow(reading_user, user) From 844d1a14e0b4aabbb61a6693fa6dd3a0aa0dbc5b Mon Sep 17 00:00:00 2001 From: tusooa Date: Sat, 1 Apr 2023 15:31:56 -0400 Subject: [PATCH 184/199] Start writing api docs for streaming endpoint --- lib/pleroma/web/api_spec.ex | 10 +- .../operations/streaming_operation.ex | 186 ++++++++++++++++++ 2 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 lib/pleroma/web/api_spec/operations/streaming_operation.ex diff --git a/lib/pleroma/web/api_spec.ex b/lib/pleroma/web/api_spec.ex index 2d56dc643..163226ce5 100644 --- a/lib/pleroma/web/api_spec.ex +++ b/lib/pleroma/web/api_spec.ex @@ -10,6 +10,14 @@ defmodule Pleroma.Web.ApiSpec do @behaviour OpenApi + defp streaming_paths do + %{ + "/api/v1/streaming" => %OpenApiSpex.PathItem{ + get: Pleroma.Web.ApiSpec.StreamingOperation.streaming_operation() + } + } + end + @impl OpenApi def spec(opts \\ []) do %OpenApi{ @@ -45,7 +53,7 @@ def spec(opts \\ []) do } }, # populate the paths from a phoenix router - paths: OpenApiSpex.Paths.from_router(Router), + paths: Map.merge(streaming_paths(), OpenApiSpex.Paths.from_router(Router)), components: %OpenApiSpex.Components{ parameters: %{ "accountIdOrNickname" => diff --git a/lib/pleroma/web/api_spec/operations/streaming_operation.ex b/lib/pleroma/web/api_spec/operations/streaming_operation.ex new file mode 100644 index 000000000..1ef7d72ef --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/streaming_operation.ex @@ -0,0 +1,186 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.StreamingOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Response + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Helpers + alias Pleroma.Web.ApiSpec.Schemas.Status + + @spec open_api_operation(atom) :: Operation.t() + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + @spec streaming_operation() :: Operation.t() + def streaming_operation do + %Operation{ + tags: ["Timelines"], + summary: "Establish streaming connection", + description: "Receive statuses in real-time via WebSocket.", + security: [%{"oAuth" => ["read:statuses", "read:notifications"]}], + parameters: [ + Operation.parameter(:connection, :header, %Schema{type: :string}, "connection header", + required: true + ), + Operation.parameter(:upgrade, :header, %Schema{type: :string}, "upgrade header", + required: true + ), + Operation.parameter( + :"sec-websocket-key", + :header, + %Schema{type: :string}, + "sec-websocket-key header", + required: true + ), + Operation.parameter( + :"sec-websocket-version", + :header, + %Schema{type: :string}, + "sec-websocket-version header", + required: true + ) + ], + responses: %{ + 101 => switching_protocols_response(), + 200 => + Operation.response( + "Server-sent events", + "application/json", + server_sent_events() + ) + } + } + end + + defp switching_protocols_response do + %Response{ + description: "Switching protocols", + headers: %{ + "connection" => %OpenApiSpex.Header{required: true}, + "upgrade" => %OpenApiSpex.Header{required: true}, + "sec-websocket-accept" => %OpenApiSpex.Header{required: true} + } + } + end + + defp server_sent_events do + %Schema{ + oneOf: [ + update_event(), + status_update_event(), + pleroma_respond_event() + ] + } + end + + defp stream do + %Schema{ + type: :array, + title: "Stream", + description: """ + The stream identifier. + The first item is the name of the stream. If the stream needs a differentiator, the second item will be the corresponding identifier. + Currently, for the following stream types, there is a second element in the array: + + - `list`: The second element is the id of the list, as a string. + - `hashtag`: The second element is the name of the hashtag. + - `public:remote:media` and `public:remote`: The second element is the domain of the corresponding instance. + """, + maxItems: 2, + minItems: 1, + items: %Schema{type: :string}, + example: ["hashtag", "mew"] + } + end + + defp get_schema(%Schema{} = schema), do: schema + defp get_schema(schema), do: schema.schema + + defp server_sent_event_helper(name, description, type, payload, opts \\ []) do + payload_type = opts[:payload_type] || :json + has_stream = opts[:has_stream] || true + + stream_properties = + if has_stream do + %{stream: stream()} + else + %{} + end + + stream_example = if has_stream, do: %{"stream" => get_schema(stream()).example}, else: %{} + + stream_required = if has_stream, do: [:stream], else: [] + + %Schema{ + type: :object, + title: name, + description: description, + required: [:event, :payload] ++ stream_required, + properties: + %{ + event: %Schema{ + title: "Event type", + description: "Type of the event.", + type: :string, + required: true, + enum: [type] + }, + payload: + if payload_type == :json do + %Schema{ + title: "Event payload", + description: "JSON-encoded string of #{get_schema(payload).title}", + allOf: [payload] + } + else + payload + end + } + |> Map.merge(stream_properties), + example: + %{ + "event" => type, + "payload" => get_schema(payload).example |> Jason.encode!() + } + |> Map.merge(stream_example) + } + end + + defp update_event do + server_sent_event_helper("New status", "A newly-posted status.", "update", Status) + end + + defp status_update_event do + server_sent_event_helper("Edit", "A status that was just edited", "status.update", Status) + end + + defp pleroma_respond_event do + server_sent_event_helper( + "Server response", + "A response to a client-sent event.", + "pleroma:respond", + %Schema{ + type: :object, + title: "Results", + required: [:result], + properties: %{ + result: %Schema{ + type: :string, + title: "Result of the request", + enum: ["success", "error", "ignored"] + }, + error: %Schema{ + type: :string, + title: "Error code", + description: "An error identifier. Only appears if `result` is `error`." + } + }, + example: %{"result" => "success"} + } + ) + end +end From dcef33f5f0c93883a634d12dc662b83d7ef6abfa Mon Sep 17 00:00:00 2001 From: tusooa Date: Sat, 1 Apr 2023 16:07:27 -0400 Subject: [PATCH 185/199] Document server-sent events of streaming --- .../operations/streaming_operation.ex | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/lib/pleroma/web/api_spec/operations/streaming_operation.ex b/lib/pleroma/web/api_spec/operations/streaming_operation.ex index 1ef7d72ef..cdef41603 100644 --- a/lib/pleroma/web/api_spec/operations/streaming_operation.ex +++ b/lib/pleroma/web/api_spec/operations/streaming_operation.ex @@ -7,6 +7,10 @@ defmodule Pleroma.Web.ApiSpec.StreamingOperation do alias OpenApiSpex.Response alias OpenApiSpex.Schema alias Pleroma.Web.ApiSpec.Helpers + alias Pleroma.Web.ApiSpec.NotificationOperation + alias Pleroma.Web.ApiSpec.Schemas.Chat + alias Pleroma.Web.ApiSpec.Schemas.Conversation + alias Pleroma.Web.ApiSpec.Schemas.FlakeID alias Pleroma.Web.ApiSpec.Schemas.Status @spec open_api_operation(atom) :: Operation.t() @@ -22,6 +26,7 @@ def streaming_operation do summary: "Establish streaming connection", description: "Receive statuses in real-time via WebSocket.", security: [%{"oAuth" => ["read:statuses", "read:notifications"]}], + operationId: "WebsocketHandler.streaming", parameters: [ Operation.parameter(:connection, :header, %Schema{type: :string}, "connection header", required: true @@ -72,6 +77,11 @@ defp server_sent_events do oneOf: [ update_event(), status_update_event(), + notification_event(), + chat_update_event(), + follow_relationships_update_event(), + conversation_event(), + delete_event(), pleroma_respond_event() ] } @@ -158,6 +168,102 @@ defp status_update_event do server_sent_event_helper("Edit", "A status that was just edited", "status.update", Status) end + defp notification_event do + server_sent_event_helper( + "Notification", + "A new notification.", + "notification", + NotificationOperation.notification() + ) + end + + defp follow_relationships_update_event do + server_sent_event_helper( + "Follow relationships update", + "An update to follow relationships.", + "pleroma:follow_relationships_update", + %Schema{ + type: :object, + title: "Follow relationships update", + required: [:state, :follower, :following], + properties: %{ + state: %Schema{ + type: :string, + description: "Follow state of the relationship.", + enum: ["follow_pending", "follow_accept", "follow_reject", "unfollow"] + }, + follower: %Schema{ + type: :object, + description: "Information about the follower.", + required: [:id, :follower_count, :following_count], + properties: %{ + id: FlakeID, + follower_count: %Schema{type: :integer}, + following_count: %Schema{type: :integer} + } + }, + following: %Schema{ + type: :object, + description: "Information about the following person.", + required: [:id, :follower_count, :following_count], + properties: %{ + id: FlakeID, + follower_count: %Schema{type: :integer}, + following_count: %Schema{type: :integer} + } + } + }, + example: %{ + "state" => "follow_pending", + "follower" => %{ + "id" => "someUser1", + "follower_count" => 1, + "following_count" => 1 + }, + "following" => %{ + "id" => "someUser2", + "follower_count" => 1, + "following_count" => 1 + } + } + } + ) + end + + defp chat_update_event do + server_sent_event_helper( + "Chat update", + "A new chat message.", + "pleroma:chat_update", + Chat + ) + end + + defp conversation_event do + server_sent_event_helper( + "Conversation", + "An update about a conversation", + "conversation", + Conversation + ) + end + + defp delete_event do + server_sent_event_helper( + "Delete", + "A status that was just deleted.", + "delete", + %Schema{ + type: :string, + title: "Status id", + description: "Id of the deleted status", + allOf: [FlakeID], + example: "some-opaque-id" + }, + payload_type: :string + ) + end + defp pleroma_respond_event do server_sent_event_helper( "Server response", From 8829dcaee42b3ad1ee50f95b0586b22118771785 Mon Sep 17 00:00:00 2001 From: tusooa Date: Sat, 1 Apr 2023 16:33:22 -0400 Subject: [PATCH 186/199] Document client-sent events in streaming --- .../operations/streaming_operation.ex | 126 +++++++++++++++++- 1 file changed, 125 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/api_spec/operations/streaming_operation.ex b/lib/pleroma/web/api_spec/operations/streaming_operation.ex index cdef41603..18674c9a1 100644 --- a/lib/pleroma/web/api_spec/operations/streaming_operation.ex +++ b/lib/pleroma/web/api_spec/operations/streaming_operation.ex @@ -6,13 +6,14 @@ defmodule Pleroma.Web.ApiSpec.StreamingOperation do alias OpenApiSpex.Operation alias OpenApiSpex.Response alias OpenApiSpex.Schema - alias Pleroma.Web.ApiSpec.Helpers alias Pleroma.Web.ApiSpec.NotificationOperation alias Pleroma.Web.ApiSpec.Schemas.Chat alias Pleroma.Web.ApiSpec.Schemas.Conversation alias Pleroma.Web.ApiSpec.Schemas.FlakeID alias Pleroma.Web.ApiSpec.Schemas.Status + require Pleroma.Constants + @spec open_api_operation(atom) :: Operation.t() def open_api_operation(action) do operation = String.to_existing_atom("#{action}_operation") @@ -49,6 +50,7 @@ def streaming_operation do required: true ) ], + requestBody: request_body("Client-sent events", client_sent_events()), responses: %{ 101 => switching_protocols_response(), 200 => @@ -289,4 +291,126 @@ defp pleroma_respond_event do } ) end + + defp client_sent_events do + %Schema{ + oneOf: [ + subscribe_event(), + unsubscribe_event(), + authenticate_event() + ] + } + end + + defp request_body(description, schema, opts \\ []) do + %OpenApiSpex.RequestBody{ + description: description, + content: %{ + "application/json" => %OpenApiSpex.MediaType{ + schema: schema, + example: opts[:example], + examples: opts[:examples] + } + } + } + end + + defp client_sent_event_helper(name, description, type, properties, opts) do + required = opts[:required] || [] + + %Schema{ + type: :object, + title: name, + required: [:type] ++ required, + description: description, + properties: + %{ + type: %Schema{type: :string, enum: [type], description: "Type of the event."} + } + |> Map.merge(properties), + example: opts[:example] + } + end + + defp subscribe_event do + client_sent_event_helper( + "Subscribe", + "Subscribe to a stream.", + "subscribe", + stream_specifier(), + required: [:stream], + example: %{"type" => "subscribe", "stream" => "list", "list" => "1"} + ) + end + + defp unsubscribe_event do + client_sent_event_helper( + "Unsubscribe", + "Unsubscribe from a stream.", + "subscribe", + stream_specifier(), + required: [:stream], + example: %{ + "type" => "unsubscribe", + "stream" => "public:remote:media", + "instance" => "example.org" + } + ) + end + + defp authenticate_event do + client_sent_event_helper( + "Authenticate", + "Authenticate via an access token.", + "pleroma:authenticate", + %{ + token: %Schema{ + type: :string, + description: "An OAuth access token with corresponding permissions.", + example: "some token" + } + }, + required: [:token] + ) + end + + defp stream_specifier do + %{ + stream: %Schema{ + type: :string, + description: "The name of the stream.", + enum: + Pleroma.Constants.public_streams() ++ + [ + "public:remote", + "public:remote:media", + "user", + "user:pleroma_chat", + "user:notification", + "direct", + "list", + "hashtag" + ] + }, + list: %Schema{ + type: :string, + title: "List id", + description: "The id of the list. Required when `stream` is `list`.", + example: "some-id" + }, + tag: %Schema{ + type: :string, + title: "Hashtag name", + description: "The name of the hashtag. Required when `stream` is `hashtag`.", + example: "mew" + }, + instance: %Schema{ + type: :string, + title: "Domain name", + description: + "Domain name of the instance. Required when `stream` is `public:remote` or `public:remote:media`.", + example: "example.org" + } + } + end end From f393a15dd1217a7f6aec9e9acc7b983e7b165a91 Mon Sep 17 00:00:00 2001 From: tusooa Date: Sat, 1 Apr 2023 16:46:32 -0400 Subject: [PATCH 187/199] Fix some specs about server-sent events in streaming --- .../operations/streaming_operation.ex | 49 ++++++++++++------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/streaming_operation.ex b/lib/pleroma/web/api_spec/operations/streaming_operation.ex index 18674c9a1..fa48b9613 100644 --- a/lib/pleroma/web/api_spec/operations/streaming_operation.ex +++ b/lib/pleroma/web/api_spec/operations/streaming_operation.ex @@ -113,8 +113,8 @@ defp get_schema(%Schema{} = schema), do: schema defp get_schema(schema), do: schema.schema defp server_sent_event_helper(name, description, type, payload, opts \\ []) do - payload_type = opts[:payload_type] || :json - has_stream = opts[:has_stream] || true + payload_type = Keyword.get(opts, :payload_type, :json) + has_stream = Keyword.get(opts, :has_stream, true) stream_properties = if has_stream do @@ -127,6 +127,24 @@ defp server_sent_event_helper(name, description, type, payload, opts \\ []) do stream_required = if has_stream, do: [:stream], else: [] + payload_schema = + if payload_type == :json do + %Schema{ + title: "Event payload", + description: "JSON-encoded string of #{get_schema(payload).title}", + allOf: [payload] + } + else + payload + end + + payload_example = + if payload_type == :json do + get_schema(payload).example |> Jason.encode!() + else + get_schema(payload).example + end + %Schema{ type: :object, title: name, @@ -141,22 +159,13 @@ defp server_sent_event_helper(name, description, type, payload, opts \\ []) do required: true, enum: [type] }, - payload: - if payload_type == :json do - %Schema{ - title: "Event payload", - description: "JSON-encoded string of #{get_schema(payload).title}", - allOf: [payload] - } - else - payload - end + payload: payload_schema } |> Map.merge(stream_properties), example: %{ "event" => type, - "payload" => get_schema(payload).example |> Jason.encode!() + "payload" => payload_example } |> Map.merge(stream_example) } @@ -262,7 +271,8 @@ defp delete_event do allOf: [FlakeID], example: "some-opaque-id" }, - payload_type: :string + payload_type: :string, + has_stream: false ) end @@ -274,7 +284,7 @@ defp pleroma_respond_event do %Schema{ type: :object, title: "Results", - required: [:result], + required: [:result, :type], properties: %{ result: %Schema{ type: :string, @@ -285,10 +295,15 @@ defp pleroma_respond_event do type: :string, title: "Error code", description: "An error identifier. Only appears if `result` is `error`." + }, + type: %Schema{ + type: :string, + description: "Type of the request." } }, - example: %{"result" => "success"} - } + example: %{"result" => "success", "type" => "pleroma:authenticate"} + }, + has_stream: false ) end From c13f0a8460853d8fe9aa04cb5d57ea06b692a3bf Mon Sep 17 00:00:00 2001 From: tusooa Date: Sat, 1 Apr 2023 17:00:35 -0400 Subject: [PATCH 188/199] Add meta-info and query strings to streaming doc --- .../operations/streaming_operation.ex | 89 +++++++++++++------ 1 file changed, 61 insertions(+), 28 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/streaming_operation.ex b/lib/pleroma/web/api_spec/operations/streaming_operation.ex index fa48b9613..4c4888d8e 100644 --- a/lib/pleroma/web/api_spec/operations/streaming_operation.ex +++ b/lib/pleroma/web/api_spec/operations/streaming_operation.ex @@ -25,31 +25,46 @@ def streaming_operation do %Operation{ tags: ["Timelines"], summary: "Establish streaming connection", - description: "Receive statuses in real-time via WebSocket.", + description: """ + Receive statuses in real-time via WebSocket. + + You can specify the access token on the query string or through the `sec-websocket-protocol` header. Using + the query string to authenticate is considered unsafe and should not be used unless you have to (e.g. to maintain + your client's compatibility with Mastodon). + + You may specify a stream on the query string. If you do so and you are connecting to a stream that requires logged-in users, + you must specify the access token at the time of the connection (i.e. via query string or header). + + Otherwise, you have the option to authenticate after you have established the connection through client-sent events. + + The "Request body" section below describes what events clients can send through WebSocket, and the "Responses" section + describes what events server will send through WebSocket. + """, security: [%{"oAuth" => ["read:statuses", "read:notifications"]}], operationId: "WebsocketHandler.streaming", - parameters: [ - Operation.parameter(:connection, :header, %Schema{type: :string}, "connection header", - required: true - ), - Operation.parameter(:upgrade, :header, %Schema{type: :string}, "upgrade header", - required: true - ), - Operation.parameter( - :"sec-websocket-key", - :header, - %Schema{type: :string}, - "sec-websocket-key header", - required: true - ), - Operation.parameter( - :"sec-websocket-version", - :header, - %Schema{type: :string}, - "sec-websocket-version header", - required: true - ) - ], + parameters: + [ + Operation.parameter(:connection, :header, %Schema{type: :string}, "connection header", + required: true + ), + Operation.parameter(:upgrade, :header, %Schema{type: :string}, "upgrade header", + required: true + ), + Operation.parameter( + :"sec-websocket-key", + :header, + %Schema{type: :string}, + "sec-websocket-key header", + required: true + ), + Operation.parameter( + :"sec-websocket-version", + :header, + %Schema{type: :string}, + "sec-websocket-version header", + required: true + ) + ] ++ stream_params() ++ access_token_params(), requestBody: request_body("Client-sent events", client_sent_events()), responses: %{ 101 => switching_protocols_response(), @@ -63,6 +78,20 @@ def streaming_operation do } end + defp stream_params do + stream_specifier() + |> Enum.map(fn {name, schema} -> + Operation.parameter(name, :query, schema, get_schema(schema).description) + end) + end + + defp access_token_params do + [ + Operation.parameter(:access_token, :query, token(), token().description), + Operation.parameter(:"sec-websocket-protocol", :header, token(), token().description) + ] + end + defp switching_protocols_response do %Response{ description: "Switching protocols", @@ -379,16 +408,20 @@ defp authenticate_event do "Authenticate via an access token.", "pleroma:authenticate", %{ - token: %Schema{ - type: :string, - description: "An OAuth access token with corresponding permissions.", - example: "some token" - } + token: token() }, required: [:token] ) end + defp token do + %Schema{ + type: :string, + description: "An OAuth access token with corresponding permissions.", + example: "some token" + } + end + defp stream_specifier do %{ stream: %Schema{ From 32de0683c4069f5877722dce0b4f5a9a6825b7c7 Mon Sep 17 00:00:00 2001 From: tusooa Date: Sat, 1 Apr 2023 17:15:58 -0400 Subject: [PATCH 189/199] Fix unsubscribe event type in streaming doc --- lib/pleroma/web/api_spec/operations/streaming_operation.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/api_spec/operations/streaming_operation.ex b/lib/pleroma/web/api_spec/operations/streaming_operation.ex index 4c4888d8e..ae3aeb4ab 100644 --- a/lib/pleroma/web/api_spec/operations/streaming_operation.ex +++ b/lib/pleroma/web/api_spec/operations/streaming_operation.ex @@ -391,7 +391,7 @@ defp unsubscribe_event do client_sent_event_helper( "Unsubscribe", "Unsubscribe from a stream.", - "subscribe", + "unsubscribe", stream_specifier(), required: [:stream], example: %{ From 840dd01035581a37613f695facdd99fbf6ac8319 Mon Sep 17 00:00:00 2001 From: tusooa Date: Sat, 1 Apr 2023 18:33:43 -0400 Subject: [PATCH 190/199] Fix duplicated schema names --- lib/pleroma/web/api_spec/operations/streaming_operation.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/api_spec/operations/streaming_operation.ex b/lib/pleroma/web/api_spec/operations/streaming_operation.ex index ae3aeb4ab..b580bc2f0 100644 --- a/lib/pleroma/web/api_spec/operations/streaming_operation.ex +++ b/lib/pleroma/web/api_spec/operations/streaming_operation.ex @@ -281,7 +281,7 @@ defp chat_update_event do defp conversation_event do server_sent_event_helper( - "Conversation", + "Conversation update", "An update about a conversation", "conversation", Conversation From 3e7d2e29b369535a9a942a4090cde9a21892f8c1 Mon Sep 17 00:00:00 2001 From: tusooa Date: Sat, 1 Jul 2023 23:07:07 -0400 Subject: [PATCH 191/199] Add changelog --- changelog.d/unified-streaming.add | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/unified-streaming.add diff --git a/changelog.d/unified-streaming.add b/changelog.d/unified-streaming.add new file mode 100644 index 000000000..84821fcc8 --- /dev/null +++ b/changelog.d/unified-streaming.add @@ -0,0 +1 @@ +Add unified streaming endpoint From eb33a03d0ad8571e3f0f3e0c5e9af39158c72a09 Mon Sep 17 00:00:00 2001 From: tusooa Date: Tue, 18 Jul 2023 18:13:49 -0400 Subject: [PATCH 192/199] Explain the encode-decode roundtrip --- test/pleroma/integration/mastodon_websocket_test.exs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs index 7d5dfc255..a2c20f0a6 100644 --- a/test/pleroma/integration/mastodon_websocket_test.exs +++ b/test/pleroma/integration/mastodon_websocket_test.exs @@ -38,6 +38,13 @@ defp decode_json(json) do end end + # Turns atom keys to strings + defp atom_key_to_string(json) do + json + |> Jason.encode!() + |> Jason.decode!() + end + test "refuses invalid requests" do capture_log(fn -> assert {:error, %WebSockex.RequestError{code: 404}} = start_socket("?stream=ncjdk") @@ -80,8 +87,7 @@ test "receives well formatted events" do view_json = Pleroma.Web.MastodonAPI.StatusView.render("show.json", activity: activity, for: nil) - |> Jason.encode!() - |> Jason.decode!() + |> atom_key_to_string() assert json == view_json end From b748efe66a099b66300f2beda42f5639911bab4a Mon Sep 17 00:00:00 2001 From: tusooa Date: Sat, 29 Jul 2023 12:55:43 -0400 Subject: [PATCH 193/199] Fix mentioning punycode domains when using Markdown --- changelog.d/punycode-mention.fix | 1 + lib/pleroma/formatter.ex | 2 +- test/pleroma/web/common_api_test.exs | 12 ++++++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 changelog.d/punycode-mention.fix diff --git a/changelog.d/punycode-mention.fix b/changelog.d/punycode-mention.fix new file mode 100644 index 000000000..f013c2dac --- /dev/null +++ b/changelog.d/punycode-mention.fix @@ -0,0 +1 @@ +Fix mentioning punycode domains when using Markdown diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex index a46c3e381..11d5af2fb 100644 --- a/lib/pleroma/formatter.ex +++ b/lib/pleroma/formatter.ex @@ -124,7 +124,7 @@ def mentions_escape(text, options \\ []) do end def markdown_to_html(text) do - Earmark.as_html!(text, %Earmark.Options{compact_output: true}) + Earmark.as_html!(text, %Earmark.Options{compact_output: true, smartypants: false}) end def html_escape({text, mentions, hashtags}, type) do diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index a98b16d4b..b21dd4e23 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -843,6 +843,18 @@ test "quote posting visibility" do {:ok, _} = CommonAPI.post(user, %{status: "nice", quote_id: public.id}) {:ok, _} = CommonAPI.post(another_user, %{status: "nice", quote_id: public.id}) end + + test "it properly mentions punycode domain" do + user = insert(:user) + + _mentioned_user = + insert(:user, ap_id: "https://xn--i2raa.com/users/yyy", nickname: "yyy@xn--i2raa.com") + + {:ok, activity} = + CommonAPI.post(user, %{status: "hey @yyy@xn--i2raa.com", content_type: "text/markdown"}) + + assert "https://xn--i2raa.com/users/yyy" in Object.normalize(activity).data["to"] + end end describe "reactions" do From df0b56576a6863db9f1ca2c7987f2a465bd1f1c3 Mon Sep 17 00:00:00 2001 From: tusooa Date: Sun, 24 Sep 2023 22:46:30 -0400 Subject: [PATCH 194/199] Fix other quotation mark conversion tests --- test/pleroma/web/common_api/utils_test.exs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/pleroma/web/common_api/utils_test.exs b/test/pleroma/web/common_api/utils_test.exs index 4ce039d64..27b1da1e3 100644 --- a/test/pleroma/web/common_api/utils_test.exs +++ b/test/pleroma/web/common_api/utils_test.exs @@ -200,7 +200,7 @@ test "local mentions" do {result, _, []} = Utils.format_input(code, "text/markdown") assert result == - ~s[

@mario @luigi yo what’s up?

] + ~s[

@mario @luigi yo what's up?

] end test "remote mentions" do @@ -211,7 +211,7 @@ test "remote mentions" do {result, _, []} = Utils.format_input(code, "text/markdown") assert result == - ~s[

@mario @luigi yo what’s up?

] + ~s[

@mario @luigi yo what's up?

] end test "raw HTML" do @@ -229,7 +229,7 @@ test "rulers" do test "blockquote" do code = ~s[> whoms't are you quoting?] {result, [], []} = Utils.format_input(code, "text/markdown") - assert result == "

whoms’t are you quoting?

" + assert result == "

whoms't are you quoting?

" end test "code" do From a2a69709b51692be307940c79d0befdd3c9678bb Mon Sep 17 00:00:00 2001 From: tusooa Date: Tue, 24 Oct 2023 19:57:31 -0400 Subject: [PATCH 195/199] Bump version to 2.6.0 --- CHANGELOG.md | 38 +++++++++++++++++-- changelog.d/2023-06-deps-update.skip | 0 changelog.d/3126.fix | 1 - changelog.d/3739.skip | 0 changelog.d/3801.fix | 1 - changelog.d/3831.skip | 0 changelog.d/3848.add | 1 - changelog.d/3870.skip | 0 changelog.d/3872.remove | 1 - changelog.d/3873.fix | 1 - changelog.d/3874.remove | 1 - changelog.d/3876.skip | 0 changelog.d/3877.skip | 0 changelog.d/3878.skip | 0 changelog.d/3879.fix | 1 - changelog.d/3880.remove | 1 - changelog.d/3882.add | 1 - changelog.d/3883.fix | 1 - changelog.d/3884.fix | 1 - changelog.d/3885.fix | 1 - changelog.d/3888.fix | 1 - changelog.d/3891.fix | 1 - changelog.d/3893.skip | 0 changelog.d/3897.add | 1 - changelog.d/3899.skip | 0 changelog.d/3901.security | 1 - changelog.d/3902.skip | 0 changelog.d/3909.skip | 0 .../akkoma-xml-remote-entities.security | 1 - changelog.d/amd64-runner.skip | 0 changelog.d/attachment-type-check.fix | 1 - changelog.d/changelog-improve.skip | 0 .../check-attachment-attribution.security | 1 - changelog.d/delete-status-of-banned-user.fix | 1 - changelog.d/deprecate-scrobbles.remove | 1 - .../disable-xml-entity-resolution.security | 1 - changelog.d/distro-docs-elixir-1.11.skip | 0 changelog.d/dockerfile-config-perms.fix | 1 - changelog.d/emoji-pack-sanitization.security | 1 - changelog.d/emoji-policy.add | 1 - ...d-collection-shouldnt-break-user-fetch.fix | 1 - changelog.d/fix-object-test.fix | 1 - changelog.d/gentoo_otp.skip | 0 changelog.d/gentoo_otp_hotfix.skip | 0 changelog.d/gentoo_otp_intro.skip | 0 .../handle-report-from-deactivated-user.fix | 1 - changelog.d/lint.skip | 0 changelog.d/media-altdomain.skip | 0 changelog.d/no_new_privs.add | 1 - changelog.d/otp_perms.security | 1 - changelog.d/pipeline-triggers.skip | 0 ...revent-bypassing-authorized-fetch-mode.fix | 1 - changelog.d/punycode-mention.fix | 1 - changelog.d/quote.add | 1 - changelog.d/testfix-system-config-use.skip | 0 changelog.d/unified-streaming.add | 1 - .../update-credentials-limit-error.fix | 1 - mix.exs | 2 +- 58 files changed, 35 insertions(+), 40 deletions(-) delete mode 100644 changelog.d/2023-06-deps-update.skip delete mode 100644 changelog.d/3126.fix delete mode 100644 changelog.d/3739.skip delete mode 100644 changelog.d/3801.fix delete mode 100644 changelog.d/3831.skip delete mode 100644 changelog.d/3848.add delete mode 100644 changelog.d/3870.skip delete mode 100644 changelog.d/3872.remove delete mode 100644 changelog.d/3873.fix delete mode 100644 changelog.d/3874.remove delete mode 100644 changelog.d/3876.skip delete mode 100644 changelog.d/3877.skip delete mode 100644 changelog.d/3878.skip delete mode 100644 changelog.d/3879.fix delete mode 100644 changelog.d/3880.remove delete mode 100644 changelog.d/3882.add delete mode 100644 changelog.d/3883.fix delete mode 100644 changelog.d/3884.fix delete mode 100644 changelog.d/3885.fix delete mode 100644 changelog.d/3888.fix delete mode 100644 changelog.d/3891.fix delete mode 100644 changelog.d/3893.skip delete mode 100644 changelog.d/3897.add delete mode 100644 changelog.d/3899.skip delete mode 100644 changelog.d/3901.security delete mode 100644 changelog.d/3902.skip delete mode 100644 changelog.d/3909.skip delete mode 100644 changelog.d/akkoma-xml-remote-entities.security delete mode 100644 changelog.d/amd64-runner.skip delete mode 100644 changelog.d/attachment-type-check.fix delete mode 100644 changelog.d/changelog-improve.skip delete mode 100644 changelog.d/check-attachment-attribution.security delete mode 100644 changelog.d/delete-status-of-banned-user.fix delete mode 100644 changelog.d/deprecate-scrobbles.remove delete mode 100644 changelog.d/disable-xml-entity-resolution.security delete mode 100644 changelog.d/distro-docs-elixir-1.11.skip delete mode 100644 changelog.d/dockerfile-config-perms.fix delete mode 100644 changelog.d/emoji-pack-sanitization.security delete mode 100644 changelog.d/emoji-policy.add delete mode 100644 changelog.d/featured-collection-shouldnt-break-user-fetch.fix delete mode 100644 changelog.d/fix-object-test.fix delete mode 100644 changelog.d/gentoo_otp.skip delete mode 100644 changelog.d/gentoo_otp_hotfix.skip delete mode 100644 changelog.d/gentoo_otp_intro.skip delete mode 100644 changelog.d/handle-report-from-deactivated-user.fix delete mode 100644 changelog.d/lint.skip delete mode 100644 changelog.d/media-altdomain.skip delete mode 100644 changelog.d/no_new_privs.add delete mode 100644 changelog.d/otp_perms.security delete mode 100644 changelog.d/pipeline-triggers.skip delete mode 100644 changelog.d/prevent-bypassing-authorized-fetch-mode.fix delete mode 100644 changelog.d/punycode-mention.fix delete mode 100644 changelog.d/quote.add delete mode 100644 changelog.d/testfix-system-config-use.skip delete mode 100644 changelog.d/unified-streaming.add delete mode 100644 changelog.d/update-credentials-limit-error.fix diff --git a/CHANGELOG.md b/CHANGELOG.md index 65acfad3e..211e611ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,19 +4,49 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). -## Unreleased - -### Changed +## 2.6.0 +### Security +- Preload: Make generated JSON html-safe. It already was html safe because it only consists of config data that is base64 encoded, but this will keep it safe it that ever changes. +- CommonAPI: Prevent users from accessing media of other users by creating a status with reused attachment ID +- Disable XML entity resolution completely to fix a dos vulnerability ### Added - Support for Image activities, namely from Hubzilla +- Add OAuth scope descriptions +- Allow lang attribute in status text +- OnlyMedia Upload Filter +- Implement MRF policy to reject or delist according to emojis +- (hardening) Add no_new_privs=yes to OpenRC service files +- Implement quotes +- Add unified streaming endpoint ### Fixed - - rel="me" was missing its cache +- MediaProxy responses now return a sandbox CSP header +- Filter context activities using Visibility.visible_for_user? +- UploadedMedia: Add missing disposition_type to Content-Disposition +- fix not being able to fetch flash file from remote instance +- Fix abnormal behaviour when refetching a poll +- Allow non-HTTP(s) URIs in "url" fields for compatibility with "FEP-fffd: Proxy Objects" +- Fix opengraph and twitter card meta tags +- ForceMentionsInContent: fix double mentions for Mastodon/Misskey posts +- OEmbed HTML tags are now filtered +- Restrict attachments to only uploaded files only +- Fix error 404 when deleting status of a banned user +- Fix config ownership in dockerfile to pass restriction test +- Fix user fetch completely broken if featured collection is not in a supported form +- Correctly handle the situation when a poll has both "anyOf" and "oneOf" but one of them being empty +- Fix handling report from a deactivated user +- Prevent using the .json format to bypass authorized fetch mode +- Fix mentioning punycode domains when using Markdown +- Show more informative errors when profile exceeds char limits ### Removed - BREAKING: Support for passwords generated with `crypt(3)` (Gnu Social migration artifact) +- remove BBS/SSH feature, replaced by an external bridge. +- Remove a few unused indexes. +- Cleanup OStatus-era user upgrades and ap_enabled indicator +- Deprecate Pleroma's audio scrobbling ## 2.5.4 diff --git a/changelog.d/2023-06-deps-update.skip b/changelog.d/2023-06-deps-update.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/3126.fix b/changelog.d/3126.fix deleted file mode 100644 index 91d396c89..000000000 --- a/changelog.d/3126.fix +++ /dev/null @@ -1 +0,0 @@ -MediaProxy responses now return a sandbox CSP header diff --git a/changelog.d/3739.skip b/changelog.d/3739.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/3801.fix b/changelog.d/3801.fix deleted file mode 100644 index 8c2ec0199..000000000 --- a/changelog.d/3801.fix +++ /dev/null @@ -1 +0,0 @@ -Filter context activities using Visibility.visible_for_user? diff --git a/changelog.d/3831.skip b/changelog.d/3831.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/3848.add b/changelog.d/3848.add deleted file mode 100644 index d7b1b0a84..000000000 --- a/changelog.d/3848.add +++ /dev/null @@ -1 +0,0 @@ -Add OAuth scope descriptions diff --git a/changelog.d/3870.skip b/changelog.d/3870.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/3872.remove b/changelog.d/3872.remove deleted file mode 100644 index 54cbb660e..000000000 --- a/changelog.d/3872.remove +++ /dev/null @@ -1 +0,0 @@ -remove BBS/SSH feature, replaced by an external bridge. \ No newline at end of file diff --git a/changelog.d/3873.fix b/changelog.d/3873.fix deleted file mode 100644 index 4699f7b58..000000000 --- a/changelog.d/3873.fix +++ /dev/null @@ -1 +0,0 @@ -UploadedMedia: Add missing disposition_type to Content-Disposition \ No newline at end of file diff --git a/changelog.d/3874.remove b/changelog.d/3874.remove deleted file mode 100644 index a81f744bf..000000000 --- a/changelog.d/3874.remove +++ /dev/null @@ -1 +0,0 @@ -Remove a few unused indexes. diff --git a/changelog.d/3876.skip b/changelog.d/3876.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/3877.skip b/changelog.d/3877.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/3878.skip b/changelog.d/3878.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/3879.fix b/changelog.d/3879.fix deleted file mode 100644 index 7c58cc3c2..000000000 --- a/changelog.d/3879.fix +++ /dev/null @@ -1 +0,0 @@ -fix not being able to fetch flash file from remote instance \ No newline at end of file diff --git a/changelog.d/3880.remove b/changelog.d/3880.remove deleted file mode 100644 index 113c76c85..000000000 --- a/changelog.d/3880.remove +++ /dev/null @@ -1 +0,0 @@ -Cleanup OStatus-era user upgrades and ap_enabled indicator \ No newline at end of file diff --git a/changelog.d/3882.add b/changelog.d/3882.add deleted file mode 100644 index 4712de1dc..000000000 --- a/changelog.d/3882.add +++ /dev/null @@ -1 +0,0 @@ -Allow lang attribute in status text diff --git a/changelog.d/3883.fix b/changelog.d/3883.fix deleted file mode 100644 index 6824f2013..000000000 --- a/changelog.d/3883.fix +++ /dev/null @@ -1 +0,0 @@ -Fix abnormal behaviour when refetching a poll diff --git a/changelog.d/3884.fix b/changelog.d/3884.fix deleted file mode 100644 index f8dbb2bbf..000000000 --- a/changelog.d/3884.fix +++ /dev/null @@ -1 +0,0 @@ -Allow non-HTTP(s) URIs in "url" fields for compatibility with "FEP-fffd: Proxy Objects" \ No newline at end of file diff --git a/changelog.d/3885.fix b/changelog.d/3885.fix deleted file mode 100644 index c5fbb0ed4..000000000 --- a/changelog.d/3885.fix +++ /dev/null @@ -1 +0,0 @@ -Fix opengraph and twitter card meta tags diff --git a/changelog.d/3888.fix b/changelog.d/3888.fix deleted file mode 100644 index 886aa7b39..000000000 --- a/changelog.d/3888.fix +++ /dev/null @@ -1 +0,0 @@ -ForceMentionsInContent: fix double mentions for Mastodon/Misskey posts \ No newline at end of file diff --git a/changelog.d/3891.fix b/changelog.d/3891.fix deleted file mode 100644 index f1fb62d82..000000000 --- a/changelog.d/3891.fix +++ /dev/null @@ -1 +0,0 @@ -OEmbed HTML tags are now filtered diff --git a/changelog.d/3893.skip b/changelog.d/3893.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/3897.add b/changelog.d/3897.add deleted file mode 100644 index 5c4402f45..000000000 --- a/changelog.d/3897.add +++ /dev/null @@ -1 +0,0 @@ -OnlyMedia Upload Filter diff --git a/changelog.d/3899.skip b/changelog.d/3899.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/3901.security b/changelog.d/3901.security deleted file mode 100644 index a3d8bd01f..000000000 --- a/changelog.d/3901.security +++ /dev/null @@ -1 +0,0 @@ -Preload: Make generated JSON html-safe. It already was html safe because it only consists of config data that is base64 encoded, but this will keep it safe it that ever changes. diff --git a/changelog.d/3902.skip b/changelog.d/3902.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/3909.skip b/changelog.d/3909.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/akkoma-xml-remote-entities.security b/changelog.d/akkoma-xml-remote-entities.security deleted file mode 100644 index 5e6725e5b..000000000 --- a/changelog.d/akkoma-xml-remote-entities.security +++ /dev/null @@ -1 +0,0 @@ -Fix XML External Entity (XXE) loading vulnerability allowing to fetch arbitary files from the server's filesystem diff --git a/changelog.d/amd64-runner.skip b/changelog.d/amd64-runner.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/attachment-type-check.fix b/changelog.d/attachment-type-check.fix deleted file mode 100644 index 9e14b75f1..000000000 --- a/changelog.d/attachment-type-check.fix +++ /dev/null @@ -1 +0,0 @@ -Restrict attachments to only uploaded files only diff --git a/changelog.d/changelog-improve.skip b/changelog.d/changelog-improve.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/check-attachment-attribution.security b/changelog.d/check-attachment-attribution.security deleted file mode 100644 index e0e46525b..000000000 --- a/changelog.d/check-attachment-attribution.security +++ /dev/null @@ -1 +0,0 @@ -CommonAPI: Prevent users from accessing media of other users by creating a status with reused attachment ID diff --git a/changelog.d/delete-status-of-banned-user.fix b/changelog.d/delete-status-of-banned-user.fix deleted file mode 100644 index 1fa6a29d8..000000000 --- a/changelog.d/delete-status-of-banned-user.fix +++ /dev/null @@ -1 +0,0 @@ -Fix error 404 when deleting status of a banned user diff --git a/changelog.d/deprecate-scrobbles.remove b/changelog.d/deprecate-scrobbles.remove deleted file mode 100644 index c453a9784..000000000 --- a/changelog.d/deprecate-scrobbles.remove +++ /dev/null @@ -1 +0,0 @@ -Deprecate Pleroma's audio scrobbling diff --git a/changelog.d/disable-xml-entity-resolution.security b/changelog.d/disable-xml-entity-resolution.security deleted file mode 100644 index db8e12f67..000000000 --- a/changelog.d/disable-xml-entity-resolution.security +++ /dev/null @@ -1 +0,0 @@ -Disable XML entity resolution completely to fix a dos vulnerability diff --git a/changelog.d/distro-docs-elixir-1.11.skip b/changelog.d/distro-docs-elixir-1.11.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/dockerfile-config-perms.fix b/changelog.d/dockerfile-config-perms.fix deleted file mode 100644 index 49ea5becb..000000000 --- a/changelog.d/dockerfile-config-perms.fix +++ /dev/null @@ -1 +0,0 @@ -- Fix config ownership in dockerfile to pass restriction test diff --git a/changelog.d/emoji-pack-sanitization.security b/changelog.d/emoji-pack-sanitization.security deleted file mode 100644 index f3218abd4..000000000 --- a/changelog.d/emoji-pack-sanitization.security +++ /dev/null @@ -1 +0,0 @@ -Emoji pack loader sanitizes pack names diff --git a/changelog.d/emoji-policy.add b/changelog.d/emoji-policy.add deleted file mode 100644 index 45510c4f6..000000000 --- a/changelog.d/emoji-policy.add +++ /dev/null @@ -1 +0,0 @@ -Implement MRF policy to reject or delist according to emojis diff --git a/changelog.d/featured-collection-shouldnt-break-user-fetch.fix b/changelog.d/featured-collection-shouldnt-break-user-fetch.fix deleted file mode 100644 index e8ce288cc..000000000 --- a/changelog.d/featured-collection-shouldnt-break-user-fetch.fix +++ /dev/null @@ -1 +0,0 @@ -Fix user fetch completely broken if featured collection is not in a supported form diff --git a/changelog.d/fix-object-test.fix b/changelog.d/fix-object-test.fix deleted file mode 100644 index 5eea719f0..000000000 --- a/changelog.d/fix-object-test.fix +++ /dev/null @@ -1 +0,0 @@ -Correctly handle the situation when a poll has both "anyOf" and "oneOf" but one of them being empty diff --git a/changelog.d/gentoo_otp.skip b/changelog.d/gentoo_otp.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/gentoo_otp_hotfix.skip b/changelog.d/gentoo_otp_hotfix.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/gentoo_otp_intro.skip b/changelog.d/gentoo_otp_intro.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/handle-report-from-deactivated-user.fix b/changelog.d/handle-report-from-deactivated-user.fix deleted file mode 100644 index 6692d1aa8..000000000 --- a/changelog.d/handle-report-from-deactivated-user.fix +++ /dev/null @@ -1 +0,0 @@ -Fix handling report from a deactivated user diff --git a/changelog.d/lint.skip b/changelog.d/lint.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/media-altdomain.skip b/changelog.d/media-altdomain.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/no_new_privs.add b/changelog.d/no_new_privs.add deleted file mode 100644 index b67396a4b..000000000 --- a/changelog.d/no_new_privs.add +++ /dev/null @@ -1 +0,0 @@ -(hardening) Add no_new_privs=yes to OpenRC service files diff --git a/changelog.d/otp_perms.security b/changelog.d/otp_perms.security deleted file mode 100644 index a3da1c677..000000000 --- a/changelog.d/otp_perms.security +++ /dev/null @@ -1 +0,0 @@ -- Reduced permissions of config files and directories, distros requiring greater permissions like group-read need to pre-create the directories \ No newline at end of file diff --git a/changelog.d/pipeline-triggers.skip b/changelog.d/pipeline-triggers.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/prevent-bypassing-authorized-fetch-mode.fix b/changelog.d/prevent-bypassing-authorized-fetch-mode.fix deleted file mode 100644 index 12f7260d7..000000000 --- a/changelog.d/prevent-bypassing-authorized-fetch-mode.fix +++ /dev/null @@ -1 +0,0 @@ -Prevent using the .json format to bypass authorized fetch mode \ No newline at end of file diff --git a/changelog.d/punycode-mention.fix b/changelog.d/punycode-mention.fix deleted file mode 100644 index f013c2dac..000000000 --- a/changelog.d/punycode-mention.fix +++ /dev/null @@ -1 +0,0 @@ -Fix mentioning punycode domains when using Markdown diff --git a/changelog.d/quote.add b/changelog.d/quote.add deleted file mode 100644 index 1c368ae75..000000000 --- a/changelog.d/quote.add +++ /dev/null @@ -1 +0,0 @@ -Implement quotes diff --git a/changelog.d/testfix-system-config-use.skip b/changelog.d/testfix-system-config-use.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/unified-streaming.add b/changelog.d/unified-streaming.add deleted file mode 100644 index 84821fcc8..000000000 --- a/changelog.d/unified-streaming.add +++ /dev/null @@ -1 +0,0 @@ -Add unified streaming endpoint diff --git a/changelog.d/update-credentials-limit-error.fix b/changelog.d/update-credentials-limit-error.fix deleted file mode 100644 index 7682f958e..000000000 --- a/changelog.d/update-credentials-limit-error.fix +++ /dev/null @@ -1 +0,0 @@ -Show more informative errors when profile exceeds char limits diff --git a/mix.exs b/mix.exs index b071e7c7b..082b39e55 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do def project do [ app: :pleroma, - version: version("2.5.54"), + version: version("2.6.0"), elixir: "~> 1.11", elixirc_paths: elixirc_paths(Mix.env()), compilers: [:phoenix] ++ Mix.compilers(), From 6a13c2d180177692b976b179f0bd86f8f93e2803 Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 25 Oct 2023 20:44:47 -0400 Subject: [PATCH 196/199] Add collect-changelog script --- .gitlab/merge_request_templates/Release.md | 2 +- tools/collect-changelog | 27 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100755 tools/collect-changelog diff --git a/.gitlab/merge_request_templates/Release.md b/.gitlab/merge_request_templates/Release.md index 9638d6d11..e57556e6c 100644 --- a/.gitlab/merge_request_templates/Release.md +++ b/.gitlab/merge_request_templates/Release.md @@ -1,6 +1,6 @@ ### Release checklist * [ ] Bump version in `mix.exs` -* [ ] Compile a changelog +* [ ] Compile a changelog with the `tools/collect-changelog` script * [ ] Create an MR with an announcement to pleroma.social #### post-merge * [ ] Tag the release on the merge commit diff --git a/tools/collect-changelog b/tools/collect-changelog new file mode 100755 index 000000000..1e12d5640 --- /dev/null +++ b/tools/collect-changelog @@ -0,0 +1,27 @@ +#!/bin/sh + +collectType() { + local suffix="$1" + local header="$2" + local printed=0 + for file in changelog.d/*."$suffix"; do + if [ '!' -f "$file" ]; then + continue + fi + if [ "$printed" = 0 ]; then + echo + echo "### $header" + printed=1 + fi + # Normalize any trailing newlines/spaces, etc. + echo "- $(cat "$file")" + done +} + +collectType security Security +collectType change Changed +collectType add Added +collectType fix Fixed +collectType remove Removed + +rm changelog.d/* From ab894d98f4be5aadc033b71dacc499b85b579bc1 Mon Sep 17 00:00:00 2001 From: tusooa Date: Sun, 29 Oct 2023 12:59:03 -0400 Subject: [PATCH 197/199] Bundle 2.6.0 frontend --- priv/static/index.html | 2 +- .../static/css/5948.06d2a0d84620cba6a4fb.css | Bin 1335 -> 0 bytes .../css/5948.06d2a0d84620cba6a4fb.css.map | 1 - .../static/css/7586.0d43f70bc6240422f179.css | Bin 0 -> 1511 bytes .../css/7586.0d43f70bc6240422f179.css.map | 1 + .../static/css/7962.76663e78ad5ea0bb0b90.css | Bin 0 -> 19677 bytes .../css/7962.76663e78ad5ea0bb0b90.css.map | 1 + .../static/css/9114.8def3b2b7fe70b3b3712.css | Bin 19517 -> 0 bytes .../css/9114.8def3b2b7fe70b3b3712.css.map | 1 - .../static/css/9801.cfe503d4c949ae0c3813.css | Bin 0 -> 1884 bytes .../css/9801.cfe503d4c949ae0c3813.css.map | 1 + .../static/css/app.48e52505beba5b9ab69b.css | Bin 124954 -> 0 bytes .../css/app.48e52505beba5b9ab69b.css.map | 1 - .../static/css/app.c18a2c80794a1b699a61.css | Bin 0 -> 128030 bytes .../css/app.c18a2c80794a1b699a61.css.map | 1 + .../static/js/159.3a9274574f1e33801c4a.js.map | Bin 5445 -> 0 bytes ...3801c4a.js => 159.903e90c9de8ef6c67077.js} | Bin 1642 -> 1642 bytes .../static/js/159.903e90c9de8ef6c67077.js.map | Bin 0 -> 5535 bytes .../static/js/2724.e4840c73281069ba54ab.js | Bin 471273 -> 0 bytes .../js/2724.e4840c73281069ba54ab.js.map | Bin 3648078 -> 0 bytes .../static/js/3733.7060d1e6bca813125a0c.js | Bin 0 -> 473444 bytes ... 3733.7060d1e6bca813125a0c.js.LICENSE.txt} | 0 .../js/3733.7060d1e6bca813125a0c.js.map | Bin 0 -> 3584678 bytes ...c89c4958.js => 48.b5ecdbc517423af07ca4.js} | Bin 63226 -> 63304 bytes ...=> 48.b5ecdbc517423af07ca4.js.LICENSE.txt} | 4 ++-- .../static/js/48.b5ecdbc517423af07ca4.js.map | Bin 0 -> 306732 bytes .../static/js/48.d7e479b200a6c89c4958.js.map | Bin 307324 -> 0 bytes .../static/js/5948.2b7b4e97487f2539eb44.js | Bin 11022 -> 0 bytes .../js/5948.2b7b4e97487f2539eb44.js.map | Bin 30738 -> 0 bytes ...e4e5f8.js => 6464.eb9c90a1c948cde554e9.js} | Bin 3916 -> 3916 bytes .../js/6464.eb9c90a1c948cde554e9.js.map | Bin 0 -> 10548 bytes .../js/6464.fea96fa80a7373e4e5f8.js.map | Bin 10178 -> 0 bytes .../static/js/7586.981b2305a0019f6042a5.js | Bin 0 -> 12834 bytes .../js/7586.981b2305a0019f6042a5.js.map | Bin 0 -> 35169 bytes .../static/js/7962.e25d40b042f8ee7389c3.js | Bin 0 -> 179038 bytes .../js/7962.e25d40b042f8ee7389c3.js.map | Bin 0 -> 394938 bytes .../static/js/9060.24271e167e0471a1a732.js | Bin 0 -> 15495 bytes .../js/9060.24271e167e0471a1a732.js.map | Bin 0 -> 35326 bytes .../static/js/9114.e761a1c6846fea99aaf1.js | Bin 186399 -> 0 bytes .../js/9114.e761a1c6846fea99aaf1.js.map | Bin 399283 -> 0 bytes .../static/js/9801.99ace6b5dc657bf1a65b.js | Bin 0 -> 25711 bytes .../js/9801.99ace6b5dc657bf1a65b.js.map | Bin 0 -> 57596 bytes .../static/js/app.7c4b412b26221a7c8572.js | Bin 0 -> 875657 bytes .../static/js/app.7c4b412b26221a7c8572.js.map | Bin 0 -> 2207711 bytes .../static/js/app.8d2126d35dba9482db51.js | Bin 828121 -> 0 bytes .../static/js/app.8d2126d35dba9482db51.js.map | Bin 2083092 -> 0 bytes .../js/i18n/ar-json.4916f840147303aa65fe.js | Bin 0 -> 75033 bytes .../i18n/ar-json.4916f840147303aa65fe.js.map | Bin 0 -> 175929 bytes .../js/i18n/ar-json.d09609af3224232857d6.js | Bin 14319 -> 0 bytes .../i18n/ar-json.d09609af3224232857d6.js.map | Bin 33742 -> 0 bytes .../js/i18n/eo-json.6c62eef99e850912498b.js | Bin 0 -> 87510 bytes .../i18n/eo-json.6c62eef99e850912498b.js.map | Bin 0 -> 212971 bytes .../js/i18n/eo-json.d81690d5be30b23e516b.js | Bin 81295 -> 0 bytes .../i18n/eo-json.d81690d5be30b23e516b.js.map | Bin 198451 -> 0 bytes .../js/i18n/id-json.3e42564ce7a3a847ecb0.js | Bin 39564 -> 0 bytes .../i18n/id-json.3e42564ce7a3a847ecb0.js.map | Bin 99138 -> 0 bytes .../js/i18n/id-json.e5c9ee768155f88128b9.js | Bin 0 -> 44139 bytes .../i18n/id-json.e5c9ee768155f88128b9.js.map | Bin 0 -> 111136 bytes .../js/i18n/ko-json.4bd28b26a7390a09afc2.js | Bin 84194 -> 0 bytes .../i18n/ko-json.4bd28b26a7390a09afc2.js.map | Bin 199920 -> 0 bytes .../js/i18n/ko-json.9029d09084bb22d8b705.js | Bin 0 -> 87936 bytes .../i18n/ko-json.9029d09084bb22d8b705.js.map | Bin 0 -> 208897 bytes .../i18n/nan-TW-json.7f2789d8a461e86d1734.js | Bin 0 -> 57301 bytes .../nan-TW-json.7f2789d8a461e86d1734.js.map | Bin 0 -> 136688 bytes ...dac.js => zh-json.5831b903c3e6d281f122.js} | Bin 84380 -> 90880 bytes .../i18n/zh-json.5831b903c3e6d281f122.js.map | Bin 0 -> 223171 bytes .../i18n/zh-json.63e4c9fe0197374a5dac.js.map | Bin 208083 -> 0 bytes .../zh_Hant-json.bfa569654a5cd74767ce.js.map | Bin 148361 -> 0 bytes ...s => zh_Hant-json.f7e1d0f4b873c60d6396.js} | Bin 59401 -> 59689 bytes .../zh_Hant-json.f7e1d0f4b873c60d6396.js.map | Bin 0 -> 149004 bytes priv/static/sw-pleroma.js | Bin 182074 -> 183059 bytes priv/static/sw-pleroma.js.map | Bin 1245266 -> 1248352 bytes 72 files changed, 7 insertions(+), 6 deletions(-) delete mode 100644 priv/static/static/css/5948.06d2a0d84620cba6a4fb.css delete mode 100644 priv/static/static/css/5948.06d2a0d84620cba6a4fb.css.map create mode 100644 priv/static/static/css/7586.0d43f70bc6240422f179.css create mode 100644 priv/static/static/css/7586.0d43f70bc6240422f179.css.map create mode 100644 priv/static/static/css/7962.76663e78ad5ea0bb0b90.css create mode 100644 priv/static/static/css/7962.76663e78ad5ea0bb0b90.css.map delete mode 100644 priv/static/static/css/9114.8def3b2b7fe70b3b3712.css delete mode 100644 priv/static/static/css/9114.8def3b2b7fe70b3b3712.css.map create mode 100644 priv/static/static/css/9801.cfe503d4c949ae0c3813.css create mode 100644 priv/static/static/css/9801.cfe503d4c949ae0c3813.css.map delete mode 100644 priv/static/static/css/app.48e52505beba5b9ab69b.css delete mode 100644 priv/static/static/css/app.48e52505beba5b9ab69b.css.map create mode 100644 priv/static/static/css/app.c18a2c80794a1b699a61.css create mode 100644 priv/static/static/css/app.c18a2c80794a1b699a61.css.map delete mode 100644 priv/static/static/js/159.3a9274574f1e33801c4a.js.map rename priv/static/static/js/{159.3a9274574f1e33801c4a.js => 159.903e90c9de8ef6c67077.js} (96%) create mode 100644 priv/static/static/js/159.903e90c9de8ef6c67077.js.map delete mode 100644 priv/static/static/js/2724.e4840c73281069ba54ab.js delete mode 100644 priv/static/static/js/2724.e4840c73281069ba54ab.js.map create mode 100644 priv/static/static/js/3733.7060d1e6bca813125a0c.js rename priv/static/static/js/{2724.e4840c73281069ba54ab.js.LICENSE.txt => 3733.7060d1e6bca813125a0c.js.LICENSE.txt} (100%) create mode 100644 priv/static/static/js/3733.7060d1e6bca813125a0c.js.map rename priv/static/static/js/{48.d7e479b200a6c89c4958.js => 48.b5ecdbc517423af07ca4.js} (59%) rename priv/static/static/js/{48.d7e479b200a6c89c4958.js.LICENSE.txt => 48.b5ecdbc517423af07ca4.js.LICENSE.txt} (77%) create mode 100644 priv/static/static/js/48.b5ecdbc517423af07ca4.js.map delete mode 100644 priv/static/static/js/48.d7e479b200a6c89c4958.js.map delete mode 100644 priv/static/static/js/5948.2b7b4e97487f2539eb44.js delete mode 100644 priv/static/static/js/5948.2b7b4e97487f2539eb44.js.map rename priv/static/static/js/{6464.fea96fa80a7373e4e5f8.js => 6464.eb9c90a1c948cde554e9.js} (98%) create mode 100644 priv/static/static/js/6464.eb9c90a1c948cde554e9.js.map delete mode 100644 priv/static/static/js/6464.fea96fa80a7373e4e5f8.js.map create mode 100644 priv/static/static/js/7586.981b2305a0019f6042a5.js create mode 100644 priv/static/static/js/7586.981b2305a0019f6042a5.js.map create mode 100644 priv/static/static/js/7962.e25d40b042f8ee7389c3.js create mode 100644 priv/static/static/js/7962.e25d40b042f8ee7389c3.js.map create mode 100644 priv/static/static/js/9060.24271e167e0471a1a732.js create mode 100644 priv/static/static/js/9060.24271e167e0471a1a732.js.map delete mode 100644 priv/static/static/js/9114.e761a1c6846fea99aaf1.js delete mode 100644 priv/static/static/js/9114.e761a1c6846fea99aaf1.js.map create mode 100644 priv/static/static/js/9801.99ace6b5dc657bf1a65b.js create mode 100644 priv/static/static/js/9801.99ace6b5dc657bf1a65b.js.map create mode 100644 priv/static/static/js/app.7c4b412b26221a7c8572.js create mode 100644 priv/static/static/js/app.7c4b412b26221a7c8572.js.map delete mode 100644 priv/static/static/js/app.8d2126d35dba9482db51.js delete mode 100644 priv/static/static/js/app.8d2126d35dba9482db51.js.map create mode 100644 priv/static/static/js/i18n/ar-json.4916f840147303aa65fe.js create mode 100644 priv/static/static/js/i18n/ar-json.4916f840147303aa65fe.js.map delete mode 100644 priv/static/static/js/i18n/ar-json.d09609af3224232857d6.js delete mode 100644 priv/static/static/js/i18n/ar-json.d09609af3224232857d6.js.map create mode 100644 priv/static/static/js/i18n/eo-json.6c62eef99e850912498b.js create mode 100644 priv/static/static/js/i18n/eo-json.6c62eef99e850912498b.js.map delete mode 100644 priv/static/static/js/i18n/eo-json.d81690d5be30b23e516b.js delete mode 100644 priv/static/static/js/i18n/eo-json.d81690d5be30b23e516b.js.map delete mode 100644 priv/static/static/js/i18n/id-json.3e42564ce7a3a847ecb0.js delete mode 100644 priv/static/static/js/i18n/id-json.3e42564ce7a3a847ecb0.js.map create mode 100644 priv/static/static/js/i18n/id-json.e5c9ee768155f88128b9.js create mode 100644 priv/static/static/js/i18n/id-json.e5c9ee768155f88128b9.js.map delete mode 100644 priv/static/static/js/i18n/ko-json.4bd28b26a7390a09afc2.js delete mode 100644 priv/static/static/js/i18n/ko-json.4bd28b26a7390a09afc2.js.map create mode 100644 priv/static/static/js/i18n/ko-json.9029d09084bb22d8b705.js create mode 100644 priv/static/static/js/i18n/ko-json.9029d09084bb22d8b705.js.map create mode 100644 priv/static/static/js/i18n/nan-TW-json.7f2789d8a461e86d1734.js create mode 100644 priv/static/static/js/i18n/nan-TW-json.7f2789d8a461e86d1734.js.map rename priv/static/static/js/i18n/{zh-json.63e4c9fe0197374a5dac.js => zh-json.5831b903c3e6d281f122.js} (58%) create mode 100644 priv/static/static/js/i18n/zh-json.5831b903c3e6d281f122.js.map delete mode 100644 priv/static/static/js/i18n/zh-json.63e4c9fe0197374a5dac.js.map delete mode 100644 priv/static/static/js/i18n/zh_Hant-json.bfa569654a5cd74767ce.js.map rename priv/static/static/js/i18n/{zh_Hant-json.bfa569654a5cd74767ce.js => zh_Hant-json.f7e1d0f4b873c60d6396.js} (61%) create mode 100644 priv/static/static/js/i18n/zh_Hant-json.f7e1d0f4b873c60d6396.js.map diff --git a/priv/static/index.html b/priv/static/index.html index 7dd5d0b78..52ff685c0 100644 --- a/priv/static/index.html +++ b/priv/static/index.html @@ -1 +1 @@ -
\ No newline at end of file +
\ No newline at end of file diff --git a/priv/static/static/css/5948.06d2a0d84620cba6a4fb.css b/priv/static/static/css/5948.06d2a0d84620cba6a4fb.css deleted file mode 100644 index b14e141435c66818830a5f2eb95c152e18c260ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1335 zcmbVL+m6~W5Pk1gM5IbwhmHN;}6HmacZ?YXAium_VOtJ))Le-Zj zbMefaGiNCCwJu0ulr0SP- z@9O6ZKvK;@QkF7zrI$rGqO^$-GjJW+--*_aKuQkreH z7Mz1Vk0;dFAdDtb`0(lManjo{+O#Rehgt#&wS`P0=6(Pop$yrD0ZtkuQe5wW2^~DL zG$9xjfn6OMhpd(d5dKE-L1y127@N`O_q@G9?|3AZX^>5~xtDaa&K28{t>9sup}Eb^ zh|Ys!+KZOkEdG|psezO5%CQ)w&wl6+LgD&IS?MB%fq5W86H|r!&<=Hz+TH$(0xnoG zI?l0Z{r643wQlt5P@)KrCD7b#_wNZmRn>uwR~kvdP4T*CCYI}|AoX8WcdK5fbj00J zj?&7Jn2zyDI?#T4N%#ZQr7?Jd&F}=T06b*Bj(df;c&qKP-F%c#*x|1GTR!QSx0g>= z4li+XUWi(N--ChU<#vjD)6PLfk_GrVDwr%rGZbO+kj&D%gy0u9fB(;#_`i.panel-body {\n height: 100%;\n overflow-y: hidden;\n\n .btn {\n min-height: 2em;\n min-width: 10em;\n padding: 0 2em;\n }\n }\n }\n\n .settings-footer {\n display: flex;\n\n >* {\n margin-right: 0.5em;\n }\n\n .extra-content {\n display: flex;\n flex-grow: 1;\n }\n }\n\n &.peek {\n .settings-modal-panel {\n /* Explanation:\n * Modal is positioned vertically centered.\n * 100vh - 100% = Distance between modal's top+bottom boundaries and screen\n * (100vh - 100%) / 2 = Distance between bottom (or top) boundary and screen\n * + 100% - we move modal completely off-screen, it's top boundary touches\n * bottom of the screen\n * - 50px - leaving tiny amount of space so that titlebar + tiny amount of modal is visible\n */\n transform: translateY(calc(((100vh - 100%) / 2 + 100%) - 50px));\n\n @media all and (max-width: 800px) {\n /* For mobile, the modal takes 100% of the available screen.\n This ensures the minimized modal is always 50px above the browser bottom\n bar regardless of whether or not it is visible.\n */\n transform: translateY(calc(100% - 50px));\n }\n }\n }\n}\n"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/priv/static/static/css/7586.0d43f70bc6240422f179.css b/priv/static/static/css/7586.0d43f70bc6240422f179.css new file mode 100644 index 0000000000000000000000000000000000000000..7da2aa2ea553e16d597de1814c074b2788cdcfb5 GIT binary patch literal 1511 zcmbVM+iu%141M2M7#Ip9-K09VrnRwRAF#a)*h3!`i%zu3k|4=RJR|=;TCsDhmlS&u zOo~1v506Nht5ilZT^OxEc>>mITQez^iU<#dOEPqWO}TJJvMR|XY||Bp<;o|cIKE4N zmClR2LJW*kzK!3}UgU@_ys8UkmqI0U21RNZ=Rz%m5sjhPQ3u|m$B{yFCTqP0n@hb( zR)TX-m-i9X#tW^86wZIX_&MtAa9cMa#Q91B@s)u@Vb5IwL_+S9o&wGiL{ePOfdOrM zWTCy+MQH45(O6`;*f-&BfS*KmS-dt0oxJ7k*g6N7SrJI8a~u+J;twQq9Nm5nU7^@} z=Lw*_T$~Sw7^XnhtsI`?^$HQTWFxp=C1`Hai|M7_GUY@wuq1ru+7>5H!b{7-EuFYi zzZV5+g5;&j!Zk1lL@1(5pYGG47-O^DeJy|s77dPdJT?Ad-ASR<)q$VZK5r!``v`&N zdb4{w#XdG(Xdney;85ok?)FeG_no3`*e%U+xcO5=VT&^A zc27?^ON*OhbO}Ta!0(f8!lr2zaFFF*pptob9b`;qgBj{O`V`ILyNKY4q8h$iZvU6D z(akZ}?!&|V$H?i@X7HUEgAd~8k8fY*v!_QI^XVj?$BXQ7JdLO0aXy+q)66+qFmwO# E7sVehfdBvi literal 0 HcmV?d00001 diff --git a/priv/static/static/css/7586.0d43f70bc6240422f179.css.map b/priv/static/static/css/7586.0d43f70bc6240422f179.css.map new file mode 100644 index 000000000..f8f61fe6e --- /dev/null +++ b/priv/static/static/css/7586.0d43f70bc6240422f179.css.map @@ -0,0 +1 @@ +{"version":3,"file":"static/css/7586.0d43f70bc6240422f179.css","mappings":"AACA,uBAGE,mBAFA,aACA,YAEA,uBAEA,4BACE,YACA,iBCPJ,gBACE,gBAEA,2DAEE,qBACA,iBAEA,iEACE,mBAGF,mFACE,gBAIJ,qCAGE,cADA,kBADA,eAEA,CAGF,sCAOE,YADA,eALA,gBACA,qBAEA,wBADA,uCAEA,YAEA,CAEA,yBATF,sCAWI,YADA,eACA,EAGF,kDACE,YACA,kBAEA,uDACE,eAGF,6EACE,cAKN,iCACE,aACA,eACA,cAEA,mCACE,kBAGF,gDACE,aACA,YAKF,2CASE,8CAEA,yBAXF,2CAgBI","sources":["webpack://pleroma_fe/./src/components/async_component_error/async_component_error.vue","webpack://pleroma_fe/./src/components/settings_modal/settings_modal.scss"],"sourcesContent":["\n.async-component-error {\n display: flex;\n height: 100%;\n align-items: center;\n justify-content: center;\n\n .btn {\n margin: 0.5em;\n padding: 0.5em 2em;\n }\n}\n","@import \"src/variables\";\n\n.settings-modal {\n overflow: hidden;\n\n .setting-list,\n .option-list {\n list-style-type: none;\n padding-left: 2em;\n\n li {\n margin-bottom: 0.5em;\n }\n\n .suboptions {\n margin-top: 0.3em;\n }\n }\n\n .setting-description {\n margin-top: 0.2em;\n margin-bottom: 2em;\n font-size: 70%;\n }\n\n .settings-modal-panel {\n overflow: hidden;\n transition: transform;\n transition-timing-function: ease-in-out;\n transition-duration: 300ms;\n width: 1000px;\n max-width: 90vw;\n height: 90vh;\n\n @media all and (max-width: 800px) {\n max-width: 100vw;\n height: 100%;\n }\n\n >.panel-body {\n height: 100%;\n overflow-y: hidden;\n\n .btn {\n min-height: 2em;\n }\n\n .btn:not(.dropdown-button) {\n padding: 0 2em;\n }\n }\n }\n\n .settings-footer {\n display: flex;\n flex-wrap: wrap;\n line-height: 2;\n\n >* {\n margin-right: 0.5em;\n }\n\n .extra-content {\n display: flex;\n flex-grow: 1;\n }\n }\n\n &.peek {\n .settings-modal-panel {\n /* Explanation:\n * Modal is positioned vertically centered.\n * 100vh - 100% = Distance between modal's top+bottom boundaries and screen\n * (100vh - 100%) / 2 = Distance between bottom (or top) boundary and screen\n * + 100% - we move modal completely off-screen, it's top boundary touches\n * bottom of the screen\n * - 50px - leaving tiny amount of space so that titlebar + tiny amount of modal is visible\n */\n transform: translateY(calc(((100vh - 100%) / 2 + 100%) - 50px));\n\n @media all and (max-width: 800px) {\n /* For mobile, the modal takes 100% of the available screen.\n This ensures the minimized modal is always 50px above the browser bottom\n bar regardless of whether or not it is visible.\n */\n transform: translateY(calc(100% - 50px));\n }\n }\n }\n}\n"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/priv/static/static/css/7962.76663e78ad5ea0bb0b90.css b/priv/static/static/css/7962.76663e78ad5ea0bb0b90.css new file mode 100644 index 0000000000000000000000000000000000000000..2326ed9323c38b12859426c3d656dc350acc46fd GIT binary patch literal 19677 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 literal 0 HcmV?d00001 diff --git a/priv/static/static/css/7962.76663e78ad5ea0bb0b90.css.map b/priv/static/static/css/7962.76663e78ad5ea0bb0b90.css.map new file mode 100644 index 000000000..9d501f27a --- /dev/null +++ b/priv/static/static/css/7962.76663e78ad5ea0bb0b90.css.map @@ -0,0 +1 @@ +{"version":3,"file":"static/css/7962.76663e78ad5ea0bb0b90.css","mappings":"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","sources":["webpack://pleroma_fe/./src/components/importer/importer.vue","webpack://pleroma_fe/./src/components/exporter/exporter.vue","webpack://pleroma_fe/./src/components/autosuggest/autosuggest.vue","webpack://pleroma_fe/./src/_variables.scss","webpack://pleroma_fe/./src/components/block_card/block_card.vue","webpack://pleroma_fe/./src/components/mute_card/mute_card.vue","webpack://pleroma_fe/./src/components/domain_mute_card/domain_mute_card.vue","webpack://pleroma_fe/./src/components/selectable_list/selectable_list.vue","webpack://pleroma_fe/./src/hocs/with_subscription/with_subscription.scss","webpack://pleroma_fe/./src/components/settings_modal/tabs/mutes_and_blocks_tab.scss","webpack://pleroma_fe/./src/components/settings_modal/helpers/modified_indicator.vue","webpack://pleroma_fe/./src/components/settings_modal/helpers/profile_setting_indicator.vue","webpack://pleroma_fe/./src/components/settings_modal/helpers/draft_buttons.vue","webpack://pleroma_fe/./src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue","webpack://pleroma_fe/./src/components/settings_modal/tabs/security_tab/mfa.vue","webpack://pleroma_fe/./node_modules/cropperjs/dist/cropper.css","webpack://pleroma_fe/./src/components/image_cropper/image_cropper.vue","webpack://pleroma_fe/./src/components/settings_modal/tabs/profile_tab.scss","webpack://pleroma_fe/./src/components/settings_modal/helpers/size_setting.vue","webpack://pleroma_fe/./src/components/settings_modal/tabs/general_tab.vue","webpack://pleroma_fe/./src/components/color_input/color_input.scss","webpack://pleroma_fe/./src/components/color_input/color_input.vue","webpack://pleroma_fe/./src/components/shadow_control/shadow_control.vue","webpack://pleroma_fe/./src/components/font_control/font_control.vue","webpack://pleroma_fe/./src/components/contrast_ratio/contrast_ratio.vue","webpack://pleroma_fe/./src/components/settings_modal/tabs/theme_tab/preview.vue","webpack://pleroma_fe/./src/components/settings_modal/tabs/theme_tab/theme_tab.scss","webpack://pleroma_fe/./src/components/settings_modal/settings_modal_user_content.scss"],"sourcesContent":["\n.importer {\n &-uploading {\n font-size: 1.5em;\n margin: 0.25em;\n }\n}\n","\n.exporter {\n &-processing {\n margin: 0.25em;\n }\n}\n","\n@import \"../../variables\";\n\n.autosuggest {\n position: relative;\n\n &-input {\n display: block;\n width: 100%;\n }\n\n &-results {\n position: absolute;\n left: 0;\n top: 100%;\n right: 0;\n max-height: 400px;\n background-color: $fallback--bg;\n background-color: var(--bg, $fallback--bg);\n border-style: solid;\n border-width: 1px;\n border-color: $fallback--border;\n border-color: var(--border, $fallback--border);\n border-radius: $fallback--inputRadius;\n border-radius: var(--inputRadius, $fallback--inputRadius);\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n box-shadow: 1px 1px 4px rgb(0 0 0 / 60%);\n box-shadow: var(--panelShadow);\n overflow-y: auto;\n z-index: 1;\n }\n}\n","$main-color: #f58d2c;\n$main-background: white;\n$darkened-background: whitesmoke;\n\n$fallback--bg: #121a24;\n$fallback--fg: #182230;\n$fallback--faint: rgb(185 185 186 / 50%);\n$fallback--text: #b9b9ba;\n$fallback--link: #d8a070;\n$fallback--icon: #666;\n$fallback--lightBg: rgb(21 30 42);\n$fallback--lightText: #b9b9ba;\n$fallback--border: #222;\n$fallback--cRed: #f00;\n$fallback--cBlue: #0095ff;\n$fallback--cGreen: #0fa00f;\n$fallback--cOrange: orange;\n\n$fallback--alertError: rgb(211 16 20 / 50%);\n$fallback--alertWarning: rgb(111 111 20 / 50%);\n\n$fallback--panelRadius: 10px;\n$fallback--checkboxRadius: 2px;\n$fallback--btnRadius: 4px;\n$fallback--inputRadius: 4px;\n$fallback--tooltipRadius: 5px;\n$fallback--avatarRadius: 4px;\n$fallback--avatarAltRadius: 10px;\n$fallback--attachmentRadius: 10px;\n$fallback--chatMessageRadius: 10px;\n\n$fallback--buttonShadow: 0 0 2px 0 rgb(0 0 0 / 100%),\n 0 1px 0 0 rgb(255 255 255 / 20%) inset,\n 0 -1px 0 0 rgb(0 0 0 / 20%) inset;\n\n$status-margin: 0.75em;\n","\n.block-card-content-container {\n margin-top: 0.5em;\n text-align: right;\n\n button {\n width: 10em;\n }\n}\n","\n.mute-card-content-container {\n margin-top: 0.5em;\n text-align: right;\n\n button {\n width: 10em;\n }\n}\n","\n.domain-mute-card {\n flex: 1 0;\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 0.6em 1em 0.6em 0;\n\n &-domain {\n margin-right: 1em;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n button {\n width: 10em;\n }\n\n .autosuggest-results & {\n padding-left: 1em;\n }\n}\n","\n@import \"../../variables\";\n\n.selectable-list {\n &-item-inner {\n display: flex;\n align-items: center;\n\n > * {\n min-width: 0;\n }\n }\n\n &-item-selected-inner {\n background-color: $fallback--lightBg;\n background-color: var(--selectedMenu, $fallback--lightBg);\n color: var(--selectedMenuText, $fallback--text);\n\n --faint: var(--selectedMenuFaintText, $fallback--faint);\n --faintLink: var(--selectedMenuFaintLink, $fallback--faint);\n --lightText: var(--selectedMenuLightText, $fallback--lightText);\n --icon: var(--selectedMenuIcon, $fallback--icon);\n }\n\n &-header {\n display: flex;\n align-items: center;\n padding: 0.6em 0;\n border-bottom: 2px solid;\n border-bottom-color: $fallback--border;\n border-bottom-color: var(--border, $fallback--border);\n\n &-actions {\n flex: 1;\n }\n }\n\n &-checkbox-wrapper {\n padding: 0 10px;\n flex: none;\n }\n}\n",".with-subscription {\n &-loading {\n padding: 10px;\n text-align: center;\n\n .error {\n font-size: 1rem;\n }\n }\n}\n",".mutes-and-blocks-tab {\n height: 100%;\n\n .usersearch-wrapper {\n padding: 1em;\n }\n\n .bulk-actions {\n text-align: right;\n padding: 0 1em;\n min-height: 2em;\n }\n\n .bulk-action-button {\n width: 10em;\n }\n\n .domain-mute-form {\n padding: 1em;\n display: flex;\n flex-direction: column;\n }\n\n .domain-mute-button {\n align-self: flex-end;\n margin-top: 1em;\n width: 10em;\n }\n}\n","\n.ModifiedIndicator {\n display: inline-block;\n position: relative;\n}\n\n.modified-tooltip {\n margin: 0.5em 1em;\n min-width: 10em;\n text-align: center;\n}\n","\n.ProfileSettingIndicator {\n display: inline-block;\n position: relative;\n}\n\n.profilesetting-tooltip {\n margin: 0.5em 1em;\n min-width: 10em;\n text-align: center;\n}\n","\n.DraftButtons {\n display: inline-block;\n position: relative;\n\n .button-default {\n margin-left: 0.5em;\n }\n}\n\n.draft-tooltip {\n margin: 0.5em 1em;\n min-width: 10em;\n text-align: center;\n}\n","\n@import \"../../../../variables\";\n\n.mfa-backup-codes {\n .warning {\n color: $fallback--cOrange;\n color: var(--cOrange, $fallback--cOrange);\n }\n\n .backup-codes {\n font-family: var(--postCodeFont, monospace);\n }\n}\n","\n@import \"../../../../variables\";\n\n.mfa-settings {\n .mfa-heading,\n .method-item {\n display: flex;\n flex-wrap: wrap;\n justify-content: space-between;\n align-items: baseline;\n }\n\n .warning {\n color: $fallback--cOrange;\n color: var(--cOrange, $fallback--cOrange);\n }\n\n .setup-otp {\n display: flex;\n justify-content: center;\n flex-wrap: wrap;\n\n .qr-code {\n flex: 1;\n padding-right: 10px;\n }\n .verify { flex: 1; }\n .error { margin: 4px 0 0; }\n\n .confirm-otp-actions {\n button {\n width: 15em;\n margin-top: 5px;\n }\n }\n }\n}\n","/*!\n * Cropper.js v1.5.13\n * https://fengyuanchen.github.io/cropperjs\n *\n * Copyright 2015-present Chen Fengyuan\n * Released under the MIT license\n *\n * Date: 2022-11-20T05:30:43.444Z\n */\n\n.cropper-container {\n direction: ltr;\n font-size: 0;\n line-height: 0;\n position: relative;\n -ms-touch-action: none;\n touch-action: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n\n.cropper-container img {\n -webkit-backface-visibility: hidden;\n backface-visibility: hidden;\n display: block;\n height: 100%;\n image-orientation: 0deg;\n max-height: none !important;\n max-width: none !important;\n min-height: 0 !important;\n min-width: 0 !important;\n width: 100%;\n }\n\n.cropper-wrap-box,\n.cropper-canvas,\n.cropper-drag-box,\n.cropper-crop-box,\n.cropper-modal {\n bottom: 0;\n left: 0;\n position: absolute;\n right: 0;\n top: 0;\n}\n\n.cropper-wrap-box,\n.cropper-canvas {\n overflow: hidden;\n}\n\n.cropper-drag-box {\n background-color: #fff;\n opacity: 0;\n}\n\n.cropper-modal {\n background-color: #000;\n opacity: 0.5;\n}\n\n.cropper-view-box {\n display: block;\n height: 100%;\n outline: 1px solid #39f;\n outline-color: rgba(51, 153, 255, 75%);\n overflow: hidden;\n width: 100%;\n}\n\n.cropper-dashed {\n border: 0 dashed #eee;\n display: block;\n opacity: 0.5;\n position: absolute;\n}\n\n.cropper-dashed.dashed-h {\n border-bottom-width: 1px;\n border-top-width: 1px;\n height: calc(100% / 3);\n left: 0;\n top: calc(100% / 3);\n width: 100%;\n }\n\n.cropper-dashed.dashed-v {\n border-left-width: 1px;\n border-right-width: 1px;\n height: 100%;\n left: calc(100% / 3);\n top: 0;\n width: calc(100% / 3);\n }\n\n.cropper-center {\n display: block;\n height: 0;\n left: 50%;\n opacity: 0.75;\n position: absolute;\n top: 50%;\n width: 0;\n}\n\n.cropper-center::before,\n .cropper-center::after {\n background-color: #eee;\n content: \" \";\n display: block;\n position: absolute;\n }\n\n.cropper-center::before {\n height: 1px;\n left: -3px;\n top: 0;\n width: 7px;\n }\n\n.cropper-center::after {\n height: 7px;\n left: 0;\n top: -3px;\n width: 1px;\n }\n\n.cropper-face,\n.cropper-line,\n.cropper-point {\n display: block;\n height: 100%;\n opacity: 0.1;\n position: absolute;\n width: 100%;\n}\n\n.cropper-face {\n background-color: #fff;\n left: 0;\n top: 0;\n}\n\n.cropper-line {\n background-color: #39f;\n}\n\n.cropper-line.line-e {\n cursor: ew-resize;\n right: -3px;\n top: 0;\n width: 5px;\n }\n\n.cropper-line.line-n {\n cursor: ns-resize;\n height: 5px;\n left: 0;\n top: -3px;\n }\n\n.cropper-line.line-w {\n cursor: ew-resize;\n left: -3px;\n top: 0;\n width: 5px;\n }\n\n.cropper-line.line-s {\n bottom: -3px;\n cursor: ns-resize;\n height: 5px;\n left: 0;\n }\n\n.cropper-point {\n background-color: #39f;\n height: 5px;\n opacity: 0.75;\n width: 5px;\n}\n\n.cropper-point.point-e {\n cursor: ew-resize;\n margin-top: -3px;\n right: -3px;\n top: 50%;\n }\n\n.cropper-point.point-n {\n cursor: ns-resize;\n left: 50%;\n margin-left: -3px;\n top: -3px;\n }\n\n.cropper-point.point-w {\n cursor: ew-resize;\n left: -3px;\n margin-top: -3px;\n top: 50%;\n }\n\n.cropper-point.point-s {\n bottom: -3px;\n cursor: s-resize;\n left: 50%;\n margin-left: -3px;\n }\n\n.cropper-point.point-ne {\n cursor: nesw-resize;\n right: -3px;\n top: -3px;\n }\n\n.cropper-point.point-nw {\n cursor: nwse-resize;\n left: -3px;\n top: -3px;\n }\n\n.cropper-point.point-sw {\n bottom: -3px;\n cursor: nesw-resize;\n left: -3px;\n }\n\n.cropper-point.point-se {\n bottom: -3px;\n cursor: nwse-resize;\n height: 20px;\n opacity: 1;\n right: -3px;\n width: 20px;\n }\n\n@media (min-width: 768px) {\n\n.cropper-point.point-se {\n height: 15px;\n width: 15px;\n }\n }\n\n@media (min-width: 992px) {\n\n.cropper-point.point-se {\n height: 10px;\n width: 10px;\n }\n }\n\n@media (min-width: 1200px) {\n\n.cropper-point.point-se {\n height: 5px;\n opacity: 0.75;\n width: 5px;\n }\n }\n\n.cropper-point.point-se::before {\n background-color: #39f;\n bottom: -50%;\n content: \" \";\n display: block;\n height: 200%;\n opacity: 0;\n position: absolute;\n right: -50%;\n width: 200%;\n }\n\n.cropper-invisible {\n opacity: 0;\n}\n\n.cropper-bg {\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC\");\n}\n\n.cropper-hide {\n display: block;\n height: 0;\n position: absolute;\n width: 0;\n}\n\n.cropper-hidden {\n display: none !important;\n}\n\n.cropper-move {\n cursor: move;\n}\n\n.cropper-crop {\n cursor: crosshair;\n}\n\n.cropper-disabled .cropper-drag-box,\n.cropper-disabled .cropper-face,\n.cropper-disabled .cropper-line,\n.cropper-disabled .cropper-point {\n cursor: not-allowed;\n}\n","\n.image-cropper {\n &-img-input {\n display: none;\n }\n\n &-image-container {\n position: relative;\n\n img {\n display: block;\n max-width: 100%;\n }\n }\n\n &-buttons-wrapper {\n margin-top: 10px;\n\n button {\n margin-top: 5px;\n }\n }\n}\n","@import \"../../../variables\";\n\n.profile-tab {\n .bio {\n margin: 0;\n }\n\n .visibility-tray {\n padding-top: 5px;\n }\n\n input[type=\"file\"] {\n padding: 5px;\n height: auto;\n }\n\n .banner-background-preview {\n max-width: 100%;\n width: 300px;\n position: relative;\n\n img {\n width: 100%;\n }\n }\n\n .uploading {\n font-size: 1.5em;\n margin: 0.25em;\n }\n\n .name-changer {\n width: 100%;\n }\n\n .current-avatar-container {\n position: relative;\n width: 150px;\n height: 150px;\n }\n\n .current-avatar {\n display: block;\n width: 100%;\n height: 100%;\n border-radius: $fallback--avatarRadius;\n border-radius: var(--avatarRadius, $fallback--avatarRadius);\n }\n\n .reset-button {\n position: absolute;\n top: 0.2em;\n right: 0.2em;\n border-radius: $fallback--tooltipRadius;\n border-radius: var(--tooltipRadius, $fallback--tooltipRadius);\n background-color: rgb(0 0 0 / 60%);\n opacity: 0.7;\n width: 1.5em;\n height: 1.5em;\n text-align: center;\n line-height: 1.5em;\n font-size: 1.5em;\n cursor: pointer;\n\n &:hover {\n opacity: 1;\n }\n\n svg {\n color: white;\n }\n }\n\n .oauth-tokens {\n width: 100%;\n\n th {\n text-align: left;\n }\n\n .actions {\n text-align: right;\n }\n }\n\n &-usersearch-wrapper {\n padding: 1em;\n }\n\n &-bulk-actions {\n text-align: right;\n padding: 0 1em;\n min-height: 2em;\n\n button {\n width: 10em;\n }\n }\n\n &-domain-mute-form {\n padding: 1em;\n display: flex;\n flex-direction: column;\n\n button {\n align-self: flex-end;\n margin-top: 1em;\n width: 10em;\n }\n }\n\n .setting-subitem {\n margin-left: 1.75em;\n }\n\n .profile-fields {\n display: flex;\n\n & > .emoji-input {\n flex: 1 1 auto;\n margin: 0 0.2em 0.5em;\n min-width: 0;\n }\n\n .delete-field {\n width: 20px;\n align-self: center;\n margin: 0 0.2em 0.5em;\n padding: 0 0.5em;\n }\n }\n\n .birthday-input {\n display: block;\n margin-bottom: 1em;\n }\n}\n","\n.SizeSetting {\n .number-input {\n max-width: 6.5em;\n }\n\n .css-unit-input,\n .css-unit-input select {\n margin-left: 0.5em;\n width: 4em;\n max-width: 4em;\n min-width: 4em;\n }\n}\n\n","\n.column-settings {\n display: flex;\n justify-content: space-evenly;\n flex-wrap: wrap;\n}\n\n.column-settings .size-label {\n display: block;\n margin-bottom: 0.5em;\n margin-top: 0.5em;\n}\n","@import \"../../variables\";\n\n.color-input {\n display: inline-flex;\n\n &-field.input {\n display: inline-flex;\n flex: 0 0 0;\n max-width: 9em;\n align-items: stretch;\n padding: 0.2em 8px;\n\n input {\n background: none;\n color: $fallback--lightText;\n color: var(--inputText, $fallback--lightText);\n border: none;\n padding: 0;\n margin: 0;\n\n &.textColor {\n flex: 1 0 3em;\n min-width: 3em;\n padding: 0;\n }\n\n &.nativeColor {\n flex: 0 0 2em;\n min-width: 2em;\n align-self: stretch;\n min-height: 100%;\n }\n }\n\n .computedIndicator,\n .transparentIndicator {\n flex: 0 0 2em;\n min-width: 2em;\n align-self: stretch;\n min-height: 100%;\n }\n\n .transparentIndicator {\n // forgot to install counter-strike source, ooops\n background-color: #f0f;\n position: relative;\n\n &::before,\n &::after {\n display: block;\n content: \"\";\n background-color: #000;\n position: absolute;\n height: 50%;\n width: 50%;\n }\n\n &::after {\n top: 0;\n left: 0;\n }\n\n &::before {\n bottom: 0;\n right: 0;\n }\n }\n }\n\n .label {\n flex: 1 1 auto;\n }\n}\n","\n.color-control {\n input.text-input {\n max-width: 7em;\n flex: 1;\n }\n}\n","\n@import \"../../variables\";\n\n.shadow-control {\n display: flex;\n flex-wrap: wrap;\n justify-content: center;\n margin-bottom: 1em;\n\n .shadow-preview-container,\n .shadow-tweak {\n margin: 5px 6px 0 0;\n }\n\n .shadow-preview-container {\n flex: 0;\n display: flex;\n flex-wrap: wrap;\n\n $side: 15em;\n\n input[type=\"number\"] {\n width: 5em;\n min-width: 2em;\n }\n\n .x-shift-control,\n .y-shift-control {\n display: flex;\n flex: 0;\n\n &[disabled=\"disabled\"] * {\n opacity: 0.5;\n }\n }\n\n .x-shift-control {\n align-items: flex-start;\n }\n\n .x-shift-control .wrap,\n input[type=\"range\"] {\n margin: 0;\n width: $side;\n height: 2em;\n }\n\n .y-shift-control {\n flex-direction: column;\n align-items: flex-end;\n\n .wrap {\n width: 2em;\n height: $side;\n }\n\n input[type=\"range\"] {\n transform-origin: 1em 1em;\n transform: rotate(90deg);\n }\n }\n\n .preview-window {\n flex: 1;\n background-color: #999;\n display: flex;\n align-items: center;\n justify-content: center;\n background-image:\n linear-gradient(45deg, #666 25%, transparent 25%),\n linear-gradient(-45deg, #666 25%, transparent 25%),\n linear-gradient(45deg, transparent 75%, #666 75%),\n linear-gradient(-45deg, transparent 75%, #666 75%);\n background-size: 20px 20px;\n background-position: 0 0, 0 10px, 10px -10px, -10px 0;\n border-radius: $fallback--inputRadius;\n border-radius: var(--inputRadius, $fallback--inputRadius);\n\n .preview-block {\n width: 33%;\n height: 33%;\n background-color: $fallback--bg;\n background-color: var(--bg, $fallback--bg);\n border-radius: $fallback--panelRadius;\n border-radius: var(--panelRadius, $fallback--panelRadius);\n }\n }\n }\n\n .shadow-tweak {\n flex: 1;\n min-width: 280px;\n\n .id-control {\n align-items: stretch;\n\n .shadow-switcher {\n flex: 1;\n }\n\n .shadow-switcher,\n .btn {\n min-width: 1px;\n margin-right: 5px;\n }\n\n .btn {\n padding: 0 0.4em;\n margin: 0 0.1em;\n }\n }\n }\n}\n","\n@import \"../../variables\";\n\n.font-control {\n input.custom-font {\n min-width: 10em;\n }\n\n &.custom {\n /* TODO Should make proper joiners... */\n .font-switcher {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n }\n\n .custom-font {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n }\n }\n}\n","\n.contrast-ratio {\n display: flex;\n justify-content: flex-end;\n margin-top: -4px;\n margin-bottom: 5px;\n\n .label {\n margin-right: 1em;\n }\n\n .rating {\n display: inline-block;\n text-align: center;\n margin-left: 0.5em;\n }\n}\n","\n.preview-container {\n position: relative;\n}\n\n.underlay-preview {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 10px;\n right: 10px;\n}\n","@import \"src/variables\";\n\n.theme-tab {\n padding-bottom: 2em;\n\n .preset-switcher {\n margin-right: 1em;\n }\n\n .btn {\n margin-left: 0.25em;\n margin-right: 0.25em;\n }\n\n .style-control {\n display: flex;\n align-items: baseline;\n margin-bottom: 5px;\n\n .label {\n flex: 1;\n }\n\n .opt {\n margin: 0.5em;\n }\n\n .color-input {\n flex: 0 0 0;\n }\n\n input,\n select {\n min-width: 3em;\n margin: 0;\n flex: 0;\n\n &[type=\"number\"] {\n min-width: 5em;\n }\n\n &[type=\"range\"] {\n flex: 1;\n min-width: 3em;\n align-self: flex-start;\n }\n }\n\n &.disabled {\n input,\n select {\n opacity: 0.5;\n }\n }\n }\n\n .reset-container {\n flex-wrap: wrap;\n }\n\n .fonts-container,\n .reset-container,\n .apply-container,\n .radius-container,\n .color-container, {\n display: flex;\n }\n\n .fonts-container,\n .radius-container {\n flex-direction: column;\n }\n\n .color-container {\n > h4 {\n width: 99%;\n }\n\n flex-wrap: wrap;\n justify-content: space-between;\n }\n\n .fonts-container,\n .color-container,\n .shadow-container,\n .radius-container,\n .presets-container {\n margin: 1em 1em 0;\n }\n\n .tab-header {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n width: 100%;\n min-height: 30px;\n margin-bottom: 1em;\n\n p {\n flex: 1;\n margin: 0;\n margin-right: 0.5em;\n }\n }\n\n .tab-header-buttons {\n display: flex;\n flex-direction: column;\n\n .btn {\n min-width: 1px;\n flex: 0 auto;\n padding: 0 1em;\n margin-bottom: 0.5em;\n }\n }\n\n .shadow-selector {\n .override {\n flex: 1;\n margin-left: 0.5em;\n }\n\n .select-container {\n margin-top: -4px;\n margin-bottom: -3px;\n }\n }\n\n .save-load,\n .save-load-options {\n display: flex;\n justify-content: center;\n align-items: baseline;\n flex-wrap: wrap;\n\n .presets,\n .import-export {\n margin-bottom: 0.5em;\n }\n\n .import-export {\n display: flex;\n }\n\n .override {\n margin-left: 0.5em;\n }\n }\n\n .save-load-options {\n flex-wrap: wrap;\n margin-top: 0.5em;\n justify-content: center;\n\n .keep-option {\n margin: 0 0.5em 0.5em;\n min-width: 25%;\n }\n }\n\n .preview-container {\n border-top: 1px dashed;\n border-bottom: 1px dashed;\n border-color: $fallback--border;\n border-color: var(--border, $fallback--border);\n margin: 1em 0;\n padding: 1em;\n background-color: var(--wallpaper);\n background-image: var(--body-background-image);\n background-size: cover;\n background-position: 50% 50%;\n\n .dummy {\n .post {\n font-family: var(--postFont);\n display: flex;\n\n .content {\n flex: 1;\n\n h4 {\n margin-bottom: 0.25em;\n }\n\n .icons {\n margin-top: 0.5em;\n display: flex;\n\n i {\n margin-right: 1em;\n }\n }\n }\n }\n\n .after-post {\n margin-top: 1em;\n display: flex;\n align-items: center;\n }\n\n .avatar,\n .avatar-alt {\n background:\n linear-gradient(\n 135deg,\n #b8e1fc 0%,\n #a9d2f3 10%,\n #90bae4 25%,\n #90bcea 37%,\n #90bff0 50%,\n #6ba8e5 51%,\n #a2daf5 83%,\n #bdf3fd 100%\n );\n color: black;\n font-family: sans-serif;\n text-align: center;\n margin-right: 1em;\n }\n\n .avatar-alt {\n flex: 0 auto;\n margin-left: 28px;\n font-size: 12px;\n min-width: 20px;\n min-height: 20px;\n line-height: 20px;\n border-radius: $fallback--avatarAltRadius;\n border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);\n }\n\n .avatar {\n flex: 0 auto;\n width: 48px;\n height: 48px;\n font-size: 14px;\n line-height: 48px;\n }\n\n .actions {\n display: flex;\n align-items: baseline;\n\n .checkbox {\n display: inline-flex;\n align-items: baseline;\n margin-right: 1em;\n flex: 1;\n }\n }\n\n .separator {\n margin: 1em;\n border-bottom: 1px solid;\n border-color: $fallback--border;\n border-color: var(--border, $fallback--border);\n }\n\n .btn {\n min-width: 3em;\n }\n }\n }\n\n .radius-item {\n flex-basis: auto;\n }\n\n .radius-item,\n .color-item {\n min-width: 20em;\n margin: 5px 6px 0 0;\n display: flex;\n flex-direction: column;\n flex: 1 1 0;\n\n &.wide {\n min-width: 60%;\n }\n\n &:not(.wide):nth-child(2n+1) {\n margin-right: 7px;\n }\n\n .color,\n .opacity {\n display: flex;\n align-items: baseline;\n }\n }\n\n .theme-radius-rn,\n .theme-color-cl {\n border: 0;\n box-shadow: none;\n background: transparent;\n color: var(--faint, $fallback--faint);\n align-self: stretch;\n }\n\n .theme-color-cl,\n .theme-radius-in,\n .theme-color-in {\n margin-left: 4px;\n }\n\n .theme-radius-in {\n min-width: 1em;\n max-width: 7em;\n flex: 1;\n }\n\n .theme-radius-lb {\n max-width: 50em;\n }\n\n .theme-preview-content {\n padding: 20px;\n }\n\n .theme-warning {\n display: flex;\n align-items: baseline;\n margin-bottom: 0.5em;\n\n .buttons {\n .btn {\n margin-bottom: 0.5em;\n }\n }\n }\n}\n\n.extra-content {\n .apply-container {\n display: flex;\n flex-direction: row;\n justify-content: space-around;\n flex-grow: 1;\n\n /* stylelint-disable-next-line no-descending-specificity */\n .btn {\n flex-grow: 1;\n min-height: 2em;\n min-width: 0;\n max-width: 10em;\n padding: 0;\n }\n }\n}\n","@import \"src/variables\";\n\n.settings_tab-switcher {\n height: 100%;\n\n .setting-item {\n border-bottom: 2px solid var(--fg, $fallback--fg);\n margin: 1em 1em 1.4em;\n padding-bottom: 1.4em;\n\n > div,\n > label {\n display: block;\n margin-bottom: 0.5em;\n\n &:last-child {\n margin-bottom: 0;\n }\n }\n\n .select-multiple {\n display: flex;\n\n .option-list {\n margin: 0;\n padding-left: 0.5em;\n }\n }\n\n &:last-child {\n border-bottom: none;\n padding-bottom: 0;\n margin-bottom: 1em;\n }\n\n select {\n min-width: 10em;\n }\n\n textarea {\n width: 100%;\n max-width: 100%;\n height: 100px;\n }\n\n .unavailable,\n .unavailable svg {\n color: var(--cRed, $fallback--cRed);\n color: $fallback--cRed;\n }\n }\n}\n"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/priv/static/static/css/9114.8def3b2b7fe70b3b3712.css b/priv/static/static/css/9114.8def3b2b7fe70b3b3712.css deleted file mode 100644 index 6c25e903087e00fc72c45b4dd1111867c9c00c2f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19517 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 * {\n min-width: 0;\n }\n }\n\n &-item-selected-inner {\n background-color: $fallback--lightBg;\n background-color: var(--selectedMenu, $fallback--lightBg);\n color: var(--selectedMenuText, $fallback--text);\n\n --faint: var(--selectedMenuFaintText, $fallback--faint);\n --faintLink: var(--selectedMenuFaintLink, $fallback--faint);\n --lightText: var(--selectedMenuLightText, $fallback--lightText);\n --icon: var(--selectedMenuIcon, $fallback--icon);\n }\n\n &-header {\n display: flex;\n align-items: center;\n padding: 0.6em 0;\n border-bottom: 2px solid;\n border-bottom-color: $fallback--border;\n border-bottom-color: var(--border, $fallback--border);\n\n &-actions {\n flex: 1;\n }\n }\n\n &-checkbox-wrapper {\n padding: 0 10px;\n flex: none;\n }\n}\n",".with-subscription {\n &-loading {\n padding: 10px;\n text-align: center;\n\n .error {\n font-size: 1rem;\n }\n }\n}\n",".mutes-and-blocks-tab {\n height: 100%;\n\n .usersearch-wrapper {\n padding: 1em;\n }\n\n .bulk-actions {\n text-align: right;\n padding: 0 1em;\n min-height: 2em;\n }\n\n .bulk-action-button {\n width: 10em;\n }\n\n .domain-mute-form {\n padding: 1em;\n display: flex;\n flex-direction: column;\n }\n\n .domain-mute-button {\n align-self: flex-end;\n margin-top: 1em;\n width: 10em;\n }\n}\n","\n.ModifiedIndicator {\n display: inline-block;\n position: relative;\n}\n\n.modified-tooltip {\n margin: 0.5em 1em;\n min-width: 10em;\n text-align: center;\n}\n","\n.ServerSideIndicator {\n display: inline-block;\n position: relative;\n}\n\n.serverside-tooltip {\n margin: 0.5em 1em;\n min-width: 10em;\n text-align: center;\n}\n","\n@import \"../../../../variables\";\n\n.mfa-backup-codes {\n .warning {\n color: $fallback--cOrange;\n color: var(--cOrange, $fallback--cOrange);\n }\n\n .backup-codes {\n font-family: var(--postCodeFont, monospace);\n }\n}\n","\n@import \"../../../../variables\";\n\n.mfa-settings {\n .mfa-heading,\n .method-item {\n display: flex;\n flex-wrap: wrap;\n justify-content: space-between;\n align-items: baseline;\n }\n\n .warning {\n color: $fallback--cOrange;\n color: var(--cOrange, $fallback--cOrange);\n }\n\n .setup-otp {\n display: flex;\n justify-content: center;\n flex-wrap: wrap;\n\n .qr-code {\n flex: 1;\n padding-right: 10px;\n }\n .verify { flex: 1; }\n .error { margin: 4px 0 0; }\n\n .confirm-otp-actions {\n button {\n width: 15em;\n margin-top: 5px;\n }\n }\n }\n}\n","/*!\n * Cropper.js v1.5.12\n * https://fengyuanchen.github.io/cropperjs\n *\n * Copyright 2015-present Chen Fengyuan\n * Released under the MIT license\n *\n * Date: 2021-06-12T08:00:11.623Z\n */\n\n.cropper-container {\n direction: ltr;\n font-size: 0;\n line-height: 0;\n position: relative;\n -ms-touch-action: none;\n touch-action: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n\n.cropper-container img {\n display: block;\n height: 100%;\n image-orientation: 0deg;\n max-height: none !important;\n max-width: none !important;\n min-height: 0 !important;\n min-width: 0 !important;\n width: 100%;\n}\n\n.cropper-wrap-box,\n.cropper-canvas,\n.cropper-drag-box,\n.cropper-crop-box,\n.cropper-modal {\n bottom: 0;\n left: 0;\n position: absolute;\n right: 0;\n top: 0;\n}\n\n.cropper-wrap-box,\n.cropper-canvas {\n overflow: hidden;\n}\n\n.cropper-drag-box {\n background-color: #fff;\n opacity: 0;\n}\n\n.cropper-modal {\n background-color: #000;\n opacity: 0.5;\n}\n\n.cropper-view-box {\n display: block;\n height: 100%;\n outline: 1px solid #39f;\n outline-color: rgba(51, 153, 255, 0.75);\n overflow: hidden;\n width: 100%;\n}\n\n.cropper-dashed {\n border: 0 dashed #eee;\n display: block;\n opacity: 0.5;\n position: absolute;\n}\n\n.cropper-dashed.dashed-h {\n border-bottom-width: 1px;\n border-top-width: 1px;\n height: calc(100% / 3);\n left: 0;\n top: calc(100% / 3);\n width: 100%;\n}\n\n.cropper-dashed.dashed-v {\n border-left-width: 1px;\n border-right-width: 1px;\n height: 100%;\n left: calc(100% / 3);\n top: 0;\n width: calc(100% / 3);\n}\n\n.cropper-center {\n display: block;\n height: 0;\n left: 50%;\n opacity: 0.75;\n position: absolute;\n top: 50%;\n width: 0;\n}\n\n.cropper-center::before,\n.cropper-center::after {\n background-color: #eee;\n content: ' ';\n display: block;\n position: absolute;\n}\n\n.cropper-center::before {\n height: 1px;\n left: -3px;\n top: 0;\n width: 7px;\n}\n\n.cropper-center::after {\n height: 7px;\n left: 0;\n top: -3px;\n width: 1px;\n}\n\n.cropper-face,\n.cropper-line,\n.cropper-point {\n display: block;\n height: 100%;\n opacity: 0.1;\n position: absolute;\n width: 100%;\n}\n\n.cropper-face {\n background-color: #fff;\n left: 0;\n top: 0;\n}\n\n.cropper-line {\n background-color: #39f;\n}\n\n.cropper-line.line-e {\n cursor: ew-resize;\n right: -3px;\n top: 0;\n width: 5px;\n}\n\n.cropper-line.line-n {\n cursor: ns-resize;\n height: 5px;\n left: 0;\n top: -3px;\n}\n\n.cropper-line.line-w {\n cursor: ew-resize;\n left: -3px;\n top: 0;\n width: 5px;\n}\n\n.cropper-line.line-s {\n bottom: -3px;\n cursor: ns-resize;\n height: 5px;\n left: 0;\n}\n\n.cropper-point {\n background-color: #39f;\n height: 5px;\n opacity: 0.75;\n width: 5px;\n}\n\n.cropper-point.point-e {\n cursor: ew-resize;\n margin-top: -3px;\n right: -3px;\n top: 50%;\n}\n\n.cropper-point.point-n {\n cursor: ns-resize;\n left: 50%;\n margin-left: -3px;\n top: -3px;\n}\n\n.cropper-point.point-w {\n cursor: ew-resize;\n left: -3px;\n margin-top: -3px;\n top: 50%;\n}\n\n.cropper-point.point-s {\n bottom: -3px;\n cursor: s-resize;\n left: 50%;\n margin-left: -3px;\n}\n\n.cropper-point.point-ne {\n cursor: nesw-resize;\n right: -3px;\n top: -3px;\n}\n\n.cropper-point.point-nw {\n cursor: nwse-resize;\n left: -3px;\n top: -3px;\n}\n\n.cropper-point.point-sw {\n bottom: -3px;\n cursor: nesw-resize;\n left: -3px;\n}\n\n.cropper-point.point-se {\n bottom: -3px;\n cursor: nwse-resize;\n height: 20px;\n opacity: 1;\n right: -3px;\n width: 20px;\n}\n\n@media (min-width: 768px) {\n .cropper-point.point-se {\n height: 15px;\n width: 15px;\n }\n}\n\n@media (min-width: 992px) {\n .cropper-point.point-se {\n height: 10px;\n width: 10px;\n }\n}\n\n@media (min-width: 1200px) {\n .cropper-point.point-se {\n height: 5px;\n opacity: 0.75;\n width: 5px;\n }\n}\n\n.cropper-point.point-se::before {\n background-color: #39f;\n bottom: -50%;\n content: ' ';\n display: block;\n height: 200%;\n opacity: 0;\n position: absolute;\n right: -50%;\n width: 200%;\n}\n\n.cropper-invisible {\n opacity: 0;\n}\n\n.cropper-bg {\n background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC');\n}\n\n.cropper-hide {\n display: block;\n height: 0;\n position: absolute;\n width: 0;\n}\n\n.cropper-hidden {\n display: none !important;\n}\n\n.cropper-move {\n cursor: move;\n}\n\n.cropper-crop {\n cursor: crosshair;\n}\n\n.cropper-disabled .cropper-drag-box,\n.cropper-disabled .cropper-face,\n.cropper-disabled .cropper-line,\n.cropper-disabled .cropper-point {\n cursor: not-allowed;\n}\n","\n.image-cropper {\n &-img-input {\n display: none;\n }\n\n &-image-container {\n position: relative;\n\n img {\n display: block;\n max-width: 100%;\n }\n }\n\n &-buttons-wrapper {\n margin-top: 10px;\n\n button {\n margin-top: 5px;\n }\n }\n}\n","@import \"../../../variables\";\n\n.profile-tab {\n .bio {\n margin: 0;\n }\n\n .visibility-tray {\n padding-top: 5px;\n }\n\n input[type=\"file\"] {\n padding: 5px;\n height: auto;\n }\n\n .banner-background-preview {\n max-width: 100%;\n width: 300px;\n position: relative;\n\n img {\n width: 100%;\n }\n }\n\n .uploading {\n font-size: 1.5em;\n margin: 0.25em;\n }\n\n .name-changer {\n width: 100%;\n }\n\n .current-avatar-container {\n position: relative;\n width: 150px;\n height: 150px;\n }\n\n .current-avatar {\n display: block;\n width: 100%;\n height: 100%;\n border-radius: $fallback--avatarRadius;\n border-radius: var(--avatarRadius, $fallback--avatarRadius);\n }\n\n .reset-button {\n position: absolute;\n top: 0.2em;\n right: 0.2em;\n border-radius: $fallback--tooltipRadius;\n border-radius: var(--tooltipRadius, $fallback--tooltipRadius);\n background-color: rgb(0 0 0 / 60%);\n opacity: 0.7;\n width: 1.5em;\n height: 1.5em;\n text-align: center;\n line-height: 1.5em;\n font-size: 1.5em;\n cursor: pointer;\n\n &:hover {\n opacity: 1;\n }\n\n svg {\n color: white;\n }\n }\n\n .oauth-tokens {\n width: 100%;\n\n th {\n text-align: left;\n }\n\n .actions {\n text-align: right;\n }\n }\n\n &-usersearch-wrapper {\n padding: 1em;\n }\n\n &-bulk-actions {\n text-align: right;\n padding: 0 1em;\n min-height: 2em;\n\n button {\n width: 10em;\n }\n }\n\n &-domain-mute-form {\n padding: 1em;\n display: flex;\n flex-direction: column;\n\n button {\n align-self: flex-end;\n margin-top: 1em;\n width: 10em;\n }\n }\n\n .setting-subitem {\n margin-left: 1.75em;\n }\n\n .profile-fields {\n display: flex;\n\n & > .emoji-input {\n flex: 1 1 auto;\n margin: 0 0.2em 0.5em;\n min-width: 0;\n }\n\n .delete-field {\n width: 20px;\n align-self: center;\n margin: 0 0.2em 0.5em;\n padding: 0 0.5em;\n }\n }\n\n .birthday-input {\n display: block;\n margin-bottom: 1em;\n }\n}\n","\n.css-unit-input,\n.css-unit-input select {\n margin-left: 0.5em;\n width: 4em;\n max-width: 4em;\n min-width: 4em;\n}\n","\n.column-settings {\n display: flex;\n justify-content: space-evenly;\n flex-wrap: wrap;\n}\n\n.column-settings .size-label {\n display: block;\n margin-bottom: 0.5em;\n margin-top: 0.5em;\n}\n","@import \"../../variables\";\n\n.color-input {\n display: inline-flex;\n\n &-field.input {\n display: inline-flex;\n flex: 0 0 0;\n max-width: 9em;\n align-items: stretch;\n padding: 0.2em 8px;\n\n input {\n background: none;\n color: $fallback--lightText;\n color: var(--inputText, $fallback--lightText);\n border: none;\n padding: 0;\n margin: 0;\n\n &.textColor {\n flex: 1 0 3em;\n min-width: 3em;\n padding: 0;\n }\n\n &.nativeColor {\n flex: 0 0 2em;\n min-width: 2em;\n align-self: stretch;\n min-height: 100%;\n }\n }\n\n .computedIndicator,\n .transparentIndicator {\n flex: 0 0 2em;\n min-width: 2em;\n align-self: stretch;\n min-height: 100%;\n }\n\n .transparentIndicator {\n // forgot to install counter-strike source, ooops\n background-color: #f0f;\n position: relative;\n\n &::before,\n &::after {\n display: block;\n content: \"\";\n background-color: #000;\n position: absolute;\n height: 50%;\n width: 50%;\n }\n\n &::after {\n top: 0;\n left: 0;\n }\n\n &::before {\n bottom: 0;\n right: 0;\n }\n }\n }\n\n .label {\n flex: 1 1 auto;\n }\n}\n","\n.color-control {\n input.text-input {\n max-width: 7em;\n flex: 1;\n }\n}\n","\n@import \"../../variables\";\n\n.shadow-control {\n display: flex;\n flex-wrap: wrap;\n justify-content: center;\n margin-bottom: 1em;\n\n .shadow-preview-container,\n .shadow-tweak {\n margin: 5px 6px 0 0;\n }\n\n .shadow-preview-container {\n flex: 0;\n display: flex;\n flex-wrap: wrap;\n\n $side: 15em;\n\n input[type=\"number\"] {\n width: 5em;\n min-width: 2em;\n }\n\n .x-shift-control,\n .y-shift-control {\n display: flex;\n flex: 0;\n\n &[disabled=\"disabled\"] * {\n opacity: 0.5;\n }\n }\n\n .x-shift-control {\n align-items: flex-start;\n }\n\n .x-shift-control .wrap,\n input[type=\"range\"] {\n margin: 0;\n width: $side;\n height: 2em;\n }\n\n .y-shift-control {\n flex-direction: column;\n align-items: flex-end;\n\n .wrap {\n width: 2em;\n height: $side;\n }\n\n input[type=\"range\"] {\n transform-origin: 1em 1em;\n transform: rotate(90deg);\n }\n }\n\n .preview-window {\n flex: 1;\n background-color: #999;\n display: flex;\n align-items: center;\n justify-content: center;\n background-image:\n linear-gradient(45deg, #666 25%, transparent 25%),\n linear-gradient(-45deg, #666 25%, transparent 25%),\n linear-gradient(45deg, transparent 75%, #666 75%),\n linear-gradient(-45deg, transparent 75%, #666 75%);\n background-size: 20px 20px;\n background-position: 0 0, 0 10px, 10px -10px, -10px 0;\n border-radius: $fallback--inputRadius;\n border-radius: var(--inputRadius, $fallback--inputRadius);\n\n .preview-block {\n width: 33%;\n height: 33%;\n background-color: $fallback--bg;\n background-color: var(--bg, $fallback--bg);\n border-radius: $fallback--panelRadius;\n border-radius: var(--panelRadius, $fallback--panelRadius);\n }\n }\n }\n\n .shadow-tweak {\n flex: 1;\n min-width: 280px;\n\n .id-control {\n align-items: stretch;\n\n .shadow-switcher {\n flex: 1;\n }\n\n .shadow-switcher,\n .btn {\n min-width: 1px;\n margin-right: 5px;\n }\n\n .btn {\n padding: 0 0.4em;\n margin: 0 0.1em;\n }\n }\n }\n}\n","\n@import \"../../variables\";\n\n.font-control {\n input.custom-font {\n min-width: 10em;\n }\n\n &.custom {\n /* TODO Should make proper joiners... */\n .font-switcher {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n }\n\n .custom-font {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n }\n }\n}\n","\n.contrast-ratio {\n display: flex;\n justify-content: flex-end;\n margin-top: -4px;\n margin-bottom: 5px;\n\n .label {\n margin-right: 1em;\n }\n\n .rating {\n display: inline-block;\n text-align: center;\n margin-left: 0.5em;\n }\n}\n","\n.preview-container {\n position: relative;\n}\n\n.underlay-preview {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 10px;\n right: 10px;\n}\n","@import \"src/variables\";\n\n.theme-tab {\n padding-bottom: 2em;\n\n .preset-switcher {\n margin-right: 1em;\n }\n\n .btn {\n margin-left: 0.25em;\n margin-right: 0.25em;\n }\n\n .style-control {\n display: flex;\n align-items: baseline;\n margin-bottom: 5px;\n\n .label {\n flex: 1;\n }\n\n .opt {\n margin: 0.5em;\n }\n\n .color-input {\n flex: 0 0 0;\n }\n\n input,\n select {\n min-width: 3em;\n margin: 0;\n flex: 0;\n\n &[type=\"number\"] {\n min-width: 5em;\n }\n\n &[type=\"range\"] {\n flex: 1;\n min-width: 3em;\n align-self: flex-start;\n }\n }\n\n &.disabled {\n input,\n select {\n opacity: 0.5;\n }\n }\n }\n\n .reset-container {\n flex-wrap: wrap;\n }\n\n .fonts-container,\n .reset-container,\n .apply-container,\n .radius-container,\n .color-container, {\n display: flex;\n }\n\n .fonts-container,\n .radius-container {\n flex-direction: column;\n }\n\n .color-container {\n > h4 {\n width: 99%;\n }\n\n flex-wrap: wrap;\n justify-content: space-between;\n }\n\n .fonts-container,\n .color-container,\n .shadow-container,\n .radius-container,\n .presets-container {\n margin: 1em 1em 0;\n }\n\n .tab-header {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n width: 100%;\n min-height: 30px;\n margin-bottom: 1em;\n\n p {\n flex: 1;\n margin: 0;\n margin-right: 0.5em;\n }\n }\n\n .tab-header-buttons {\n display: flex;\n flex-direction: column;\n\n .btn {\n min-width: 1px;\n flex: 0 auto;\n padding: 0 1em;\n margin-bottom: 0.5em;\n }\n }\n\n .shadow-selector {\n .override {\n flex: 1;\n margin-left: 0.5em;\n }\n\n .select-container {\n margin-top: -4px;\n margin-bottom: -3px;\n }\n }\n\n .save-load,\n .save-load-options {\n display: flex;\n justify-content: center;\n align-items: baseline;\n flex-wrap: wrap;\n\n .presets,\n .import-export {\n margin-bottom: 0.5em;\n }\n\n .import-export {\n display: flex;\n }\n\n .override {\n margin-left: 0.5em;\n }\n }\n\n .save-load-options {\n flex-wrap: wrap;\n margin-top: 0.5em;\n justify-content: center;\n\n .keep-option {\n margin: 0 0.5em 0.5em;\n min-width: 25%;\n }\n }\n\n .preview-container {\n border-top: 1px dashed;\n border-bottom: 1px dashed;\n border-color: $fallback--border;\n border-color: var(--border, $fallback--border);\n margin: 1em 0;\n padding: 1em;\n background-color: var(--wallpaper);\n background-image: var(--body-background-image);\n background-size: cover;\n background-position: 50% 50%;\n\n .dummy {\n .post {\n font-family: var(--postFont);\n display: flex;\n\n .content {\n flex: 1;\n\n h4 {\n margin-bottom: 0.25em;\n }\n\n .icons {\n margin-top: 0.5em;\n display: flex;\n\n i {\n margin-right: 1em;\n }\n }\n }\n }\n\n .after-post {\n margin-top: 1em;\n display: flex;\n align-items: center;\n }\n\n .avatar,\n .avatar-alt {\n background:\n linear-gradient(\n 135deg,\n #b8e1fc 0%,\n #a9d2f3 10%,\n #90bae4 25%,\n #90bcea 37%,\n #90bff0 50%,\n #6ba8e5 51%,\n #a2daf5 83%,\n #bdf3fd 100%\n );\n color: black;\n font-family: sans-serif;\n text-align: center;\n margin-right: 1em;\n }\n\n .avatar-alt {\n flex: 0 auto;\n margin-left: 28px;\n font-size: 12px;\n min-width: 20px;\n min-height: 20px;\n line-height: 20px;\n border-radius: $fallback--avatarAltRadius;\n border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);\n }\n\n .avatar {\n flex: 0 auto;\n width: 48px;\n height: 48px;\n font-size: 14px;\n line-height: 48px;\n }\n\n .actions {\n display: flex;\n align-items: baseline;\n\n .checkbox {\n display: inline-flex;\n align-items: baseline;\n margin-right: 1em;\n flex: 1;\n }\n }\n\n .separator {\n margin: 1em;\n border-bottom: 1px solid;\n border-color: $fallback--border;\n border-color: var(--border, $fallback--border);\n }\n\n .btn {\n min-width: 3em;\n }\n }\n }\n\n .radius-item {\n flex-basis: auto;\n }\n\n .radius-item,\n .color-item {\n min-width: 20em;\n margin: 5px 6px 0 0;\n display: flex;\n flex-direction: column;\n flex: 1 1 0;\n\n &.wide {\n min-width: 60%;\n }\n\n &:not(.wide):nth-child(2n+1) {\n margin-right: 7px;\n }\n\n .color,\n .opacity {\n display: flex;\n align-items: baseline;\n }\n }\n\n .theme-radius-rn,\n .theme-color-cl {\n border: 0;\n box-shadow: none;\n background: transparent;\n color: var(--faint, $fallback--faint);\n align-self: stretch;\n }\n\n .theme-color-cl,\n .theme-radius-in,\n .theme-color-in {\n margin-left: 4px;\n }\n\n .theme-radius-in {\n min-width: 1em;\n max-width: 7em;\n flex: 1;\n }\n\n .theme-radius-lb {\n max-width: 50em;\n }\n\n .theme-preview-content {\n padding: 20px;\n }\n\n .theme-warning {\n display: flex;\n align-items: baseline;\n margin-bottom: 0.5em;\n\n .buttons {\n .btn {\n margin-bottom: 0.5em;\n }\n }\n }\n}\n\n.extra-content {\n .apply-container {\n display: flex;\n flex-direction: row;\n justify-content: space-around;\n flex-grow: 1;\n\n /* stylelint-disable-next-line no-descending-specificity */\n .btn {\n flex-grow: 1;\n min-height: 2em;\n min-width: 0;\n max-width: 10em;\n padding: 0;\n }\n }\n}\n","@import \"src/variables\";\n\n.settings_tab-switcher {\n height: 100%;\n\n .setting-item {\n border-bottom: 2px solid var(--fg, $fallback--fg);\n margin: 1em 1em 1.4em;\n padding-bottom: 1.4em;\n\n > div,\n > label {\n display: block;\n margin-bottom: 0.5em;\n\n &:last-child {\n margin-bottom: 0;\n }\n }\n\n .select-multiple {\n display: flex;\n\n .option-list {\n margin: 0;\n padding-left: 0.5em;\n }\n }\n\n &:last-child {\n border-bottom: none;\n padding-bottom: 0;\n margin-bottom: 1em;\n }\n\n select {\n min-width: 10em;\n }\n\n textarea {\n width: 100%;\n max-width: 100%;\n height: 100px;\n }\n\n .unavailable,\n .unavailable svg {\n color: var(--cRed, $fallback--cRed);\n color: $fallback--cRed;\n }\n\n .number-input {\n max-width: 6em;\n }\n }\n}\n"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/priv/static/static/css/9801.cfe503d4c949ae0c3813.css b/priv/static/static/css/9801.cfe503d4c949ae0c3813.css new file mode 100644 index 0000000000000000000000000000000000000000..b27df4a19f6e80a806e1267d2e6ce2fa158028fd GIT binary patch literal 1884 zcmbtU+iKh}5Pk1g7($>4E5+VTVArOBmcA4kC<%QoT4}raG>OBnhm$8v;!dR;lm}bWA6Dlm;R(D)wR<}R)5g0t z{vANRT9*4Sd+)V!vl}oDis<$nO$6j@FMZbxA|dzZj?`2%HQs^ov-gY_WoTX3uZXhY zXaL>HRRHb^pC&8q3AObD$vmv;a`%?8) z&U_p3+Y-wl{EH>HR^Dpq`aX@&y?Q@t+q`%QcAqn9L61r1Hq-+p9Pw-$QO<1ml<0hA zm=M8xXm9jy;wbE%KyloluisMh2CaSBqRapDrFoNyXU7Azmv_ugdnEgmdzFS-* z>*aD4Uv&zlL7ZkfqlDfDnL7G?{P}QJ;d_CHMGs|4Vv6uyvMtELLajfadvbQsR5IrY zFHnj%)E`CtW4R#wj6OG$g1E}s)9_h;xVgRvL$$XY9+)v div,\n > label {\n display: block;\n margin-bottom: 0.5em;\n\n &:last-child {\n margin-bottom: 0;\n }\n }\n\n .select-multiple {\n display: flex;\n\n .option-list {\n margin: 0;\n padding-left: 0.5em;\n }\n }\n\n &:last-child {\n border-bottom: none;\n padding-bottom: 0;\n margin-bottom: 1em;\n }\n\n select {\n min-width: 10em;\n }\n\n textarea {\n width: 100%;\n max-width: 100%;\n height: 100px;\n }\n\n .unavailable,\n .unavailable svg {\n color: var(--cRed, $fallback--cRed);\n color: $fallback--cRed;\n }\n }\n}\n","$main-color: #f58d2c;\n$main-background: white;\n$darkened-background: whitesmoke;\n\n$fallback--bg: #121a24;\n$fallback--fg: #182230;\n$fallback--faint: rgb(185 185 186 / 50%);\n$fallback--text: #b9b9ba;\n$fallback--link: #d8a070;\n$fallback--icon: #666;\n$fallback--lightBg: rgb(21 30 42);\n$fallback--lightText: #b9b9ba;\n$fallback--border: #222;\n$fallback--cRed: #f00;\n$fallback--cBlue: #0095ff;\n$fallback--cGreen: #0fa00f;\n$fallback--cOrange: orange;\n\n$fallback--alertError: rgb(211 16 20 / 50%);\n$fallback--alertWarning: rgb(111 111 20 / 50%);\n\n$fallback--panelRadius: 10px;\n$fallback--checkboxRadius: 2px;\n$fallback--btnRadius: 4px;\n$fallback--inputRadius: 4px;\n$fallback--tooltipRadius: 5px;\n$fallback--avatarRadius: 4px;\n$fallback--avatarAltRadius: 10px;\n$fallback--attachmentRadius: 10px;\n$fallback--chatMessageRadius: 10px;\n\n$fallback--buttonShadow: 0 0 2px 0 rgb(0 0 0 / 100%),\n 0 1px 0 0 rgb(255 255 255 / 20%) inset,\n 0 -1px 0 0 rgb(0 0 0 / 20%) inset;\n\n$status-margin: 0.75em;\n"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/priv/static/static/css/app.48e52505beba5b9ab69b.css b/priv/static/static/css/app.48e52505beba5b9ab69b.css deleted file mode 100644 index ee1ea9cb439dd39f1b70fd7eb159d7381866a9b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 124954 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 F{=a;iI0FCx diff --git a/priv/static/static/css/app.48e52505beba5b9ab69b.css.map b/priv/static/static/css/app.48e52505beba5b9ab69b.css.map deleted file mode 100644 index a87315d2b..000000000 --- a/priv/static/static/css/app.48e52505beba5b9ab69b.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"static/css/app.48e52505beba5b9ab69b.css","mappings":"AACA,YASE,mBAGA,uBACA,uCAPA,SACA,aACA,uBAJA,OAUA,SAAQ,CAJR,cACA,oBATA,eAGA,QAFA,MAFA,wBAaA,CAEA,cACE,oBAGF,6BAEE,gCADA,mBACA,CAGF,iBACE,UAIJ,mCACE,GACE,6BAGF,GACE,iCCrCJ,sBAAsB,iBAAiB,CAAC,yDAAyD,eAAe,CAAC,2DAA2D,eAAe,CAAC,2CAA2C,mBAAW,CAAX,mBAAW,CAAX,YAAY,CAAC,4BAA4B,kBAAY,CAAZ,mBAAY,CAAZ,aAAa,CAAC,oCAAoC,kBAAM,CAAC,6BAAqB,CAArB,qBAAqB,CAA5B,UAAM,CAAN,MAAM,CAAuB,eAAe,CAAC,iBAAiB,CAAC,6DAAqF,MAAM,CAA9B,iBAAiB,CAAC,KAAK,CAAQ,qBAAqB,CAAC,6EAA6E,UAAU,CAAC,+EAA+E,WAAW,CAAC,gFAAgF,UAAU,CAAC,kFAAkF,WAAW,CAAC,kCAA+G,4BAA4B,CAAxC,WAAW,CAAgF,SAAS,CAAC,2EAAxC,aAAa,CAAtF,WAAW,CAAxC,MAAM,CAA8G,eAAe,CAAjD,mBAAmB,CAA7H,iBAAiB,CAAC,KAAK,CAAmB,UAAU,CAArB,UAAkS,CCGlsC,YACE,aACA,sBACA,aAEA,iBACE,eACA,WAGF,sBACE,SAGF,0BAIE,mBAFA,aACA,mBAEA,8BAJA,cAIA,CAGF,wBACE,aACA,sBAEA,iBADA,sBACA,CAGF,yBACE,aAEA,YADA,YACA,CAEA,gCACE,WAGF,2BAGE,aAFA,aACA,aACA,CAIJ,mBAGE,uBADA,0BAEA,sCAHA,iBAGA,CCjDJ,cACE,eAEA,iCACE,aCHF,sBAEE,eADA,qBAGA,iBADA,gBAEA,kBAEA,mCACE,aCDgB,CDEhB,+BEbN,UAKE,oBACA,kBAFF,iBAGE,qBAGE,mBADF,iBAEE,4BAeA,wBDrBW,sCCuBX,CANA,iBDAuB,wCCEvB,8BACA,8BACA,CAQA,sBAFA,iBACA,CAfA,WACA,CAFA,aACA,CAaA,eACA,CAXA,YACA,CAQA,iBACA,CAEA,eACA,CApBF,iBACE,QACA,CAaA,iBACA,CAdA,KACA,CAEA,oBACA,CAQA,kBACA,CATA,WAeA,yEAIA,UAEE,2BAGF,yBDtCc,uCCwCZ,mEAKF,aD5Ca,+BC8CX,yEAIA,aDlDW,gCCiDb,WAGE,gBAIJ,gBACE,CChEJ,wBAGA,oBACE,UAOA,qCACA,+BAFA,4BACA,CAFA,WACA,CAFA,cACA,CAFF,qDAME,kBAsBA,gDAEA,qDACA,yDACA,kDACA,4DACA,2CAVA,wBF3Ba,wCE6Bb,CAjBF,iBFOsB,mCEQpB,CAEA,aF1Be,iCEmCf,wBAtBE,QACA,CAGA,qCACA,8BACA,CATF,UACE,CAGA,MACA,CAIA,oBARA,iBACA,CAGA,OACA,CAJA,KACA,CAGA,SAIA,gBAkBJ,aACE,CACA,aACA,CACA,eACA,gBACA,CALA,eACA,CACA,eACA,CAGA,mBADA,qDAEA,kCAKE,yBACA,yCAJF,QACE,eACA,gBAGA,+BAkBA,6CALA,4BACA,CAHA,WACA,gBACA,CACA,eACA,CAEA,qBACA,CAXA,UACA,CAHA,aACA,CAEA,eACA,CAOA,WACA,CAdF,gBACE,gBACA,CACA,kBACA,CAEA,kBACA,mBACA,CAIA,UAKA,wCAKI,kCADA,mBACA,CAFF,UAGE,0DAMA,iBADF,mBAEE,0EAQF,wDAEA,6DACA,iEACA,qEACA,uDATF,wBFvFgB,oDE0Fd,gBAOA,kFAGE,sDADF,yCAGE,8CAaF,wBFxHS,sCE0HT,CAHA,eACA,CAEA,6BACA,8BACA,CAbF,oBACE,CAKA,gBACA,CAMA,mBARA,eACA,CAHA,cACA,gBACA,CAHA,cACA,CAIA,iBACA,CAPA,qBAaA,0EAGE,YADF,gBAEE,qDAGF,oBACE,iFAGE,YADF,aAEE,2GAON,aF9Ia,6BEiJX,qDAGF,wBFjJgB,oDEmJd,cFrJW,6CEuJX,uDAGF,aF3Ja,qCE6JX,sDAGF,aFhKa,oCEkKX,CCtKN,aAKE,mBADA,oBAFA,cACA,gBAFA,iBAIA,CAEA,oBAGE,SACA,OAHA,kBAIA,QAHA,MAOA,yDAGF,qCALE,YACA,yCAFA,UASA,CAIA,6BACE,uCAOA,6BAIA,iBHhBoB,CGiBpB,uCAJA,WAPA,cAQA,cALA,eAEA,UAHA,cAOA,gBARA,kBAGA,SASA,wDADA,SACA,CAGF,mCACE,aAGF,mCACE,uDAGF,0BACE,qDAGF,gCACE,mBCrDN,cAUE,gDAAkD,CAClD,oDAAsD,CACtD,wDAA0D,CAC1D,yCAA0C,CAR1C,wBJRa,CISb,wCACA,aJNe,CIOf,iCALA,aACA,sBAFA,6BADA,UAY2C,CAE3C,2BAGE,mBAFA,oBAKA,WAxBiC,CAoBjC,uBAKA,gBAFA,cAxBgC,CAuBhC,UAtBiC,CA2BjC,wCAGE,YADA,gBADA,eAIA,yCADA,UACA,CAIJ,uDAGE,mBADA,WACA,CAGF,8BACE,aACA,sBAGF,+BAEE,aADA,aACA,CAGF,uBACE,aACA,qBAGF,uBACE,aAEA,cADA,sBAEA,aAGF,0BAEE,aACA,qBAFA,YAGA,gBAGF,+BAIE,8DAHA,aAKA,cADA,gBACA,CAGF,yDAIE,qBADA,aADA,eAEA,CAEA,mEASE,mBAPA,eAMA,aALA,iBAGA,WA5F+B,CA6F/B,eA7F+B,CA2F/B,cA5F8B,CAwF9B,cAGA,UAKA,CAEA,qFACE,WACA,oBAGF,iFACE,wBAEA,yFACE,aJnGY,CIoGZ,+BAMR,8BACE,cAKA,6DACE,aAEA,cADA,sBAEA,aAEA,2EACE,UACA,oBACA,kBAMJ,4BAEE,cADA,WACA,CAEA,kCACE,WAIJ,4BAGE,aAFA,YAMA,+JACE,CADF,uJACE,CAMF,mBACA,kDAHA,8EAVA,iBAGA,cADA,kBAOA,6GALA,+DASA,CAGE,yCACE,wEAGF,4CACE,wEAKN,2BAEE,mBADA,aAEA,eAEA,qBADA,gBACA,CAEA,iCACE,gBAEA,QAAO,CADP,UACA,CAEA,0CACE,aAKN,0BAME,mBAHA,sBAMA,eALA,aAFA,WA9LoB,CAmMpB,uBAFA,gBAjMoB,CAoMpB,WAPA,UAQA,CAEA,sDAGE,gBADA,eADA,wCAEA,CAGF,uDACE,eACA,gBCjNR,aACE,aACA,sBACA,kBAEA,gCAME,eADA,gBAEA,iBAHA,kBAHA,kBAEA,QADA,KAKA,CAEA,wCACE,aLXW,CKYX,0BAIJ,iCAGE,eAFA,kBACA,UACA,CAEA,sCACE,aAIJ,yCAEE,cAGF,+BACE,mBAGF,6BAKE,SAMA,UAJA,OANA,UAOA,gBANA,oBACA,kBAGA,QAFA,KAOA,CAIA,oCAGE,qBADA,8BADA,OAEA,CAMJ,oBACE,kBAGF,mBAIE,uCAFA,eADA,aAIA,YAFA,iBAEA,CAEA,0BAKE,eAHA,YACA,iBAGA,iBAFA,kBAHA,UAKA,CAEA,8BAEE,YACA,yCAFA,UAEA,CAIJ,0BACE,aACA,sBACA,uBACA,qBAEA,uCACE,gBAGF,sCACE,cACA,gBAIJ,+BAKE,4DAA8D,CAC9D,gEAAkE,CAClE,oEAAsE,CACtE,qDAAsD,CAPtD,wBLxGS,CKyGT,oDACA,4CAKuD,CChH7D,aACE,UAEA,oBACE,6DACA,uBACA,YACA,aNJa,CMKb,sCAGA,uBACA,wCACA,cAGA,WACA,iBARA,SACA,qBAIA,WACA,SAEA,CAGF,+BAGE,SAIA,aNxBa,CMyBb,+BAHA,YAIA,cAEA,oBAVA,kBAGA,UAFA,MAIA,aAIA,SACA,CChCJ,WACE,aACA,sBACA,oBAEA,uBACE,sBAEA,kBADA,iBACA,CAGF,wBAEE,qBADA,aAEA,8BACA,oBAGF,4BACE,WAEA,kCAEE,oBACA,WAIJ,0BAGE,mBADA,YAEA,UAGF,6BAEE,aADA,gBAEA,WAGF,sBAEE,aADA,kBACA,CAEA,wCACE,oBAIJ,wBACE,aAEA,uCAEE,iBADA,SACA,CCvDN,OACE,qBAGA,kBAOA,0CARA,YADA,UAgBE,CAPF,oBAIE,mBAEA,qBACA,kBAJA,aAEA,sBAEA,CAGF,cACE,MAGF,cAKE,iBAHA,WACA,gBAFA,kBAGA,kBACA,CAGF,eACE,aACA,oBCpCJ,YAIE,sBAOA,qBTDiB,CSEjB,gCAHA,kBTiB2B,CShB3B,2CATA,oBACA,sBAIA,YADA,cAFA,iBASA,CAEA,gCACE,cACA,YAEA,gBADA,iBACA,CAGF,mCAEE,aADA,WAEA,iBACA,UAEA,qCACE,OAEA,gBAEA,SAGA,gBAJA,aAFA,kBAKA,uBADA,kBAEA,CAGF,2CAME,0BAFA,SAGA,8BALA,OAGA,cAJA,kBAEA,OAIA,CAIJ,+BACE,OACA,YAGF,qLAME,aAGA,YAFA,uBACA,UACA,CAIA,oCAEE,YADA,UACA,CAMF,8IAKE,kBAFA,YACA,yCAFA,UAGA,CAIJ,6BAEE,qBADA,YACA,CAEA,mCAEE,YADA,UACA,CAIJ,mCAGE,mBAFA,aACA,sBAEA,uBACA,iBAGF,uBAKE,0BAHA,eAEA,sBAHA,kBAKA,mCAHA,oBAGA,CAEA,8BACE,SAIJ,gCACE,aAKA,kBADA,gBAHA,kBACA,QACA,MAGA,UAEA,mDAUE,6BARA,iBTvGoB,CSwGpB,uCAKA,iBAFA,WACA,iBANA,UAGA,kBACA,SAKA,CAEA,mEACE,qBAGF,yEACE,qBAMJ,6DAEE,yCAKF,yDAEE,qCAIJ,8BAKE,aAHA,cADA,kBAGA,kBADA,UAEA,CAEA,kCACE,WAGF,qCACE,OAEA,yCACE,SACA,kBACA,YACA,qCAIJ,oCACE,OACA,WACA,qBAEA,uCACE,eACA,SAMJ,mCACE,QACA,WAGF,4CACE,QACA,WAIJ,sBACE,aAEA,uFAEE,SAIJ,yBAEE,aTnNa,CSoNb,8BAFA,qBAKA,YACA,gBAHA,gBACA,kBAEA,CAEA,yCACE,YAGF,mCAGE,qBAFA,aACA,kBACA,CAEA,iHAEE,SACA,UACA,kBAGF,0DACE,OACA,kBAGF,uDAEE,kBADA,QACA,CAIJ,2BACE,qBACA,eACA,gBACA,uBAGF,6BACE,cAIJ,qBACE,gBAIA,4CACE,oBC3QJ,uBACE,aACA,sBAGF,sBAIE,WAAU,CAFV,SADA,kBAEA,UACA,CAEA,yCAQE,sBAHA,SACA,aACA,mBAJA,OAFA,kBAGA,QAFA,KAMA,CAEA,uDAIE,sBAFA,YACA,YAFA,kBAKA,cAEA,kEACE,SAIJ,+CAKE,cADA,aAEA,yDAJA,YACA,kBAFA,UAKA,CAEA,6DAEE,aADA,QACA,CAKN,2DAEE,YAEA,iGACE,kBAIJ,wCACE,gBAKF,6BAGE,8GACE,CADF,sGACE,CAIF,mBACA,kDARA,gBACA,eAOA,CAIJ,gCAEE,aAAY,CADZ,iBACA,CAGF,mCACE,aAGF,kCACE,aACA,OACA,uBACA,cAEA,yCACE,cC9FN,QACE,4CAA6C,CAC7C,qDAAsD,CACtD,mDAAoD,CACpD,sCAAuC,CAEvC,qBAGA,YAFA,kBACA,UACA,CAEA,iBAGE,kBXUwB,CWTxB,0CAFA,YADA,UAGA,CAGF,gBAIE,iBXCqB,uCWFrB,mCADA,YADA,UXIqB,CWErB,+BACE,qCACA,kCAGF,iCACE,aAGF,yBACE,kBXXsB,CWYtB,0CAGF,6BACE,wBXtCS,CWuCT,mCAIJ,YAEE,YADA,UACA,CAGF,uBAME,6BAEA,mCANA,SAKA,WAHA,aACA,aAJA,kBAEA,OAKA,CC3DJ,aAIE,kBADA,eAFA,kBACA,mBAGA,kBAEA,yCAGE,kBADA,cACA,CAGF,6BACE,0CAEA,aAGA,kBADA,gEADA,sBAFA,WAIA,CAGF,mBAQE,iBANA,qBAKA,YADA,OAMA,iBARA,UASA,aAVA,oBAFA,kBAIA,SAKA,4BAIA,6DALA,mBAEA,SAGA,CAGF,oDAEE,gEAGF,uCAEE,mBAGF,wBACE,mBAKE,kCACE,gBAIJ,iCAEE,6CADA,qCACA,CAGF,sBACE,kBAEA,qBACA,cAGA,QAAO,CALP,WAGA,eACA,mBACA,CAIA,sCACE,6LACE,CAWJ,oCACE,kGAKF,mCACE,iEAKN,gCACE,+BAIJ,sBAEE,iBADA,eAEA,gBC/GF,cACE,qBAEA,qDACE,YAGF,4BAGE,kBAFA,iBACA,kBACA,CCVJ,aAIE,kBADA,qBAFA,kBACA,kBAEA,CCDA,wBAGE,wDADA,kBADA,wBAGA,iBAGF,iBACE,cAGF,uFAKE,0CAGF,eACE,eAGF,0BACE,SAGF,gBACE,gBACA,kBACA,eAGF,gBACE,gBACA,aAGF,gBACE,cACA,eAGF,gBACE,eAOF,sCAHE,oBAMA,CAHF,oBAGE,8BADA,4BACA,CAGF,qCAGE,iBADA,eAGA,yCADA,qBACA,CC7DF,aACE,aACA,sBACA,gBAGF,mBACE,kBAGF,qBAKE,ahBRkB,CgBSlB,+BAJA,aACA,mBAFA,YAGA,iBAEA,CAGF,2BAEE,mBADA,aAEA,mBAEA,sBADA,SACA,CAGF,yBAEE,aAAY,CADZ,WACA,CAGF,mBAKE,wBhB/BgB,CgBgChB,qCACA,kBhBtBoB,CgBuBpB,sCALA,ahBhCa,CgBiCb,8BAHA,YASA,OARA,kBAOA,MAEA,qBAGF,mBAEE,mBADA,YACA,CAGF,YACE,YAGF,cAEE,mBADA,YACA,CAGF,gBACE,gBAGF,wBAEE,kBADA,cACA,CAGF,qBACE,aCxEJ,YACE,aACA,sBAEA,mBACE,8BAA+B,CAGjC,yBACE,gBAGF,uCAKE,qBAHA,uCAKA,oCAHA,yBADA,qBAGA,qBACA,CAGF,qBACE,cACA,kBACA,oBAIA,+BAIE,aADA,gBADA,uBADA,kBAGA,CAIJ,6BAIE,gCAFA,mBACA,qBAEA,WAAU,CAJV,kBAIA,CAEA,mCACE,kBAEA,4CACE,eACA,gBAEA,uBADA,kBACA,CAKN,0BACE,aACA,wBAEA,uCAEE,aACA,kBACA,kBAHA,kBAIA,UAEA,mDAEE,8GACE,CADF,sGACE,CAIF,mBACA,kDAPA,YAOA,CAKN,wHAIE,qBAGA,kBADA,WADA,oBAEA,CAGF,+BAEE,YAEA,kBADA,iBAFA,kBAIA,UAGF,gCAEE,oBAGF,yDAEE,qBAEA,iEACE,cAIJ,uBACE,ajBpGe,CiBqGf,mCAGF,sBACE,kCAGF,qBAIE,iBAAiB,CAHjB,gBACA,kBAEkB,CAElB,6DAEE,kBAGF,2BAIE,cAOA,mBACA,kDAJA,gIAFA,oDACA,gEAFA,sEAFA,cAFA,gBACA,kBAUA,CAGF,kCAEE,WAEA,YACA,iBAJA,aAEA,aAEA,CAGF,sCAOE,YACA,qBAHA,oBACA,QAEA,CAPA,qDACE,aASJ,mCACE,qBCtKN,mBAqDE,qBlB5CiB,CkB6CjB,gCAHA,kBlB1B2B,CkB2B3B,2CALA,alB3Ce,CkB4Cf,0BA7CA,eAFA,aACA,mBAGA,gBADA,eAkDA,CA/CA,+BACE,cAEA,cADA,WACA,CAEA,mCAIE,kBlBSuB,CkBRvB,2CAHA,YACA,qCAFA,UAIA,CAIJ,iCAGE,aACA,sBAFA,YADA,eAGA,CAGF,8BACE,gBAGF,qCAKE,kBAJA,gBAOA,6BANA,gBACA,uBACA,qBAIA,CAGF,+BACE,aC9CJ,eACE,OACA,YCAF,kBACE,kBAEA,+BACE,mBAGF,+BACE,aAGA,aAFA,8BACA,YACA,CAEA,sCACE,WAGF,iCAGE,aAFA,aACA,aACA,CAIJ,oCACE,aACA,OAEA,iBACA,eAFA,iBAEA,CAGF,mCACE,aACA,kBAGF,kCAEE,eADA,OAEA,gEAEA,wCACE,0BAGF,0EAGE,eADA,iBAEA,wBAIJ,qCACE,kBAGF,iCAEE,yBpBzDc,CoB0Dd,uCAFA,iBAEA,CAGF,kCACE,sBACA,oCACA,iBpB7CsB,CoB8CtB,uCAEA,QAAO,CADP,YACA,CAIA,4CACE,yBpBxEY,CoByEZ,uCAIJ,mCAIE,qBAHA,aACA,8BACA,eACA,CAIA,+DACE,aAGF,8DACE,gBAKJ,qCAEE,qBADA,OACA,CAGF,8BAEE,uBADA,OACA,CAGF,6BAEE,sBADA,OACA,CAGF,gGAQE,mBADA,aAFA,OAFA,iBACA,gBAEA,cAEA,CAKE,+wBAGE,apBzHc,CoB0Hd,+BAKF,wQAGE,UpBpIS,CoBqIT,kCAFA,kBAEA,CAEA,4SACE,UpBxIO,CoByIP,kCAMR,yBACE,kBAGF,wCAEE,mBADA,kBAEA,WAEA,0FAGE,gBADA,wCACA,CAGF,+CACE,gBAGF,8CACE,OACA,WAIJ,wCACE,aAGA,sBAFA,kBACA,UACA,CAGF,iCACE,mBAGF,uBACE,aACA,sBACA,YACA,kBAGF,8BACE,aACA,sBAEA,iBADA,uBACA,CAGF,kCAEE,uBAMA,yCACA,6CANA,gBAGA,mEAIA,YANA,6BAMA,CAEA,kDACE,gBAIJ,8BACE,kBAGF,qCAEE,SAGA,cADA,UAHA,kBAEA,OAEA,CAEA,2CACE,SpB1NW,CoB2NX,sBAIJ,mBACE,aACA,eAGF,oBACE,cACA,cAGF,kCAME,mBAKA,wBpB7PW,CoB8PX,mCAGA,0BACA,sCAHA,iBpB1OsB,CoB2OtB,uCALA,apBxPa,CoByPb,0BALA,aADA,cADA,YAIA,uBACA,WAPA,kBACA,UAcA,CCrQJ,eACE,gBAEA,8BAEE,eADA,UACA,CCDF,qBASE,6BARA,SACA,YAGA,OAEA,QAGA,aAIJ,yCAVI,eADA,cAGA,eAEA,KAkBF,CAZF,oBAWE,wBtB1Ba,CsB2Bb,mCAVA,SAGA,iBAFA,gBACA,eAGA,2BACA,YAIA,CAGE,iDACE,kBAIJ,0CAGE,wBtBtCW,CsBuCX,mCAHA,SACA,aAGA,mBAGF,yCAGE,wBtB9CW,CsB+CX,mCACA,0BACA,wCACA,aACA,yBAPA,SACA,YAMA,CAEA,gDAEE,kBADA,UACA,CCxDN,0BACE,YAEA,mCAEE,uBACA,YAKF,wDAEE,eCZF,iCAEE,eACA,eACA,kBAHA,WAGA,CAEA,mDACE,cACA,+BCTN,WACE,aACA,sBAEA,oBAIE,mBAHA,aACA,mBACA,8BAEA,oBAEA,yBACE,eAGF,6BACE,aACA,mBACA,sBAEA,kCACE,iBAKN,sBACE,mBAGF,6BAEE,uCADA,iBACA,CCjCJ,WACE,kBACA,UAEA,iBACE,qCAAsC,CACtC,uCAAwC,CACxC,sCAAuC,CAGzC,0BAME,oBAFA,uBADA,gBAEA,sBAJA,eAOA,kBANA,iBAMA,CAGF,uBACE,qBAEA,kCADA,mCAGA,kBAGF,6BAkBE,kCANA,sBAIA,8EACA,+EAHA,wEACA,yEAVA,SAFA,OAGA,oGACE,CADF,4FACE,CAGF,mBACA,kDAEA,8CAZA,kBAGA,QAFA,MAiBA,WAEA,sCACE,gDAIJ,eAEE,cACA,gBAEA,QAAO,CADP,YAHA,iBAIA,CAEA,iBACE,a1BzDW,C0B0DX,8BAGF,mBAIE,iBADA,eAFA,yCACA,qBAEA,CAIJ,sBAME,mCAAoC,CACpC,qBAAqB,CANrB,2B1BzDoB,C0B0DpB,+CACA,4B1B3DoB,C0B4DpB,+CAGsB,CAGxB,oBAIE,mCAAoC,CACpC,sCAAsC,CAJtC,kB1BnEoB,C0BoEpB,qCAGuC,CAGzC,oBAIE,qCAAsC,CACtC,wCAAwC,CAJxC,iB1BvEsB,C0BwEtB,sCAGyC,CAG3C,qBAGE,qB1B9Fe,C0B+Ff,gCAIJ,WAGE,eAEA,wBAJA,a1BrGoB,C0BsGpB,8BAKE,CAEA,mBACE,kBAIJ,sBAIE,uBADA,aAEA,gBAJA,YACA,kBAGA,CAEA,wBACE,YAGF,wBAEE,aADA,qBACA,CAGF,8BACE,sCAAuC,CACvC,+CAAgD,CAChD,6CAA8C,CAG9C,YACA,qCAFA,UAEA,CAIJ,kBAEE,eADA,iBACA,CAEA,2BASE,mBAHA,gCAIA,iB1B5ImB,C0B6InB,sCANA,SAEA,aACA,uBANA,OAUA,UAXA,kBAGA,QADA,MAUA,4BAEA,+BACE,WAIJ,mDACE,UAIJ,iEAEE,eAGA,eACA,eAFA,kBADA,WAGA,CAEA,qGACE,a1BnLgB,C0BoLhB,+BAIJ,wBAGE,qBADA,gBADA,iBAEA,CAEA,mCACE,iBAGF,0CAEE,cADA,cAGA,gBADA,sBACA,CAGF,kCAKE,a1BjNW,C0BkNX,0BAJA,cAEA,eADA,gBAFA,aAKA,CAGF,mCAIE,wB1B3NS,C0B4NT,6CAHA,a1BvNW,C0BwNX,sCAFA,SAIA,CAIJ,yBAYE,kBAAkB,CAXlB,cAKA,WAIA,gBARA,iBACA,gBACA,uBACA,mBAIA,SAGmB,CAEnB,yEAEE,aAIJ,sBAGE,cAEA,gBADA,iBAFA,gBADA,sBAIA,CAGF,sBAGE,qBADA,aAGA,eADA,iBAHA,mBAIA,CAEA,iCACE,cAEA,iBACA,gBAGF,mCAKE,iBAHA,aADA,cAEA,eACA,kBACA,CAEA,oDAEE,cADA,gBACA,CAGF,qDAGE,cADA,iBADA,aAEA,CAGF,sDAEE,cADA,UACA,CAGF,+JAKE,oBADA,kBADA,kBAEA,CAKN,8BAEE,aACA,mBACA,oBAHA,iBAGA,CAEA,gCACE,sBAEA,eADA,kBACA,CAGF,qCACE,SAIJ,sBACE,sBAIJ,8BACE,aAGF,aAME,a1BrUoB,C0BsUpB,+BANA,aAOA,eAHA,8BAHA,iBACA,qBACA,iBAIA,CAGF,YACE,cAEA,cADA,cACA,CAEA,eACE,cACA,mBACA,iBAIF,cACE,qBAIJ,aACE,aACA,mBCvWF,uBACE,iBACA,WCAF,iBAGE,qBADA,sBAMA,a5BHe,C4BIf,0BARA,aAGA,aACA,kBACA,cACA,UAEA,CAEA,oCACE,eAGF,4BACE,OAGF,4BACE,kBAGF,+BAEE,kBADA,SACA,CAEA,0CACE,mBAIJ,uBAME,qDAAuD,CACvD,yDAA2D,CAC3D,6DAA8D,CAP9D,wB5B1BgB,C4B2BhB,6CACA,a5B9Ba,C4B+Bb,qCAI+D,CAE/D,kCACE,kCAAoC,CAIxC,yBAOE,qDAAuD,CACvD,yDAA2D,CAC3D,6DAA8D,CAP9D,wB5B1CgB,C4B2ChB,6CACA,a5B/Ca,C4BgDb,sCAJA,kBAQ+D,CAE/D,oCACE,kCAAoC,CAGtC,+BACE,0BC/DN,gBACE,aACA,eAEA,YADA,eACA,CAEA,2BAOE,oB7BHa,C6BIb,8CAPA,mBACA,YAEA,kBACA,wBACA,qBAHA,UAKA,CAGF,6BAME,sBAJA,aAKA,YAJA,cAEA,iBAJA,kBAGA,iBAGA,CAEA,sFAEE,SAGF,gDAGE,wBAFA,a7B5BW,C6B6BX,8BACA,CAEA,4HAEE,cCrCN,iBAEE,8BADA,eACA,CAGF,aACE,gBACA,SACA,UAGF,aAGE,uB9BNe,C8BMf,iB9BNe,C8BOf,gCAHA,iBAGA,CAIA,oCAGE,2B9BLkB,C8BMlB,+CAHA,4B9BHkB,C8BIlB,+CAEA,CAGF,mCAGE,8B9BZkB,C8BalB,kDAHA,+B9BVkB,C8BWlB,kDAEA,CAIJ,wBACE,YAGF,8BAEE,iBACA,CAGF,2DAHE,gBAFA,gBAOA,CAGF,gCAEE,wB9B7CgB,C8B8ChB,6CAEA,uB9B9Ce,C8B8Cf,iB9B9Ce,C8B+Cf,gCALA,kBAKA,CAGF,qBACE,wB9B3DW,C8B4DX,mCAGF,6BAGE,kCAAmC,CCrErC,mBACE,iBCDF,iBACE,sBAGF,mBAEE,YADA,UACA,CAGF,eAEE,QAAO,CADP,aACA,CAGF,qBAKE,aAHA,gBAEA,UADA,uBAFA,kBAIA,CAGF,oBAEE,aADA,UAEA,kBCvBJ,gBAEE,YAEA,eAHA,eAEA,0BACA,CAEA,sBACE,UAGF,4BACE,WAKF,4BACE,eAEA,kCACE,ajChBW,CiCiBX,+BACA,kBAGF,mCAGE,mBAFA,aACA,6BACA,CAIJ,2BAGE,gBADA,kBADA,eAEA,CAGF,qCACE,YAGF,4BACE,aACA,kBAIA,+BAGE,iBjC5BmB,CiC6BnB,sCAHA,YAIA,kBACA,iBAJA,UAIA,CAIJ,0BACE,aAEA,mCACE,OACA,YACA,iBACA,YAKF,iCACE,aACA,8BCpEJ,wBACE,GACE,UAGF,GACE,WAIJ,yCAME,gBADA,eAHA,eAQA,CAEA,wFATA,mBAFA,aAGA,sBAKA,YADA,YAEA,uBAHA,UAYE,CAIJ,0DAGE,WACA,eAEA,iBADA,uCACA,CAGF,+BACE,cAIA,iBADA,gBADA,eADA,gBAIA,qBAGF,+BAIE,mDADA,6BADA,gBADA,cAGA,CAEA,uCACE,WAIJ,mCAOE,mBAFA,aAHA,YAIA,uBAFA,oBADA,kBAFA,UAMA,CAEA,uCACE,WAIJ,qCAME,6DADA,gBAJA,SAGA,gBAIA,eAEA,UA5F4B,CAqF5B,UAIA,iBALA,UAOA,kDAEA,SA3F2B,CA6F3B,kDAQE,gCAFA,WAFA,eAFA,UAjG0B,CAoG1B,eApG0B,CAgG1B,kBAMA,kBAJA,SAKA,CAIJ,2CAEE,cAIA,WAFA,gBA9GiC,CA2GjC,kBAEA,QAEA,SAhH4B,CAmH5B,uDAME,gCAFA,WADA,eAtH0B,CAoH1B,kBAIA,kBAHA,KAIA,CAGF,iDACE,OAEA,6DACE,SA7HwB,CAiI5B,iDACE,QAEA,6DACE,UArIwB,CA0I9B,0CACE,kBAEA,OAAM,CADN,KACA,CAEA,uDAEE,WADA,QAhJ0B,CAsJhC,6BAEE,sBAiBA,gBAlBA,6BAkBA,CAfA,2GAEE,YAEA,8OAGE,gBADA,YACA,CAGF,uHACE,UCtKN,uBAQE,oBADA,aADA,YAFA,OAHA,eAEA,MAMA,uBACA,8BALA,WAHA,wBAQA,CAGF,4BACE,uBAGF,8BAEE,2BADA,qBACA,CAGF,oBASE,gCALA,aAFA,OAGA,eAJA,MAMA,gBACA,qCALA,YAGA,UAGA,CAGF,2BACE,6BAGF,2BACE,cAGF,aAiBE,gDAAkD,CAClD,oDAAsD,CACtD,wDAA0D,CAC1D,yCAA0C,CAR1C,wBnCrDa,CmCsDb,wCAHA,sCACA,8BAGA,anCnDe,CmCoDf,iCANA,aAJA,oBAGA,eAPA,kBAKA,sBAJA,gBAEA,8BADA,kDAIA,SAa2C,CAE3C,oBACE,iBAIJ,0BAEE,mBADA,aAEA,cAEA,8BACE,UACA,YACA,mBAGF,+BACE,gBACA,uBACA,mBAIJ,kCACE,WAGF,oBACE,2BAGF,qBAGE,oBAFA,uBAGA,aAFA,sBAIA,QAAO,CADP,SACA,CAGF,gBAKE,uBnCpGiB,CmCoGjB,iBnCpGiB,CmCqGjB,gCALA,gBACA,SACA,SAGA,CAGF,2BACE,SAGF,gBACE,UAEA,yCAEE,sBACA,cACA,WACA,gBACA,eAEA,qDAME,4DAA8D,CAC9D,gEAAkE,CAClE,oEAAsE,CACtE,qDAAsD,CARtD,wBnC1Hc,CmC2Hd,oDACA,anC/HW,CmCgIX,4CAKuD,CCxI3D,iCAaE,mBAJA,wBpCRW,CoCSX,oCAPA,mBAEA,aASA,6DAHA,aATA,WAUA,uBARA,eAEA,YAUA,0BACA,kDAhBA,UAcA,UAEA,CAGF,yBACE,2BAGF,sBAEE,apCvBa,CoCwBb,0BAFA,eAEA,CAIJ,yBACE,qCACE,cCjCJ,aACE,aAEA,0BAEE,8BADA,YACA,CAGF,6BACE,oBACA,gEAIA,kGAEE,arCNY,CqCOZ,2BAIA,wCACE,kBADF,yEACE,kBAKF,4FACE,mBADF,sDACE,mBC5BR,gBACE,aAEA,6BAEE,8BADA,YACA,CAGF,gCACE,oBACA,gEAIA,6CACE,uBAGF,2GAEE,YtCRc,CsCSd,4BAIA,2CACE,kBAGF,4CACE,mBALF,4EACE,kBAGF,6EACE,mBAKF,kGACE,mBAGF,oGACE,kBALF,yDACE,mBAGF,0DACE,kBCvCN,qCAEE,aADA,YACA,CAEA,2CACE,OAIJ,sCAIE,oCAHA,WAEA,YADA,UAEA,CAGF,8BASE,yBAJA,aACA,eAHA,gBADA,WASA,+JACE,CADF,uJACE,CAOF,mBACA,kDAJA,8EAZA,kBAGA,aACA,kBAOA,6GALA,gEATA,UAmBA,CAEA,4CAIE,qBAHA,eACA,eACA,eACA,CAEA,kDACE,sBAKN,8BAEE,aADA,YACA,CAEA,oDACE,avCrDW,CuCsDX,0BAIJ,qCAEE,WAGE,mDACE,kBADF,oFACE,kBAKF,kHACE,mBADF,iEACE,mBCzER,eACE,aAEA,4BAEE,8BADA,YACA,CAGF,+BACE,oBACA,gEAIA,4CACE,uBAGF,wGAEE,axCTa,CwCUb,4BAIA,0CACE,kBAGF,2CACE,mBALF,2EACE,kBAGF,4EACE,mBAKF,gGACE,mBAGF,kGACE,kBALF,wDACE,mBAGF,yDACE,kBCvCN,+BAGE,aADA,aADA,eAEA,CAEA,qDACE,azCJW,CyCKX,0BAIJ,sCAEE,WAGE,oDACE,kBADF,qFACE,kBAKF,oHACE,mBADF,kEACE,mBCzBR,SACE,aAKA,eACA,YALA,SACA,SAIA,CAEA,uBACE,mBAEA,mCACE,iBAGF,qCACE,kB1COsB,C0CNtB,0CACA,YACA,WCnBN,wBAIE,iB3CIiB,C2CHjB,gCAGA,iB3CawB,C2CZxB,uCAHA,mBACA,iBANA,eAEA,cADA,cAOA,CAGA,uCACE,YAGF,mDACE,YACA,kBAEA,qDACE,cCtBN,mBAGE,iBAAiB,CAFjB,YAEkB,CAElB,kCAEE,aACA,mBAFA,aAEA,CAEA,mDACE,aACA,sBACA,iBACA,cAEA,uDAEE,WADA,SACA,CAIJ,yDACE,gBCvBN,gBACE,aAEA,eADA,gBACA,CAEA,gCAKE,mBAEA,sBAHA,aAEA,uBAJA,kBACA,gBAFA,cAMA,CAEA,gDAEE,mBADA,YACA,CAGF,sCACE,aAGF,8CACE,eAEA,oDACE,4F7CCiB,gC6CIrB,iDACE,uCACA,iBACA,8BAIJ,uCAKE,mBADA,aAEA,uBAJA,kBACA,gBAFA,cAKA,CAEA,6CACE,0BCjDN,QAGE,qBAFA,YACA,mBAEA,sBAEA,cACE,qCAAsC,CACtC,uCAAwC,CACxC,sCAAuC,CAGzC,iBAME,yDAA2D,CAC3D,qDAAuD,CACvD,yDAA2D,CAC3D,uDAAyD,CACzD,iEAAmE,CACnE,8CAA+C,CAV/C,wB9CLgB,C8CMhB,6CACA,a9CVa,C8CWb,qCAOgD,CAGlD,oBAEE,yB9CxBc,C8CyBd,uCACA,aAHA,kCAGA,CAEA,kCAEE,mBADA,aACA,CAIJ,0BACE,aACA,mCAEA,4BACE,YAGF,kCACE,cAIJ,aAGE,mBADA,aAEA,yBAHA,+DAGA,CAGF,8BACE,oBAEA,2CAEE,YADA,mBACA,CAIJ,mBACE,wCAGF,oBACE,OACA,YAGF,kBACE,yCAGF,yBASE,+BAAgC,CAChC,iBAAiB,CALjB,cADA,gBAEA,kBAHA,cADA,gBAKA,uBANA,kBASkB,CAGpB,wBACE,YAEA,kBADA,UACA,CAGF,wBACE,mBAGF,0BACE,aACA,8BACA,gBAEA,4BACE,qBACA,qBAIJ,sBAME,WAJA,kBADA,gBAGA,gBACA,uBAFA,kBAGA,CAGF,sBACE,aACA,YAGF,uBACE,aACA,cAEA,wCAEE,YADA,WACA,CAEA,kDACE,a9ChIc,C8CiId,+BAIJ,uCACE,kBAIJ,qBACE,oBACA,mBAGF,iBACE,kBAGF,uDAGE,uBAKA,oBAJA,gBAEA,iBADA,gBAEA,eALA,iBAMA,CAGF,yEAKE,aAAY,CADZ,kBADA,WAEA,CAGF,2BACE,kBAIA,iDAME,qCAFA,SAHA,WACA,cAKA,oBAJA,kBAEA,UAEA,CAGF,4CAEE,qBAIA,yDAME,qCALA,WACA,cAKA,oBAJA,kBACA,QACA,UAEA,CAKN,oCAGE,kBADA,kBACA,CAGF,8CAEE,mBACA,gBACA,uBACA,mBAGF,uBACE,eAGF,iBAIE,aACA,eAFA,gBADA,gBADA,gBAIA,CAEA,mBACE,kBAIJ,oBACE,YAGF,qBACE,wCAEA,kCACE,a9CzOa,C8C0Ob,4BAIJ,yBACE,0CAGA,YAFA,iBACA,UACA,CAGF,uBAEE,cAAa,CADb,sBACA,CAEA,8BAEE,YAEA,yCADA,sBAFA,UAGA,CAIJ,uBACE,uBACA,sBAGF,kBACE,GACE,UAGF,GACE,WAIJ,wBAGE,aACA,sCAHA,kBACA,UAEA,CAEA,0BAEE,MAAK,CADL,aACA,CAIJ,eAME,aACA,iBALA,aACA,kBAEA,gBAJA,mBAGA,sBAGA,CAEA,uFAGE,iBAEA,mBADA,iBACA,CAGF,2DAGE,gBADA,sBACA,CAGF,gCAEE,cAEA,kBAHA,gBAEA,iBACA,CAGF,4BACE,cAGF,2BACE,aACA,iBAEA,kCACE,YAIJ,uBAGE,cAFA,cACA,gBACA,CAIJ,oBAEE,gBAAe,CADf,aACA,CAGF,oBACE,OAGF,6BACE,sCAGF,eAEE,aACA,gBAFA,UAEA,CAGF,oBAKE,mBADA,aAHA,OACA,gBACA,iBAEA,CAEA,2BAME,kDALA,WAEA,YAEA,OAHA,kBAEA,SAEA,CAIJ,oBACE,wCACA,gEAEA,gCACE,uCACA,gBAEA,kBADA,wBACA,CAGF,iCAEE,gBADA,mBAEA,gBAGF,sCACE,0BAIJ,yBACE,yBACE,iBAGF,qBAEE,YADA,UACA,CAIA,8BAEE,YADA,UACA,EC7ZN,8CACE,kBAGF,yBACE,qCACA,8CACA,iB/CUoB,C+CTpB,qCACA,a/CTa,C+CUb,0BACA,cAEA,cADA,YACA,CAEA,yCACE,oBAGF,kDACE,aAEA,8BACA,mBAFA,UAEA,CAGF,+CACE,gBAIJ,cAEE,mBADA,UACA,CCrCJ,cAIE,qBAGA,iBAAiB,CALjB,uBhDOiB,CgDPjB,iBhDOiB,CgDNjB,gCAEA,qBAEkB,CAElB,oBACE,qCAAsC,CACtC,uCAAwC,CACxC,sCAAuC,CAGzC,qBAME,aACA,iBALA,aACA,kBAEA,gBAJA,mBAGA,sBAGA,CAEA,yGAGE,iBAEA,mBADA,iBACA,CAGF,uEAGE,gBADA,sBACA,CAGF,sCAEE,cAEA,kBAHA,gBAEA,iBACA,CAGF,kCACE,cAGF,iCACE,aACA,iBAEA,wCACE,YAIJ,6BAGE,cAFA,cACA,gBACA,CAIJ,yBACE,cAGF,uCACE,ahD1De,CgD2Df,4BAQF,sFACE,ahDrEc,CgDsEd,2BAGF,qCAEE,YhDzEgB,CgD0EhB,4BAGF,qCACE,ahDhFc,CgDiFd,2BC5FF,6BAEE,oBAGF,+BACE,ajDFa,CiDGb,0BAGF,6BACE,kBAEA,mDAKE,SADA,OAEA,oBALA,kBAEA,QADA,KAIA,CAIA,0DACE,2FAOR,cACE,sBAGE,4CACE,aAGF,yCACE,mBAIJ,uCACE,mBAGF,2BACE,aACA,OACA,iBAEA,WAAU,CADV,YACA,CAEA,6CAEE,YADA,UACA,CAGF,kCACE,uBAAwB,CACxB,mBAAoB,CAKtB,2CACE,ajDhEW,CiDiEX,0BAKF,2CACE,SjDjEW,CiDkEX,sBAIJ,oDAIE,aACA,8BAFA,yBADA,cAGA,CAEA,8EACE,cACA,eACA,gBACA,uBACA,mBAKJ,sBACE,OAGF,mBACE,mBAGF,kCACE,OAEA,WAAU,CADV,iBACA,CAEA,2CACE,cACA,iBAGF,gDACE,kBAIA,+DACE,kBAKN,oCACE,gBAGF,oCAEE,qBAMA,aADA,WAEA,iBACA,8BAPA,oCAFA,YAIA,gBADA,kBAEA,UAIA,CAEA,qDACE,OACA,gBACA,uBAGF,8CACE,mBACA,eACA,uBACA,mBAGF,6CACE,kBAGF,oDACE,SACA,iBAGF,uCAIE,cACA,gBAHA,gBACA,UAFA,oBAIA,CAEA,6CACE,oBAIJ,sCAGE,gBCnLN,WACE,yBAEA,uBAME,sBALA,aAGA,+BADA,wBADA,iCAGA,UACA,CAEA,yBACE,gCAIJ,6BAGE,mBADA,aADA,UAEA,CAGF,8BAKE,eAJA,qBAEA,cACA,kBAFA,iBAGA,CAGF,sBAEE,qBADA,cACA,CAGF,iBAEE,aAGF,sBASE,oBlDvCa,CkDwCb,8CATA,mBACA,WAGA,qBAEA,gBACA,gBAJA,kBAEA,oBAHA,SAOA,CAGF,wCAaE,iCANA,sCACA,8BANA,aAIA,OAHA,kBACA,eACA,MAMA,wBADA,yBADA,8BARA,WAWA,wBACA,CAEA,gDAEE,gBADA,0BACA,CAIJ,wCAEE,mBAQA,wBlDlFW,CkDmFX,uCACA,kCACA,+BAJA,wBARA,aAKA,YAHA,8BAIA,iBACA,kBAHA,WADA,oCASA,CAEA,gDACE,OAGF,+CACE,gBACA,iBAIJ,iBACE,OAEA,8BACE,YAIJ,iCAQE,wBlDlHW,CkDmHX,mCAHA,alD7Ga,CkD8Gb,0BAJA,0CAFA,gBAGA,kBACA,kBAHA,WAOA,CAEA,gDAEE,gBACA,gBAFA,SAEA,CAEA,uDACE,gBAEA,gBADA,QACA,CAGF,6DACE,gBAGF,sEACE,gBACA,gBAMJ,8CACE,aAGF,2DACE,aClJN,WAEE,qBADA,oBAGA,yBADA,uBACA,CAEA,qBACE,WAGF,uDAEE,YAGF,6BACE,cAGF,0BACE,YAGF,wBACE,anDpBa,CmDqBb,mCC1BJ,YACE,WACA,yBAEA,kBACE,8CAGF,cACE,gCAGF,uBAKE,sBAJA,aAGA,4CADA,mCADA,wCAKA,YACA,gBAFA,eAEA,CAGF,uCACE,kBAAmB,CACnB,kBAAmB,CACnB,eAAgB,CAEhB,8HACE,CAOJ,iCAEE,4CADA,kCACA,CAGF,6CACE,4KACE,CASF,4DAEE,apDjDW,CoDkDX,mCAGF,mCACE,wBpDxDS,CoDyDT,iDACA,apDxDW,CoDyDX,0CAGF,qCACE,apD7DW,CoD8DX,2CAGF,oCAGE,wBpDtES,CoDuET,iDAHA,apDlEW,CoDmEX,yCAEA,CAIJ,kBACE,eACA,kBACA,mBAEA,wBADA,mCACA,CAEA,yBAPF,kBASI,qBAGF,wBAIE,wBpD3FS,CoD4FT,2CAGA,SACA,OAPA,kDADA,oDAEA,4CAGA,kBAIA,OAAM,CAHN,KAGA,CAGF,sBACE,qBACA,4BAIJ,sBAGE,YAFA,iBAGA,kBAFA,SAEA,CAEA,sCACE,apD9GW,CoD+GX,gCAIJ,sBACE,mBAGF,qBACE,kBAGF,kBAKE,aAJA,OAKA,eAHA,4BADA,iCAEA,eAEA,CAEA,wBACE,yBACA,iBAIJ,oBACE,UC9IF,4BAGE,oEAGF,oBAEE,aADA,iBACA,CCTJ,sBAIE,gBAFA,gBACA,gBAFA,UAGA,CAEA,kCAIE,mCtDDe,CsDCf,yBtDDe,CsDEf,gCAJA,aACA,8BAIA,gBAGF,2BAGE,sBADA,oCADA,uBAEA,CAEA,+BACE,kBAEA,0CACE,gBAIJ,6BACE,aAGF,iDACE,iBAIA,gBAFA,gBADA,YAEA,8BAEA,WAGF,gCACE,eACA,cAGF,kCAEE,kBADA,cACA,CAIJ,4BACE,aACA,sBACA,gBAGF,4BACE,aACA,8BAGA,oCACE,OAGF,sCACE,aAIJ,yBACE,kCACE,mBAGF,2BAIE,sBtDxEa,CsDwEb,iBtDxEa,CsDyEb,gCAHA,gBAIA,cALA,SAKA,CAEA,+BACE,kBAIJ,4BAEE,cACA,mBAFA,SAEA,EC/FN,iCACE,uBAGF,uBACE,cAEA,kBADA,eAGA,gBADA,UACA,CAEA,8BAPF,uBAQI,eAGF,yCACE,gBAEA,qDACE,sBCnBN,iCACE,uBAGF,uBACE,cAEA,kBADA,eAGA,gBADA,UACA,CAEA,8BAPF,uBAQI,eCZJ,sCACE,uBAGF,4BACE,cAEA,kBADA,eAGA,gBADA,UACA,CAEA,8BAPF,4BAQI,eCVJ,oBAQE,mBAFA,aACA,sBAHA,oBAHA,eACA,sCACA,WAEA,iCAGA,CAEA,mCAKE,aAEA,cACA,mBAJA,2BAEA,mBALA,oBACA,kBACA,UAKA,CAEA,mDACE,cAIJ,kCACE,2CACA,CAEA,oFAFA,wCAGE,CAIJ,oCACE,gDACA,CAEA,wFAFA,0CAGE,CAIJ,oCACE,iDACA,CAEA,wFAFA,0CAGE,CAIJ,iCACE,iDACA,CAEA,kFAFA,0CAGE,CAIJ,kCACE,mBAEA,wDACE,WCpEN,OCIE,wB5DAa,oC4DFb,YACA,sBACA,CAHF,iBAKE,qBAEA,kB5DasB,sC4DVpB,cAMA,QACA,CAGA,qCACA,8BACA,CATF,UACE,CAGA,MACA,CAIA,oBARA,iBACA,CAGA,OACA,CAJA,KACA,CAGA,SAIA,aAIJ,mCACE,0BAEA,oBACE,cACA,WACA,kBACA,eAGF,eACE,CACA,SADA,WAEA,8BAIJ,oCAEE,4BACA,+BACA,8GACA,CAOA,0CACA,CACA,qBACA,CARA,qBACA,aACA,CAIA,SACA,CAHA,sBACA,CAHA,qBACA,sCACA,CAKA,oCACA,gDACA,CAHA,2CACA,CAXA,iBAEA,CAWA,SACA,gEAEA,6BACE,yJAEA,YAEE,+FAKF,kB5DvDoB,sC4D0DlB,8CAIJ,eACE,yBACA,qFAOA,QACA,CALF,UAEE,CAIA,MACA,qBALA,iBACA,CAEA,OACA,CAHA,KAKA,4CAGF,eACE,4CAKA,kBADA,sBACA,CAFF,kBAGE,qMAYE,mBALA,qBACA,CAJF,0CAEE,CAEA,QACA,CAHA,YACA,CAEA,aACA,CACA,gBACA,CAFA,aAGA,gBAUJ,iBACA,CAEA,wB5DhIa,oC4D4Hb,oBACA,CACA,sBAIA,qCARF,2BACE,kEAeE,CARF,qBAEA,wB5DnIa,sC4DqIX,CAGA,oCAHA,UAIA,wCAGF,a5DzIe,+B4D4Ib,gRAKA,sBAGE,uBAIJ,4BACE,0B5D3Jc,4C4D6Jd,4BAGF,yB5DhKgB,2C4DkKd,uDAIA,aACE,6HAEA,a5DxKW,kC4D2KT,8DAGF,wB5DhLS,gD4DkLP,c5DhLS,yC4DkLT,gEAGF,a5DrLW,0C4DuLT,+DAGF,a5D1LW,yC4D4LT,kCAKN,kBACE,CAEA,oCACA,sDACA,kDAJA,iBACA,oCAIA,yCAEA,qBACE,CACA,WACA,CAFA,qDACA,CAEA,kBADA,UAEA,6CAEA,eACE,gCAKN,kBACE,CAEA,iDAFA,iBACA,oCAEA,oCAEA,eACE,eAOJ,kBACA,CAEA,gCALF,2BACE,kEACA,CAEA,kBACA,CAFA,oBAGA,OD1OF,sBACE,uBACA,sBAEA,0BACA,iBACA,0BACA,iBACA,mBACA,MAGF,cACE,MASA,kCACA,kCACA,CAJA,a3DlBe,0B2DoBf,CALF,sBACE,4CACA,SACA,CAKA,eACA,mBAFA,0BAGA,aAEA,YACE,0BAOJ,EACE,sCACE,qBAEA,sBACE,sDAGF,2BAEE,CACA,+BADA,8BAEA,4BAMF,kBACE,CAEA,sCAFA,oBAGA,uCAEA,uFACE,iDAEA,qIAEI,0FAEF,iDAGF,qIAEI,0FAEF,qCAIJ,uFACE,+CAEA,qIAEI,uFAEF,+CAGF,qIAEI,uFAEF,MAQN,4BADF,oDAEE,IAKF,a3DxGe,2B2DuGjB,oBAGE,IAGF,QACE,aAGF,oBACE,CACA,iBADA,iBAEA,6CAGF,U3DtHiB,uB2D0Hf,sLAKA,iBAGE,KAKF,wB3D3Ia,uC2D6Ib,CAEA,iCACA,+BACA,sBACA,CALA,yB3D5IgB,uC2D8IhB,CAGA,2BACA,gBATF,wBAUE,UAGF,iBACE,QAGF,iBACE,yBACA,qBAIA,gBADF,wBAEE,gBAGF,iBACE,kBACA,gBAGF,gBACE,iBAWA,iCACA,8CACA,yBAHA,2BACA,CAFA,qBACA,CANA,WACA,CAEA,MACA,CALF,cACE,CAIA,WACA,CAJA,wBACA,cAQA,WAMA,gCACA,iDACA,CALF,oBACE,aACA,oBACA,CAEA,aACA,aAGF,kBACE,mBACA,gBACA,uBACA,oGACA,kGACA,oGACA,CAUA,wBACA,eACA,CAPE,qCAEF,CAJA,2FAEE,CAEF,sBACA,CAIA,sBACA,CAJA,aACA,CAGA,gBACA,iBAdA,iBAeA,iCAPA,qBACA,CAPA,YAyBE,CAZF,oBAEA,kCACE,CAQA,oBAJA,YACA,CAHA,0BACA,CAEA,uCACA,uCACA,+BAEA,uCAEA,+BACE,2BAGF,SACE,kCAGF,eACE,CACA,iBADA,aAEA,iCAGF,6CACE,CAMA,8CACA,CAJA,6CACA,CACA,iBACA,CAFA,eACA,CAEA,wEAPA,eAEA,yBAMA,sEAIA,sDAEI,+CACA,0EAFF,oBAGE,0EAEA,aACE,QACA,yDAKN,6BACE,0CAMJ,oBACE,+DAMA,iBACE,MACA,2BASJ,oBAFA,qBACA,CAHF,YACE,2BACA,CACA,WAEA,2CAKE,sCAFJ,2FAIE,mBAKE,6CAFJ,6HAKE,4BAII,6CAFJ,6HAKE,qBAKF,6BACA,CAFF,2BACE,CACA,SACA,6BAGE,kCADF,aAEE,mLAGF,wBAKE,0BACA,CAKA,mGAKF,YACE,cAKN,iBACE,iBAMA,wB3D5Wa,oC2D8Wb,YACA,kB3D7VoB,mC2D+VpB,CACA,4F3DxVuB,+B2D0VvB,CAVA,a3DxWe,6B2D0Wf,CAKA,cACA,CAGA,sBACA,6CAFA,aACA,CAZF,wBACE,CADF,qBACE,CADF,gBAcE,0BAEA,sBACE,iEAGF,a3D3Xe,6B2D8Xb,mCAGF,WACE,uBAGF,qCACE,oCACA,wBAUA,wB3DnZW,4C2D4Yb,0GAEI,sCAOF,4EAJA,a3D/Ya,oC2DwZX,0BAOF,wB3DjaW,6C2D8Zb,kBAKE,kFAJA,a3D7Za,qC2DsaX,yBAMF,wB3D9aW,2C2DgbX,2GAEE,sCAGF,+EATF,a3D1ae,oC2DwbX,wBAOF,mC3DpbmB,uD2DibrB,a3D5be,yC2Dicb,kBAIJ,eACE,YACA,CAQA,sBACA,eAFA,cACA,CAPA,cACA,CAEA,mBACA,CAFA,cACA,CAEA,iBACA,CAPA,YACA,CAIA,SACA,CAJA,kBAQA,wBAEA,a3Dlde,0B2Dodb,6BAGF,UACE,6CAIA,a3DzdkB,+B2D2dhB,uBAKN,gBAUE,CASA,wB3Dzfa,sC2D2fb,CAXA,WAEA,kB3D/dsB,qC2DietB,mGAEE,8BAGF,CAQA,qBACA,CAPA,a3DrfoB,+B2DufpB,CAKA,oBACA,CANA,sBACA,wCACA,cACA,CAKA,oBACA,CADA,YACA,CAFA,aACA,CALA,QACA,CAKA,0BAHA,iBAIA,kDA7BE,eACA,CAFF,eACE,CACA,eACA,aACA,kLA4BF,kBAGE,WACA,2DAGF,eACE,YACA,CACA,eACA,QAFA,QAGA,2DAGF,YACE,0HAIE,uCAFF,qDACE,gEAEA,yTAIA,UAGE,kGAcF,wB3DnjBS,sC2DqjBT,CANA,kBACA,8BACA,8BACA,CAOA,qBACA,kBACA,CAhBA,UACA,CAFA,oBACA,CAFF,aACE,CAcA,eACA,CAXA,YACA,CAQA,eACA,CANA,iBACA,CAQA,gBALA,iBACA,CAXA,yBACA,CAQA,kBACA,CATA,WAeA,mIAKF,a3D/jBa,+B2DikBX,oVAIA,UAGE,2GAeF,wB3DzlBS,sC2D2lBT,CAPA,iB3DnkBqB,wC2DqkBrB,8BACA,8BACA,CAOA,qBACA,kBACA,CAjBA,WACA,CAFA,oBACA,CAFF,aACE,CAeA,eACA,CAZA,YACA,CASA,eACA,CANA,iBACA,CAQA,gBALA,iBACA,CAZA,oBACA,CASA,kBACA,CAVA,WAgBA,iEAIJ,eACE,UAMF,oCADF,uBAEE,QAKA,wB3DpnBa,oC2DknBf,a3D/mBiB,0B2DmnBf,sBAGF,4BACE,CADF,yBACE,CADF,oBACE,2HAIE,aAFF,SAGE,YAIJ,aACE,WACA,YAIA,mBACA,CAFF,iBACE,CACA,qBACA,mBAGE,cADF,iBAEE,oCAGE,6BADF,yBAEE,qCAIA,4BADF,wBAEE,KAKN,UACE,eAGF,YACE,QAKA,kBACA,CAHF,qBACE,qBACA,CAQA,cACA,CAFA,iBACA,CAFA,eACA,CAJA,YACA,CAKA,aACA,CATA,cACA,gBACA,CASA,eACA,CATA,aACA,CAKA,iBACA,CAEA,uBARA,qBACA,CAKA,kBAGA,2BAEA,oB3D9qBe,8C2DgrBb,WACA,wCACA,QAMF,iB3D5qBwB,wC2D0qB1B,cACE,gBAGA,cAEA,mC3DvrBqB,sD2DyrBnB,c3DpsBa,oC2DssBb,6BAEA,a3DxsBa,yC2D0sBX,gBAIJ,oC3DlsBuB,yD2DosBrB,c3DhtBa,sC2DktBb,+BAEA,a3DptBa,2C2DstBX,gBAIJ,wDACE,sCACA,+BAEA,0CACE,CAOJ,mBAGF,yB3D1uBkB,uC2D4uBhB,mBAEA,yBACE,oBAKF,oCACA,kDACA,kB3DpuBsB,sC2DiuBxB,YAKE,qBAGF,kBACE,kBACA,8BAME,cADA,YACA,CAJF,iBACE,CACA,OACA,CAFA,KAIA,uDAKF,eAEE,iFAKF,cAGE,YAIJ,WACE,aAGF,iBACE,0BAEA,YAHF,YAII,gBAGF,oBACE,cACA,WACA,qBAIJ,cACE,0BAMA,OAFA,eACA,CAFF,iBACE,CACA,SAEA,0BAGF,eACE,YACE,kBAIJ,GACE,sBACE,IAGF,wBACE,wBAIJ,GACE,uBACE,KAGF,6BACE,KAGF,8BACE,KAGF,6BACE,KAGF,8BACE,KAGF,6BACE,KAGF,8BACE,IAGF,uBACE,wCAKJ,sBAEE,qCAGF,SAEE,gCAUA,kBACA,CAPF,aACE,CACA,UACA,YACA,gBACA,CAEA,SACA,mBAHA,kBACA,CALA,SAQA,CE93BF,qBAEE,yCADA,sCACA,CAGF,4BAKE,oBADA,aAEA,sBALA,kCAKA,CCXF,cACE,UAEA,kDAOE,oBALA,2CACA,gBAGA,aAEA,sBAPA,kCAOA,CAGF,gCAEE,yCADA,sCACA,CAGF,qDACE,uBAAwB,CACxB,mBAAoB,CAEpB,kBAGF,wCAEE,2CACA,eAAc,CAFd,uCAEA,CAGA,sFAGE,oBADA,aAEA,sBAIJ,8CACE,mCAGF,mCACE,2CACA,gBAGF,iTAKE,mBAGF,kEACE,wCAIF,mDAKE,2CAHA,4DACA,4BACA,iEACA,CAGF,sCACE,2CCvEJ,uBAME,wBAAuB,CADvB,0BADA,eADA,iBADA,gBADA,eAKA,CAEA,0BACE,gBACA,SACA,UAGF,yBACE,cAEA,aACA,kBAFA,eAEA,CAEA,+BAGE,a/DlBW,C+DmBX,qCAKgD,CAGlD,2EANE,qDAAuD,CACvD,yDAA2D,CAC3D,6DAA+D,CAC/D,8CAA+C,CAR/C,wB/Ddc,C+Ded,4CAoBgD,CAVlD,4CAIE,a/DhCW,C+DiCX,sCAJA,kBASgD,CAEhD,kDACE,0BAIJ,6BAEE,kBADA,iBACA,CAIJ,0BAEE,uB/DhDe,C+DgDf,iB/DhDe,C+DiDf,gCACA,UAEA,uCAGE,8B/D9CkB,C+D+ClB,kDAHA,+B/D5CkB,C+D6ClB,kDAEA,CAGF,qCACE,YAKN,cACE,kBACA,YAEA,sCACE,sBAGF,2BAEE,wBAAuB,CADvB,yBACA,CAGF,mCAEE,eAGA,aAJA,SAEA,gEACA,UACA,CAEA,uDACE,gBACA,uBACA,mBAGF,uCACE,iBACA,yBAGF,kDACE,eACA,YAIJ,4CACE,a/D5Ga,C+D6Gb,+BACA,yBAGF,qBACE,gCCtHF,qBACE,mBACA,WAGA,qBAEA,gBACA,gBAFA,oBAHA,SAMA,CAGF,4CAHE,qCALA,iBAoBA,CAZF,uBAIE,mCAQA,8BAXA,gBAKA,sBAJA,cAOA,iBACA,gBAFA,aALA,iBAIA,oBAKA,CAGF,2BACE,kBAGF,mBACE,gBAGF,gCACE,oEACA,UAIA,sCAEE,mBACA,eAFA,iBAEA,CAEA,mGAEE,gBACA,WCjDR,cACE,aAEA,wBAEE,cADA,gBACA,CAGF,uBACE,sBAEA,6BAME,cADA,mBAFA,gBADA,kBAEA,gBAHA,UAKA,CAEA,uEAME,oEAJA,WACA,aAGA,CAGF,0CACE,WAEA,6DAME,oEAHA,SAFA,OACA,OAIA,CAIJ,kCAGE,4BACA,6BAEA,oBAJA,cAGA,oBAJA,UAKA,CAIJ,iDACE,aAIJ,wBACE,mBAEA,yBAHF,wBAII,iBAGF,kCACE,cAGF,8BACE,cAGA,sBADA,kBADA,eAEA,CAEA,yEAOE,kEAHA,WADA,gBADA,aAKA,CAGF,oCACE,YAGF,qCACE,YAGF,2CAEE,aACA,sBAFA,cAEA,CAEA,yBALF,2CAMI,eAGF,8DAME,kEAHA,SADA,QADA,KAKA,CAGF,kDAKE,kEAHA,WADA,YAIA,CAGF,2DACE,gBAIJ,mCAME,6BADA,0BAHA,uBADA,OASA,gBADA,oBANA,eACA,cAGA,iBACA,+BAEA,CAEA,yBAZF,mCAgBI,kBADA,iCAFA,mBACA,iCAEA,CAEA,yCACE,cAOV,wBACE,cACA,aAEA,gCACE,aAGF,kDAEE,aACA,sBAFA,WAEA,CAEA,sEACE,OAIJ,wCACE,gBAIJ,mBAGE,gBAFA,kBACA,kBACA,CAEA,gCACE,UAEA,sCACE,UAIJ,0BACE,uBAEA,ajEvLW,CiEwLX,mCAFA,SAEA,CAGF,uBAGE,gBAFA,gBACA,kBACA,CAIJ,oBAGE,sBAFA,aACA,iBACA,CAEA,qDAEE,cACA,cAIJ,2BAEE,aACA,cAFA,iBAEA,CAGE,8CACE,WACA,kBACA,UAKN,4BAME,2CADA,oBADA,iBADA,gBADA,qBADA,iBAKA,CAEA,yBARF,4BASI,cCzON,YAME,iBAAiB,CALjB,YAKkB,CAElB,kCANA,gBACA,uBACA,kBAUE,CANF,sBAKE,qBADA,eAHA,cAKA,CAGF,8BACE,kBACA,cAGF,6BAIE,kBlEFwB,CkEGxB,0CAHA,aADA,kBAEA,WAEA,CAEA,6CACE,aCjCN,gBAME,sBACA,eANA,aACA,mBAEA,WACA,gBAFA,aAIA,CAEA,uBACE,aAGF,sBACE,6CACA,sCAGF,qCACE,iBAGF,uCAIE,qBAFA,sBACA,gBAFA,UAGA,CAGF,yBAEE,oBACA,8BACA,gBAHA,UAGA,CAGF,+BACE,mBAGF,uCAIE,cACA,oCAFA,gBAFA,uBACA,kBAGA,CAGF,8BAME,anE/Ca,CmEgDb,2BANA,oBAIA,eAHA,gBAEA,uBADA,mBAKA,WAGF,kBACE,+BAEA,oBADA,oBACA,CAIA,8CACE,aAGF,2CACE,mBAIJ,wBACE,kBnEjDwB,CmEkDxB,0CAGF,mCACE,kBAAmB,CAEnB,kBAGF,8BACE,oCCtFJ,iBAME,iBAAiB,CALjB,aACA,SACA,SACA,gBAEkB,CAElB,mCAGE,OAFA,iBAGA,WAAU,CAFV,eAEA,CAIA,+BAEE,YADA,yCAGA,sBADA,UACA,CAIJ,8DAEE,qBACA,eACA,gBAEA,uBADA,kBACA,CAGF,kCACE,OACA,iBACA,YCpCF,sBACE,aACA,iBAEA,4BACE,WAIJ,uBACE,kBAGF,uBACE,qBAGF,iCAEE,6CADA,cACA,CAGF,0BAIE,iBADA,YADA,cADA,kBAIA,0CCzBJ,WAEE,eAAc,CADd,eACA,CAGF,uBAKE,atENe,CsEOf,2BAHA,aADA,gBAEA,uBAHA,WAKA,CCTI,oEACE,aAGF,iEACE,mBAKN,yCAEE,UACA,kBACA,UAHA,sBAGA,CAEA,gDAEE,oBADA,gBACA,CAIJ,iCACE,eAEA,mGAEE,avEzBW,CuE0BX,0BAIJ,+BACE,WAGF,oCACE,aACA,oBAEA,uDACE,qCAAsC,CACtC,uCAAwC,CACxC,sCAAuC,CAI3C,sCACE,mBACA,WAGF,uEAEE,kBAGF,8BACE,kBvElC0B,CuEmC1B,4CACA,aACA,cAGF,kCAEE,YACA,eAEA,kBADA,oBAEA,WALA,iBAKA,CAME,8EAEE,YACA,qBAFA,kBAEA,CAMJ,qGAEE,mBAKF,iGAEE,SvEtFW,CuEuFX,mCAIJ,0CAGE,uBAFA,aACA,sBAEA,cACA,eACA,WAGF,gCAGE,kBAFA,aACA,mBAEA,yBAEA,kCACE,6CAGF,wCAEE,sDACA,4DAFA,4CAEA,CAGF,oDACE,qBAGF,mDACE,YAKF,kCACE,6CAGF,wCAEE,sDACA,2DAIA,sFANA,4CAOE,CAIJ,mDACE,WAOF,kHACE,WAIJ,+BACE,UAIJ,6BAKE,avE3Ke,CuE4Kf,iCAHA,eADA,eADA,kBAGA,+DAEA,CCnLF,WACE,aACA,YAEA,4BAIE,aAHA,YAEA,iBADA,UAEA,CAGF,2BAEE,uCAOA,4BACA,kEATA,sBAEA,aACA,sBAIA,SADA,8CADA,iBADA,UAKA,CAEA,iCACE,gBAIJ,yBAGE,aACA,sBAFA,YAGA,oBAJA,cAIA,CAGF,mBAGE,wBxEnCW,CwEoCX,mCAFA,SADA,gBAIA,UAGF,8BACE,2CAGF,2BAIE,iBADA,YADA,cADA,kBAIA,0CAGF,kCAWE,mBAJA,wBxE1DW,CwE2DX,oCALA,mBASA,6DAMA,eATA,aAPA,aAQA,uBAMA,UAZA,kBACA,YACA,WAQA,oBACA,kDAEA,kBAhBA,YAYA,UAKA,CAEA,0CACE,UACA,mBAGF,oCAEE,axE5EW,CwE6EX,0BAFA,aAEA,CAGF,wDAKE,mBAJA,eACA,SACA,iBACA,aAEA,kBAGF,sDAGE,qBADA,aAEA,YAHA,UAGA,CAEA,6DACE,WCrGN,+BAEE,aACA,mBAFA,cAGA,8BACA,kBAGF,oBAGE,gBAFA,gBACA,eACA,CAGF,2BAEE,iBADA,gBAEA,WChBF,uBAKE,8DAJA,aACA,iBAGA,CAEA,8BACE,eAGF,yBACE,eCZN,cAKE,qBAAqB,CAJrB,OACA,gBAGsB,CAEtB,6BACE,oBAGF,mCACE,cAEA,uCAIE,iBADA,eAFA,yCACA,qBAEA,CAEA,6CAEE,YADA,UACA,CAIJ,uDAGE,oCACA,iB3ETkB,C2EUlB,qCAJA,aACA,YAGA,CAEA,gFAME,0CAFA,uBAHA,aACA,gBAGA,gBAFA,gBAGA,CAGF,iFAEE,kBADA,aAEA,mBAGF,iKAOE,sBALA,gBAGA,gBACA,mBAHA,uBACA,kBAGA,CAKN,oCAGE,mBAFA,aACA,uBAEA,YAKF,sCAGE,mBAFA,aACA,uBAEA,YCzEJ,uBACE,yB5EEgB,C4EDhB,uCACA,eACA,kBAGF,yBAEI,qDACE,cAEA,cADA,uBAEA,mBAKN,eAGE,uB5EZiB,C4EYjB,iB5EZiB,C4EajB,gCAHA,qBAGA,CAGF,sBAKE,wB5E5Ba,C4E6Bb,sCAHA,gCADA,mBADA,qBAGA,YAEA,CAGF,wBAEE,aACA,uBAFA,aAEA,CAEA,sCAKE,sBAFA,eADA,qBAEA,cAHA,UAIA,CAGF,uCACE,iBAIJ,cACE,YAGF,OAEE,mBADA,YACA,CAEA,gBACE,cAGA,gBACA,uBACA,mBAGF,8BAPE,a5E1Da,C4E2Db,yBAcA,CARF,cACE,cAEA,iBAEA,gBADA,oBAEA,kBAJA,UAMA,CAIJ,sBACE,aACA,kBClFA,8CACE,iBCLJ,mBAIA,YACE,sBACA,YACA,+BAEA,YACE,mBACA,iCAEA,WACE,sCAIJ,YACE,YACA,iCAKA,YACA,CAFA,QACA,CACA,sBAHF,eAIE,6BAGF,gBACE,gBACA,gCAGF,YACE,sBACA,CACA,aACA,mBAFA,cAGA,uCAIA,sBACA,CAFF,yBACE,CACA,qCACA,oDAGF,aA/CiB,0BAiDf,gCAGF,gBACE,gBACA,qCAEA,eACE,mCAIJ,eACE,CACA,aADA,iBAEA,6CAEA,YACE,kCAIJ,gBACE,gBACA,6BAIA,mBADF,eAEE,yBAIA,WADF,eAEE,2BAGF,iBACE,0BAIJ,8BACE,6BACE,EC5FJ,qBAGE,mBAFA,aACA,sBAEA,YAEA,gCACE,aACA,SACA,sBACA,gBACA,gBAEA,kCACE,YAIJ,iCACE,aACA,sBAGA,mBAFA,kBACA,cACA,CAGF,4BAGE,uBADA,0BAEA,sCAHA,iBAGA,CAGF,4BAEE,kBADA,YACA,CAGF,8CACE,sDACA,eAGF,yCACE,mBAGF,8BACE,eClDJ,uCACE,aACA,mBAEA,8CAGE,SADA,kBADA,gBAGA,eACA,cAEA,yDACE,eCZN,aACE,WCDF,aACE,iBACA,gBAEA,8BACE,eCNJ,aACE,WAEA,mBAIE,oBADA,kBADA,gBADA,UAGA,CAEA,4CAGE,gBACA,gBACA,wBAHA,WAGA,CAGF,kDAEE,WChBN,WACE,aAGF,WACE,YAGF,6BAIE,apFPe,CoFQf,0BAHA,SACA,WAEA,CAEA,yCAME,qDAAuD,CACvD,yDAA2D,CAC3D,6DAA8D,CAP9D,wBpFTgB,CoFUhB,6CACA,apFba,CoFcb,qCAI+D,CCxBjE,wBACE,eCCF,6BACE,aACA,iBAEA,mCACE,WAIJ,8BACE,kBCXJ,eAGE,mBAGA,avFFe,CuFGf,0BANA,aAIA,cAHA,YAEA,sBAGA,CAEA,iCAGE,avFRa,CuFSb,0BAHA,cACA,qBAEA,CCbJ,UACE,0BAA2B,CAI3B,aACA,sBAHA,0CACA,eAEA,CAEA,6BACE,2CAGF,sBACE,aACA,OACA,sBACA,gBAGF,kCACE,cAGF,uBACE,kBAGF,sBAEE,gBADA,oBACA,CAGF,+CAGE,sBACA,YAAW,CAFX,eAEA,CAGF,0BAIE,iBADA,YADA,cADA,kBAIA,0CAGF,eACE,cAGF,wBACE,sCAEA,uCACE,cCzDN,qBAEE,oBADA,aAEA,sBAEA,4CACE,gBAGF,oCAIE,uBAFA,YACA,cAFA,eAGA,CCXJ,cACE,2CACA,gBACA,mCAEA,2CAEE,yCAOA,mDACE,aACA,sBAIJ,+BACE,aACA,mBACA,6BAEA,oCACE,OACA,WACA,eC3BJ,+BACE,mCAEA,6EAEE,yCAGF,4CACE","sources":["webpack://pleroma_fe/./src/components/modal/modal.vue","webpack://pleroma_fe/./node_modules/vue-virtual-scroller/dist/vue-virtual-scroller.css","webpack://pleroma_fe/./src/components/login_form/login_form.vue","webpack://pleroma_fe/./src/components/media_upload/media_upload.vue","webpack://pleroma_fe/./src/components/scope_selector/scope_selector.vue","webpack://pleroma_fe/./src/_variables.scss","webpack://pleroma_fe/./src/components/checkbox/checkbox.vue","webpack://pleroma_fe/./src/components/popover/popover.vue","webpack://pleroma_fe/./src/components/still-image/still-image.vue","webpack://pleroma_fe/./src/components/emoji_picker/emoji_picker.scss","webpack://pleroma_fe/./src/components/emoji_input/emoji_input.vue","webpack://pleroma_fe/./src/components/select/select.vue","webpack://pleroma_fe/./src/components/poll/poll_form.vue","webpack://pleroma_fe/./src/components/flash/flash.vue","webpack://pleroma_fe/./src/components/attachment/attachment.scss","webpack://pleroma_fe/./src/components/gallery/gallery.vue","webpack://pleroma_fe/./src/components/user_avatar/user_avatar.vue","webpack://pleroma_fe/./src/components/mention_link/mention_link.scss","webpack://pleroma_fe/./src/components/mentions_line/mentions_line.scss","webpack://pleroma_fe/./src/components/hashtag_link/hashtag_link.scss","webpack://pleroma_fe/./src/components/rich_content/rich_content.scss","webpack://pleroma_fe/./src/components/poll/poll.vue","webpack://pleroma_fe/./src/components/status_body/status_body.scss","webpack://pleroma_fe/./src/components/link-preview/link-preview.vue","webpack://pleroma_fe/./src/components/status_content/status_content.vue","webpack://pleroma_fe/./src/components/post_status_form/post_status_form.vue","webpack://pleroma_fe/./src/components/remote_follow/remote_follow.vue","webpack://pleroma_fe/./src/components/dialog_modal/dialog_modal.vue","webpack://pleroma_fe/./src/components/moderation_tools/moderation_tools.vue","webpack://pleroma_fe/./src/components/account_actions/account_actions.vue","webpack://pleroma_fe/./src/components/user_note/user_note.vue","webpack://pleroma_fe/./src/components/user_card/user_card.scss","webpack://pleroma_fe/./src/components/user_panel/user_panel.vue","webpack://pleroma_fe/./src/components/navigation/navigation_entry.vue","webpack://pleroma_fe/./src/components/navigation/navigation_pins.vue","webpack://pleroma_fe/./src/components/nav_panel/nav_panel.vue","webpack://pleroma_fe/./src/components/features_panel/features_panel.vue","webpack://pleroma_fe/./src/components/who_to_follow_panel/who_to_follow_panel.vue","webpack://pleroma_fe/./src/components/shout_panel/shout_panel.vue","webpack://pleroma_fe/./src/components/media_modal/media_modal.vue","webpack://pleroma_fe/./src/components/side_drawer/side_drawer.vue","webpack://pleroma_fe/./src/components/mobile_post_status_button/mobile_post_status_button.vue","webpack://pleroma_fe/./src/components/reply_button/reply_button.vue","webpack://pleroma_fe/./src/components/favorite_button/favorite_button.vue","webpack://pleroma_fe/./src/components/react_button/react_button.vue","webpack://pleroma_fe/./src/components/retweet_button/retweet_button.vue","webpack://pleroma_fe/./src/components/extra_buttons/extra_buttons.vue","webpack://pleroma_fe/./src/components/avatar_list/avatar_list.vue","webpack://pleroma_fe/./src/components/status_popover/status_popover.vue","webpack://pleroma_fe/./src/components/user_list_popover/user_list_popover.vue","webpack://pleroma_fe/./src/components/emoji_reactions/emoji_reactions.vue","webpack://pleroma_fe/./src/components/status/status.scss","webpack://pleroma_fe/./src/components/report/report.scss","webpack://pleroma_fe/./src/components/notification/notification.scss","webpack://pleroma_fe/./src/components/notifications/notifications.scss","webpack://pleroma_fe/./src/components/mobile_nav/mobile_nav.vue","webpack://pleroma_fe/./src/components/search_bar/search_bar.vue","webpack://pleroma_fe/./src/components/desktop_nav/desktop_nav.scss","webpack://pleroma_fe/./src/components/list/list.vue","webpack://pleroma_fe/./src/components/user_reporting_modal/user_reporting_modal.vue","webpack://pleroma_fe/./src/components/edit_status_modal/edit_status_modal.vue","webpack://pleroma_fe/./src/components/post_status_modal/post_status_modal.vue","webpack://pleroma_fe/./src/components/status_history_modal/status_history_modal.vue","webpack://pleroma_fe/./src/components/global_notice_list/global_notice_list.vue","webpack://pleroma_fe/./src/App.scss","webpack://pleroma_fe/./src/panel.scss","webpack://pleroma_fe/./src/components/thread_tree/thread_tree.vue","webpack://pleroma_fe/./src/components/conversation/conversation.vue","webpack://pleroma_fe/./src/components/timeline_menu/timeline_menu.vue","webpack://pleroma_fe/./src/components/timeline/timeline.scss","webpack://pleroma_fe/./src/components/tab_switcher/tab_switcher.scss","webpack://pleroma_fe/./src/components/chat_title/chat_title.vue","webpack://pleroma_fe/./src/components/chat_list_item/chat_list_item.scss","webpack://pleroma_fe/./src/components/basic_user_card/basic_user_card.vue","webpack://pleroma_fe/./src/components/chat_new/chat_new.scss","webpack://pleroma_fe/./src/components/chat_list/chat_list.vue","webpack://pleroma_fe/./src/components/chat_message/chat_message.scss","webpack://pleroma_fe/./src/components/chat/chat.scss","webpack://pleroma_fe/./src/components/follow_card/follow_card.vue","webpack://pleroma_fe/./src/hocs/with_load_more/with_load_more.scss","webpack://pleroma_fe/./src/components/user_profile/user_profile.vue","webpack://pleroma_fe/./src/components/search/search.vue","webpack://pleroma_fe/./src/components/interface_language_switcher/interface_language_switcher.vue","webpack://pleroma_fe/./src/components/registration/registration.vue","webpack://pleroma_fe/./src/components/password_reset/password_reset.vue","webpack://pleroma_fe/./src/components/follow_request_card/follow_request_card.vue","webpack://pleroma_fe/./src/components/terms_of_service_panel/terms_of_service_panel.vue","webpack://pleroma_fe/./src/components/staff_panel/staff_panel.vue","webpack://pleroma_fe/./src/components/mrf_transparency_panel/mrf_transparency_panel.scss","webpack://pleroma_fe/./src/components/lists_card/lists_card.vue","webpack://pleroma_fe/./src/components/lists/lists.vue","webpack://pleroma_fe/./src/components/lists_user_search/lists_user_search.vue","webpack://pleroma_fe/./src/components/panel_loading/panel_loading.vue","webpack://pleroma_fe/./src/components/lists_edit/lists_edit.vue","webpack://pleroma_fe/./src/components/announcement_editor/announcement_editor.vue","webpack://pleroma_fe/./src/components/announcement/announcement.vue","webpack://pleroma_fe/./src/components/announcements_page/announcements_page.vue"],"sourcesContent":["\n.modal-view {\n z-index: var(--ZI_modals);\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n display: flex;\n justify-content: center;\n align-items: center;\n overflow: auto;\n pointer-events: none;\n animation-duration: 0.2s;\n animation-name: modal-background-fadein;\n opacity: 0;\n\n > * {\n pointer-events: initial;\n }\n\n &.modal-background {\n pointer-events: initial;\n background-color: rgb(0 0 0 / 50%);\n }\n\n &.open {\n opacity: 1;\n }\n}\n\n@keyframes modal-background-fadein {\n from {\n background-color: rgb(0 0 0 / 0%);\n }\n\n to {\n background-color: rgb(0 0 0 / 50%);\n }\n}\n",".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{flex:auto 0 0}.vue-recycle-scroller__item-wrapper{flex:1;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}","\n@import \"../../variables\";\n\n.login-form {\n display: flex;\n flex-direction: column;\n padding: 0.6em;\n\n .btn {\n min-height: 2em;\n width: 10em;\n }\n\n .register {\n flex: 1 1;\n }\n\n .login-bottom {\n margin-top: 1em;\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: space-between;\n }\n\n .form-group {\n display: flex;\n flex-direction: column;\n padding: 0.3em 0.5em 0.6em;\n line-height: 24px;\n }\n\n .form-bottom {\n display: flex;\n padding: 0.5em;\n height: 32px;\n\n button {\n width: 10em;\n }\n\n p {\n margin: 0.35em;\n padding: 0.35em;\n display: flex;\n }\n }\n\n .error {\n text-align: center;\n animation-name: shakeError;\n animation-duration: 0.4s;\n animation-timing-function: ease-in-out;\n }\n}\n","\n@import \"../../variables\";\n\n.media-upload {\n cursor: pointer; // We use