From c48be59f581fc6c3070a9d4cc889166b61981a6d Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Wed, 4 May 2022 22:51:40 -0400 Subject: [PATCH 01/20] Show local-only statuses in public timeline for authenticated users Ref: fix-local-public --- lib/pleroma/web/activity_pub/activity_pub.ex | 11 +++- .../controllers/timeline_controller.ex | 2 + .../controllers/status_controller_test.exs | 56 ++++++++++++++----- .../controllers/timeline_controller_test.exs | 41 ++++++++++++++ 4 files changed, 96 insertions(+), 14 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 064f93b222..f8e8405642 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -501,9 +501,18 @@ def fetch_activities(recipients, opts \\ %{}, pagination \\ :keyset) do @spec fetch_public_or_unlisted_activities(map(), Pagination.type()) :: [Activity.t()] def fetch_public_or_unlisted_activities(opts \\ %{}, pagination \\ :keyset) do + includes_local_public = Map.get(opts, :includes_local_public, false) + opts = Map.delete(opts, :user) - [Constants.as_public()] + intended_recipients = + if includes_local_public do + [Constants.as_public(), as_local_public()] + else + [Constants.as_public()] + end + + intended_recipients |> fetch_activities_query(opts) |> restrict_unlisted(opts) |> fetch_paginated_optimized(opts, pagination) diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index ba72394763..293c61b41c 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -112,6 +112,8 @@ def public(%{assigns: %{user: user}} = conn, params) do |> Map.put(:muting_user, user) |> Map.put(:reply_filtering_user, user) |> Map.put(:instance, params[:instance]) + # Restricts unfederated content to authenticated users + |> Map.put(:includes_local_public, not is_nil(user)) |> ActivityPub.fetch_public_activities() conn 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 dc6912b7bb..6d8d5f05e0 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -1901,23 +1901,53 @@ test "expires_at is nil for another user" do |> json_response_and_validate_schema(:ok) end - test "posting a local only status" do - %{user: _user, conn: conn} = oauth_access(["write:statuses"]) + describe "local-only statuses" do + test "posting a local only status" do + %{user: _user, conn: conn} = oauth_access(["write:statuses"]) - conn_one = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "status" => "cofe", - "visibility" => "local" - }) + conn_one = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "cofe", + "visibility" => "local" + }) - local = Utils.as_local_public() + local = Utils.as_local_public() - assert %{"content" => "cofe", "id" => id, "visibility" => "local"} = - json_response_and_validate_schema(conn_one, 200) + assert %{"content" => "cofe", "id" => id, "visibility" => "local"} = + json_response_and_validate_schema(conn_one, 200) - assert %Activity{id: ^id, data: %{"to" => [^local]}} = Activity.get_by_id(id) + assert %Activity{id: ^id, data: %{"to" => [^local]}} = Activity.get_by_id(id) + end + + test "other users can read local-only posts" do + user = insert(:user) + %{user: reader, conn: conn} = oauth_access(["read:statuses"]) + + {:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"}) + + received = + conn + |> get("/api/v1/statuses/#{activity.id}") + |> json_response_and_validate_schema(:ok) + + assert received["id"] == activity.id + end + + test "other users can see local-only posts" do + user = insert(:user) + %{user: _reader, conn: conn} = oauth_access(["read:statuses"]) + + {:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"}) + + received = + conn + |> get("/api/v1/statuses/#{activity.id}") + |> json_response_and_validate_schema(:ok) + + assert received["id"] == activity.id + end end describe "muted reactions" do diff --git a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs index 2c7e78595d..1328b42c95 100644 --- a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs @@ -367,6 +367,47 @@ test "muted emotions", %{conn: conn} do } ] = result end + + test "should return local-only posts for authenticated users" do + user = insert(:user) + %{user: _reader, conn: conn} = oauth_access(["read:statuses"]) + + {:ok, %{id: id}} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"}) + + result = + conn + |> get("/api/v1/timelines/public") + |> json_response_and_validate_schema(200) + + assert [%{"id" => ^id}] = result + end + + test "should not return local-only posts for users without read:statuses" do + user = insert(:user) + %{user: _reader, conn: conn} = oauth_access([]) + + {:ok, _activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"}) + + result = + conn + |> get("/api/v1/timelines/public") + |> json_response_and_validate_schema(200) + + assert [] = result + end + + test "should not return local-only posts for anonymous users" do + user = insert(:user) + + {:ok, _activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"}) + + result = + build_conn() + |> get("/api/v1/timelines/public") + |> json_response_and_validate_schema(200) + + assert [] = result + end end defp local_and_remote_activities do From 38af42968d7731ca4923a5130244638749f43ee3 Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Wed, 4 May 2022 22:58:17 -0400 Subject: [PATCH 02/20] Test that anonymous users cannot see local-only posts Ref: fix-local-public --- .../controllers/status_controller_test.exs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs index 6d8d5f05e0..d3ba9fced1 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -1923,7 +1923,7 @@ test "posting a local only status" do test "other users can read local-only posts" do user = insert(:user) - %{user: reader, conn: conn} = oauth_access(["read:statuses"]) + %{user: _reader, conn: conn} = oauth_access(["read:statuses"]) {:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"}) @@ -1935,18 +1935,15 @@ test "other users can read local-only posts" do assert received["id"] == activity.id end - test "other users can see local-only posts" do + test "anonymous users cannot see local-only posts" do user = insert(:user) - %{user: _reader, conn: conn} = oauth_access(["read:statuses"]) {:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"}) - received = - conn + _received = + build_conn() |> get("/api/v1/statuses/#{activity.id}") - |> json_response_and_validate_schema(:ok) - - assert received["id"] == activity.id + |> json_response_and_validate_schema(:not_found) end end From 826deb737588c75d9431d260eea826208100385c Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Thu, 5 May 2022 10:44:34 -0400 Subject: [PATCH 03/20] Make local-only statuses searchable Ref: fix-local-public --- lib/pleroma/activity/search.ex | 13 +++++- test/pleroma/activity/search_test.exs | 17 ++++++++ .../controllers/search_controller_test.exs | 42 +++++++++++++++++++ 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/activity/search.ex b/lib/pleroma/activity/search.ex index 694dc57098..b56d4a5aae 100644 --- a/lib/pleroma/activity/search.ex +++ b/lib/pleroma/activity/search.ex @@ -30,7 +30,7 @@ def search(user, search_query, options \\ []) do Activity |> Activity.with_preloaded_object() |> Activity.restrict_deactivated_users() - |> restrict_public() + |> restrict_public(user) |> query_with(index_type, search_query, search_function) |> maybe_restrict_local(user) |> maybe_restrict_author(author) @@ -57,7 +57,16 @@ def maybe_restrict_blocked(query, %User{} = user) do def maybe_restrict_blocked(query, _), do: query - defp restrict_public(q) do + defp restrict_public(q, user) when not is_nil(user) do + intended_recipients = [Pleroma.Constants.as_public(), Pleroma.Web.ActivityPub.Utils.as_local_public()] + + from([a, o] in q, + where: fragment("?->>'type' = 'Create'", a.data), + where: fragment("? && ?", ^intended_recipients, a.recipients) + ) + end + + defp restrict_public(q, _user) do from([a, o] in q, where: fragment("?->>'type' = 'Create'", a.data), where: ^Pleroma.Constants.as_public() in a.recipients diff --git a/test/pleroma/activity/search_test.exs b/test/pleroma/activity/search_test.exs index b8096fe733..3b5fd2c3c5 100644 --- a/test/pleroma/activity/search_test.exs +++ b/test/pleroma/activity/search_test.exs @@ -18,6 +18,23 @@ test "it finds something" do assert result.id == post.id end + test "it finds local-only posts for authenticated users" do + user = insert(:user) + reader = insert(:user) + {:ok, post} = CommonAPI.post(user, %{status: "it's wednesday my dudes", visibility: "local"}) + + [result] = Search.search(reader, "wednesday") + + assert result.id == post.id + end + + test "it does not find local-only posts for anonymous users" do + user = insert(:user) + {:ok, _post} = CommonAPI.post(user, %{status: "it's wednesday my dudes", visibility: "local"}) + + assert [] = Search.search(nil, "wednesday") + end + test "using plainto_tsquery on postgres < 11" do old_version = :persistent_term.get({Pleroma.Repo, :postgres_version}) :persistent_term.put({Pleroma.Repo, :postgres_version}, 10.0) diff --git a/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs index 8753c77164..e6599866e6 100644 --- a/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs @@ -79,6 +79,48 @@ test "search", %{conn: conn} do assert status["id"] == to_string(activity.id) end + test "search local-only status as an authenticated user" do + user = insert(:user) + %{conn: conn} = oauth_access(["read:search"]) + + {:ok, activity} = CommonAPI.post(user, %{status: "This is about 2hu private 天子", visibility: "local"}) + + results = + conn + |> get("/api/v2/search?#{URI.encode_query(%{q: "2hu"})}") + |> json_response_and_validate_schema(200) + + [status] = results["statuses"] + assert status["id"] == to_string(activity.id) + end + + test "search local-only status as an unauthenticated user" do + user = insert(:user) + %{conn: conn} = oauth_access([]) + + {:ok, _activity} = CommonAPI.post(user, %{status: "This is about 2hu private 天子", visibility: "local"}) + + results = + conn + |> get("/api/v2/search?#{URI.encode_query(%{q: "2hu"})}") + |> json_response_and_validate_schema(200) + + assert [] = results["statuses"] + end + + test "search local-only status as an anonymous user" do + user = insert(:user) + + {:ok, _activity} = CommonAPI.post(user, %{status: "This is about 2hu private 天子", visibility: "local"}) + + results = + build_conn() + |> get("/api/v2/search?#{URI.encode_query(%{q: "2hu"})}") + |> json_response_and_validate_schema(200) + + assert [] = results["statuses"] + end + @tag capture_log: true test "constructs hashtags from search query", %{conn: conn} do results = From 466568ae36fd247e635e5a1c4db2b5662eda1d02 Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Thu, 5 May 2022 11:18:18 -0400 Subject: [PATCH 04/20] Lint Ref: fix-local-public --- lib/pleroma/activity/search.ex | 5 ++++- .../mastodon_api/controllers/search_controller_test.exs | 9 ++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/activity/search.ex b/lib/pleroma/activity/search.ex index b56d4a5aae..0b9b24aa43 100644 --- a/lib/pleroma/activity/search.ex +++ b/lib/pleroma/activity/search.ex @@ -58,7 +58,10 @@ def maybe_restrict_blocked(query, %User{} = user) do def maybe_restrict_blocked(query, _), do: query defp restrict_public(q, user) when not is_nil(user) do - intended_recipients = [Pleroma.Constants.as_public(), Pleroma.Web.ActivityPub.Utils.as_local_public()] + intended_recipients = [ + Pleroma.Constants.as_public(), + Pleroma.Web.ActivityPub.Utils.as_local_public() + ] from([a, o] in q, where: fragment("?->>'type' = 'Create'", a.data), diff --git a/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs index e6599866e6..9a5d88109c 100644 --- a/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs @@ -83,7 +83,8 @@ test "search local-only status as an authenticated user" do user = insert(:user) %{conn: conn} = oauth_access(["read:search"]) - {:ok, activity} = CommonAPI.post(user, %{status: "This is about 2hu private 天子", visibility: "local"}) + {:ok, activity} = + CommonAPI.post(user, %{status: "This is about 2hu private 天子", visibility: "local"}) results = conn @@ -98,7 +99,8 @@ test "search local-only status as an unauthenticated user" do user = insert(:user) %{conn: conn} = oauth_access([]) - {:ok, _activity} = CommonAPI.post(user, %{status: "This is about 2hu private 天子", visibility: "local"}) + {:ok, _activity} = + CommonAPI.post(user, %{status: "This is about 2hu private 天子", visibility: "local"}) results = conn @@ -111,7 +113,8 @@ test "search local-only status as an unauthenticated user" do test "search local-only status as an anonymous user" do user = insert(:user) - {:ok, _activity} = CommonAPI.post(user, %{status: "This is about 2hu private 天子", visibility: "local"}) + {:ok, _activity} = + CommonAPI.post(user, %{status: "This is about 2hu private 天子", visibility: "local"}) results = build_conn() From fe933b9bf2bd9787331db3a37e6bac472eace3d5 Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Thu, 5 May 2022 18:07:30 -0400 Subject: [PATCH 05/20] Prevent remote access of local-only posts via /objects Ref: fix-local-public --- lib/pleroma/web/activity_pub/visibility.ex | 5 ++++- .../activity_pub_controller_test.exs | 21 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex index 465f8a9b7b..7c57f88f99 100644 --- a/lib/pleroma/web/activity_pub/visibility.ex +++ b/lib/pleroma/web/activity_pub/visibility.ex @@ -84,7 +84,10 @@ def visible_for_user?(%{__struct__: module} = message, user) when module in [Activity, Object] do x = [user.ap_id | User.following(user)] y = [message.data["actor"]] ++ message.data["to"] ++ (message.data["cc"] || []) - is_public?(message) || Enum.any?(x, &(&1 in y)) + + user_is_local = user.local + federatable = not is_local_public?(message) + (is_public?(message) || Enum.any?(x, &(&1 in y))) and (user_is_local || federatable) end def entire_thread_visible_for_user?(%Activity{} = activity, %User{} = user) do 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 1c5c40e848..b52c8e52e9 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -247,6 +247,27 @@ test "returns local-only objects when authenticated", %{conn: conn} do assert json_response(response, 200) == ObjectView.render("object.json", %{object: object}) end + test "does not return local-only objects for remote users", %{conn: conn} do + user = insert(:user) + reader = insert(:user, local: false) + + {:ok, post} = + CommonAPI.post(user, %{status: "test @#{reader.nickname}", visibility: "local"}) + + assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post) + + object = Object.normalize(post, fetch: false) + uuid = String.split(object.data["id"], "/") |> List.last() + + assert response = + conn + |> assign(:user, reader) + |> put_req_header("accept", "application/activity+json") + |> get("/objects/#{uuid}") + + json_response(response, 404) + end + test "it returns a json representation of the object with accept application/json", %{ conn: conn } do From 221cb3fb8125fac1757e1f1caeb36684d6c71050 Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Sat, 7 May 2022 00:20:50 -0400 Subject: [PATCH 06/20] Allow users to create backups without providing email address Ref: backup-without-email --- lib/pleroma/user/backup.ex | 18 +------- lib/pleroma/workers/backup_worker.ex | 24 ++++++++-- test/pleroma/user/backup_test.exs | 45 +++++++++++++++++-- .../controllers/backup_controller_test.exs | 20 +++++++++ 4 files changed, 82 insertions(+), 25 deletions(-) diff --git a/lib/pleroma/user/backup.ex b/lib/pleroma/user/backup.ex index 9cb3296631..9df0106057 100644 --- a/lib/pleroma/user/backup.ex +++ b/lib/pleroma/user/backup.ex @@ -32,9 +32,7 @@ defmodule Pleroma.User.Backup do end def create(user, admin_id \\ nil) do - with :ok <- validate_email_enabled(), - :ok <- validate_user_email(user), - :ok <- validate_limit(user, admin_id), + with :ok <- validate_limit(user, admin_id), {:ok, backup} <- user |> new() |> Repo.insert() do BackupWorker.process(backup, admin_id) end @@ -86,20 +84,6 @@ defp validate_limit(user, nil) do end end - defp validate_email_enabled do - if Pleroma.Config.get([Pleroma.Emails.Mailer, :enabled]) do - :ok - else - {:error, dgettext("errors", "Backups require enabled email")} - end - end - - defp validate_user_email(%User{email: nil}) do - {:error, dgettext("errors", "Email is required")} - end - - defp validate_user_email(%User{email: email}) when is_binary(email), do: :ok - def get_last(user_id) do __MODULE__ |> where(user_id: ^user_id) diff --git a/lib/pleroma/workers/backup_worker.ex b/lib/pleroma/workers/backup_worker.ex index 3caef85b7c..7657fa9ce4 100644 --- a/lib/pleroma/workers/backup_worker.ex +++ b/lib/pleroma/workers/backup_worker.ex @@ -37,10 +37,7 @@ def perform(%Job{ backup_id |> Backup.get() |> Backup.process(), {:ok, _job} <- schedule_deletion(backup), :ok <- Backup.remove_outdated(backup), - {:ok, _} <- - backup - |> Pleroma.Emails.UserEmail.backup_is_ready_email(admin_user_id) - |> Pleroma.Emails.Mailer.deliver() do + :ok <- maybe_deliver_email(backup, admin_user_id) do {:ok, backup} end end @@ -51,4 +48,23 @@ def perform(%Job{args: %{"op" => "delete", "backup_id" => backup_id}}) do nil -> :ok end end + + defp has_email?(user) do + not is_nil(user.email) and user.email != "" + end + + defp maybe_deliver_email(backup, admin_user_id) do + has_mailer = Pleroma.Config.get([Pleroma.Emails.Mailer, :enabled]) + backup = backup |> Pleroma.Repo.preload(:user) + + if has_email?(backup.user) and has_mailer do + backup + |> Pleroma.Emails.UserEmail.backup_is_ready_email(admin_user_id) + |> Pleroma.Emails.Mailer.deliver() + + :ok + else + :ok + end + end end diff --git a/test/pleroma/user/backup_test.exs b/test/pleroma/user/backup_test.exs index 6441c5ba89..5c9b940007 100644 --- a/test/pleroma/user/backup_test.exs +++ b/test/pleroma/user/backup_test.exs @@ -22,15 +22,15 @@ defmodule Pleroma.User.BackupTest do clear_config([Pleroma.Emails.Mailer, :enabled], true) end - test "it requries enabled email" do + test "it does not requrie enabled email" do clear_config([Pleroma.Emails.Mailer, :enabled], false) user = insert(:user) - assert {:error, "Backups require enabled email"} == Backup.create(user) + assert {:ok, _} = Backup.create(user) end - test "it requries user's email" do + test "it does not require user's email" do user = insert(:user, %{email: nil}) - assert {:error, "Email is required"} == Backup.create(user) + assert {:ok, _} = Backup.create(user) end test "it creates a backup record and an Oban job" do @@ -75,6 +75,43 @@ test "it process a backup record" do ) end + test "it does not send an email if the user does not have an email" do + clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) + %{id: user_id} = user = insert(:user, %{email: nil}) + + 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_no_email_sent() + end + + test "it does not send an email if mailer is not on" do + clear_config([Pleroma.Emails.Mailer, :enabled], false) + 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} = backup + + assert_no_email_sent() + end + + test "it does not send an email if the user has an empty email" do + clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) + %{id: user_id} = user = insert(:user, %{email: ""}) + + 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_no_email_sent() + end + test "it removes outdated backups after creating a fresh one" do clear_config([Backup, :limit_days], -1) clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) diff --git a/test/pleroma/web/pleroma_api/controllers/backup_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/backup_controller_test.exs index 650f3d80d3..3b4b1bfffe 100644 --- a/test/pleroma/web/pleroma_api/controllers/backup_controller_test.exs +++ b/test/pleroma/web/pleroma_api/controllers/backup_controller_test.exs @@ -82,4 +82,24 @@ test "POST /api/v1/pleroma/backups", %{user: _user, conn: conn} do |> post("/api/v1/pleroma/backups") |> json_response_and_validate_schema(400) end + + test "Backup without email address" do + user = Pleroma.Factory.insert(:user, email: nil) + %{conn: conn} = oauth_access(["read:accounts"], user: user) + + assert is_nil(user.email) + + assert [ + %{ + "content_type" => "application/zip", + "url" => _url, + "file_size" => 0, + "processed" => false, + "inserted_at" => _ + } + ] = + conn + |> post("/api/v1/pleroma/backups") + |> json_response_and_validate_schema(:ok) + end end From 38444aa92a4ae89065c138f0f0110bef4fe48ace Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Mon, 9 May 2022 15:04:51 -0400 Subject: [PATCH 07/20] Allow authenticated users to access local-only posts in MastoAPI Ref: fix-local-public --- lib/pleroma/web/activity_pub/activity_pub.ex | 7 +- ...read_visibility_to_be_local_only_aware.exs | 150 ++++++++++++++++++ .../controllers/account_controller_test.exs | 14 ++ 3 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 priv/repo/migrations/20220509180452_change_thread_visibility_to_be_local_only_aware.exs diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index f8e8405642..8e10edc245 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -612,9 +612,10 @@ defp restrict_thread_visibility(query, %{user: %User{skip_thread_containment: tr do: query defp restrict_thread_visibility(query, %{user: %User{ap_id: ap_id}}, _) do + local_public = as_local_public() from( a in query, - where: fragment("thread_visibility(?, (?)->>'id') = true", ^ap_id, a.data) + where: fragment("thread_visibility(?, (?)->>'id', ?) = true", ^ap_id, a.data, ^local_public) ) end @@ -701,8 +702,8 @@ defp fetch_activities_for_reading_user(reading_user, params) do defp user_activities_recipients(%{godmode: true}), do: [] defp user_activities_recipients(%{reading_user: reading_user}) do - if reading_user do - [Constants.as_public(), reading_user.ap_id | User.following(reading_user)] + if not is_nil(reading_user) and reading_user.local do + [Constants.as_public(), as_local_public(), reading_user.ap_id | User.following(reading_user)] else [Constants.as_public()] end diff --git a/priv/repo/migrations/20220509180452_change_thread_visibility_to_be_local_only_aware.exs b/priv/repo/migrations/20220509180452_change_thread_visibility_to_be_local_only_aware.exs new file mode 100644 index 0000000000..b514977dda --- /dev/null +++ b/priv/repo/migrations/20220509180452_change_thread_visibility_to_be_local_only_aware.exs @@ -0,0 +1,150 @@ +defmodule Pleroma.Repo.Migrations.ChangeThreadVisibilityToBeLocalOnlyAware do + use Ecto.Migration + + def up do + execute("DROP FUNCTION IF EXISTS thread_visibility(actor varchar, activity_id varchar)") + execute(update_thread_visibility()) + end + + def down do + execute("DROP FUNCTION IF EXISTS thread_visibility(actor varchar, activity_id varchar, local_public varchar)") + execute(restore_thread_visibility()) + end + + def update_thread_visibility do + """ + CREATE OR REPLACE FUNCTION thread_visibility(actor varchar, activity_id varchar, local_public varchar default '') RETURNS boolean AS $$ + DECLARE + public varchar := 'https://www.w3.org/ns/activitystreams#Public'; + child objects%ROWTYPE; + activity activities%ROWTYPE; + author_fa varchar; + valid_recipients varchar[]; + actor_user_following varchar[]; + BEGIN + --- Fetch actor following + SELECT array_agg(following.follower_address) INTO actor_user_following FROM following_relationships + JOIN users ON users.id = following_relationships.follower_id + JOIN users AS following ON following.id = following_relationships.following_id + WHERE users.ap_id = actor; + + --- Fetch our initial activity. + SELECT * INTO activity FROM activities WHERE activities.data->>'id' = activity_id; + + LOOP + --- Ensure that we have an activity before continuing. + --- If we don't, the thread is not satisfiable. + IF activity IS NULL THEN + RETURN false; + END IF; + + --- We only care about Create activities. + IF activity.data->>'type' != 'Create' THEN + RETURN true; + END IF; + + --- Normalize the child object into child. + SELECT * INTO child FROM objects + INNER JOIN activities ON COALESCE(activities.data->'object'->>'id', activities.data->>'object') = objects.data->>'id' + WHERE COALESCE(activity.data->'object'->>'id', activity.data->>'object') = objects.data->>'id'; + + --- Fetch the author's AS2 following collection. + SELECT COALESCE(users.follower_address, '') INTO author_fa FROM users WHERE users.ap_id = activity.actor; + + --- Prepare valid recipients array. + valid_recipients := ARRAY[actor, public]; + --- If we specified local public, add it. + IF local_public <> '' THEN + valid_recipients := valid_recipients || local_public; + END IF; + IF ARRAY[author_fa] && actor_user_following THEN + valid_recipients := valid_recipients || author_fa; + END IF; + + --- Check visibility. + IF NOT valid_recipients && activity.recipients THEN + --- activity not visible, break out of the loop + RETURN false; + END IF; + + --- If there's a parent, load it and do this all over again. + IF (child.data->'inReplyTo' IS NOT NULL) AND (child.data->'inReplyTo' != 'null'::jsonb) THEN + SELECT * INTO activity FROM activities + INNER JOIN objects ON COALESCE(activities.data->'object'->>'id', activities.data->>'object') = objects.data->>'id' + WHERE child.data->>'inReplyTo' = objects.data->>'id'; + ELSE + RETURN true; + END IF; + END LOOP; + END; + $$ LANGUAGE plpgsql IMMUTABLE; + """ + end + + # priv/repo/migrations/20191007073319_create_following_relationships.exs + def restore_thread_visibility do + """ + CREATE OR REPLACE FUNCTION thread_visibility(actor varchar, activity_id varchar) RETURNS boolean AS $$ + DECLARE + public varchar := 'https://www.w3.org/ns/activitystreams#Public'; + child objects%ROWTYPE; + activity activities%ROWTYPE; + author_fa varchar; + valid_recipients varchar[]; + actor_user_following varchar[]; + BEGIN + --- Fetch actor following + SELECT array_agg(following.follower_address) INTO actor_user_following FROM following_relationships + JOIN users ON users.id = following_relationships.follower_id + JOIN users AS following ON following.id = following_relationships.following_id + WHERE users.ap_id = actor; + + --- Fetch our initial activity. + SELECT * INTO activity FROM activities WHERE activities.data->>'id' = activity_id; + + LOOP + --- Ensure that we have an activity before continuing. + --- If we don't, the thread is not satisfiable. + IF activity IS NULL THEN + RETURN false; + END IF; + + --- We only care about Create activities. + IF activity.data->>'type' != 'Create' THEN + RETURN true; + END IF; + + --- Normalize the child object into child. + SELECT * INTO child FROM objects + INNER JOIN activities ON COALESCE(activities.data->'object'->>'id', activities.data->>'object') = objects.data->>'id' + WHERE COALESCE(activity.data->'object'->>'id', activity.data->>'object') = objects.data->>'id'; + + --- Fetch the author's AS2 following collection. + SELECT COALESCE(users.follower_address, '') INTO author_fa FROM users WHERE users.ap_id = activity.actor; + + --- Prepare valid recipients array. + valid_recipients := ARRAY[actor, public]; + IF ARRAY[author_fa] && actor_user_following THEN + valid_recipients := valid_recipients || author_fa; + END IF; + + --- Check visibility. + IF NOT valid_recipients && activity.recipients THEN + --- activity not visible, break out of the loop + RETURN false; + END IF; + + --- If there's a parent, load it and do this all over again. + IF (child.data->'inReplyTo' IS NOT NULL) AND (child.data->'inReplyTo' != 'null'::jsonb) THEN + SELECT * INTO activity FROM activities + INNER JOIN objects ON COALESCE(activities.data->'object'->>'id', activities.data->>'object') = objects.data->>'id' + WHERE child.data->>'inReplyTo' = objects.data->>'id'; + ELSE + RETURN true; + END IF; + END LOOP; + END; + $$ LANGUAGE plpgsql IMMUTABLE; + """ + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs index effa2144fa..bf737a9fcf 100644 --- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs @@ -407,6 +407,20 @@ test "gets users statuses", %{conn: conn} do assert id_two == to_string(activity.id) end + test "gets local-only statuses for authenticated users", %{user: _user, conn: conn} do + user_one = insert(:user) + + {:ok, activity} = CommonAPI.post(user_one, %{status: "HI!!!", visibility: "local"}) + + resp = + conn + |> get("/api/v1/accounts/#{user_one.id}/statuses") + |> json_response_and_validate_schema(200) + + assert [%{"id" => id}] = resp + assert id == to_string(activity.id) + end + test "gets an users media, excludes reblogs", %{conn: conn} do note = insert(:note_activity) user = User.get_cached_by_ap_id(note.data["actor"]) From 6e5ef7f2eb40a337107b94611bf10143f94d3d49 Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Mon, 9 May 2022 15:20:53 -0400 Subject: [PATCH 08/20] Test local-only in ap c2s outbox Ref: fix-local-public --- .../activity_pub_controller_test.exs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index b52c8e52e9..ef91066c10 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -1318,6 +1318,35 @@ test "it returns 200 even if there're no activities", %{conn: conn} do assert outbox_endpoint == result["id"] end + test "it returns a local note activity when authenticated as local user", %{conn: conn} do + user = insert(:user) + reader = insert(:user) + {:ok, note_activity} = CommonAPI.post(user, %{status: "mew mew", visibility: "local"}) + ap_id = note_activity.data["id"] + + resp = + conn + |> assign(:user, reader) + |> put_req_header("accept", "application/activity+json") + |> get("/users/#{user.nickname}/outbox?page=true") + |> json_response(200) + + assert %{"orderedItems" => [%{"id" => ^ap_id}]} = resp + end + + test "it does not return a local note activity when unauthenticated", %{conn: conn} do + user = insert(:user) + {:ok, _note_activity} = CommonAPI.post(user, %{status: "mew mew", visibility: "local"}) + + resp = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/users/#{user.nickname}/outbox?page=true") + |> json_response(200) + + assert %{"orderedItems" => []} = resp + end + test "it returns a note activity in a collection", %{conn: conn} do note_activity = insert(:note_activity) note_object = Object.normalize(note_activity, fetch: false) From f1722a9f4a0a96c6a58fe25d57928c9843f96fc8 Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Mon, 9 May 2022 15:31:26 -0400 Subject: [PATCH 09/20] Make lint happy Ref: fix-local-public --- lib/pleroma/web/activity_pub/activity_pub.ex | 7 ++++++- ...452_change_thread_visibility_to_be_local_only_aware.exs | 5 ++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 8e10edc245..c28ea5e2f2 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -613,6 +613,7 @@ defp restrict_thread_visibility(query, %{user: %User{skip_thread_containment: tr defp restrict_thread_visibility(query, %{user: %User{ap_id: ap_id}}, _) do local_public = as_local_public() + from( a in query, where: fragment("thread_visibility(?, (?)->>'id', ?) = true", ^ap_id, a.data, ^local_public) @@ -703,7 +704,11 @@ defp user_activities_recipients(%{godmode: true}), do: [] defp user_activities_recipients(%{reading_user: reading_user}) do if not is_nil(reading_user) and reading_user.local do - [Constants.as_public(), as_local_public(), reading_user.ap_id | User.following(reading_user)] + [ + Constants.as_public(), + as_local_public(), + reading_user.ap_id | User.following(reading_user) + ] else [Constants.as_public()] end diff --git a/priv/repo/migrations/20220509180452_change_thread_visibility_to_be_local_only_aware.exs b/priv/repo/migrations/20220509180452_change_thread_visibility_to_be_local_only_aware.exs index b514977dda..ea6ae6c5c8 100644 --- a/priv/repo/migrations/20220509180452_change_thread_visibility_to_be_local_only_aware.exs +++ b/priv/repo/migrations/20220509180452_change_thread_visibility_to_be_local_only_aware.exs @@ -7,7 +7,10 @@ def up do end def down do - execute("DROP FUNCTION IF EXISTS thread_visibility(actor varchar, activity_id varchar, local_public varchar)") + execute( + "DROP FUNCTION IF EXISTS thread_visibility(actor varchar, activity_id varchar, local_public varchar)" + ) + execute(restore_thread_visibility()) end From c1874bc8f943599383fe0a03f129d3113c1cf301 Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Tue, 12 Jul 2022 19:03:18 -0400 Subject: [PATCH 10/20] Make mutes and blocks behave the same as other lists --- .../controllers/account_controller.ex | 4 +- .../controllers/account_controller_test.exs | 44 ++++++++++++++----- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 50c12a1b1f..83d0f718d6 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -491,7 +491,7 @@ def mutes(%{assigns: %{user: user}} = conn, params) do users = user |> User.muted_users_relation(_restrict_deactivated = true) - |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true)) + |> Pleroma.Pagination.fetch_paginated(params) conn |> add_link_headers(users) @@ -508,7 +508,7 @@ def blocks(%{assigns: %{user: user}} = conn, params) do users = user |> User.blocked_users_relation(_restrict_deactivated = true) - |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true)) + |> Pleroma.Pagination.fetch_paginated(params) conn |> add_link_headers(users) diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs index effa2144fa..ee9db4288e 100644 --- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs @@ -1829,21 +1829,21 @@ test "getting a list of mutes" do |> get("/api/v1/mutes") |> json_response_and_validate_schema(200) - assert [id1, id2, id3] == Enum.map(result, & &1["id"]) + assert [id3, id2, id1] == Enum.map(result, & &1["id"]) result = conn |> get("/api/v1/mutes?limit=1") |> json_response_and_validate_schema(200) - assert [%{"id" => ^id1}] = result + assert [%{"id" => ^id3}] = result result = conn |> get("/api/v1/mutes?since_id=#{id1}") |> json_response_and_validate_schema(200) - assert [%{"id" => ^id2}, %{"id" => ^id3}] = result + assert [%{"id" => ^id3}, %{"id" => ^id2}] = result result = conn @@ -1857,7 +1857,7 @@ test "getting a list of mutes" do |> get("/api/v1/mutes?since_id=#{id1}&limit=1") |> json_response_and_validate_schema(200) - assert [%{"id" => ^id2}] = result + assert [%{"id" => ^id3}] = result end test "list of mutes with with_relationships parameter" do @@ -1876,7 +1876,7 @@ test "list of mutes with with_relationships parameter" do assert [ %{ - "id" => ^id1, + "id" => ^id3, "pleroma" => %{"relationship" => %{"muting" => true, "followed_by" => true}} }, %{ @@ -1884,7 +1884,7 @@ test "list of mutes with with_relationships parameter" do "pleroma" => %{"relationship" => %{"muting" => true, "followed_by" => true}} }, %{ - "id" => ^id3, + "id" => ^id1, "pleroma" => %{"relationship" => %{"muting" => true, "followed_by" => true}} } ] = @@ -1909,7 +1909,7 @@ test "getting a list of blocks" do |> get("/api/v1/blocks") |> json_response_and_validate_schema(200) - assert [id1, id2, id3] == Enum.map(result, & &1["id"]) + assert [id3, id2, id1] == Enum.map(result, & &1["id"]) result = conn @@ -1917,7 +1917,7 @@ test "getting a list of blocks" do |> get("/api/v1/blocks?limit=1") |> json_response_and_validate_schema(200) - assert [%{"id" => ^id1}] = result + assert [%{"id" => ^id3}] = result result = conn @@ -1925,7 +1925,7 @@ test "getting a list of blocks" do |> get("/api/v1/blocks?since_id=#{id1}") |> json_response_and_validate_schema(200) - assert [%{"id" => ^id2}, %{"id" => ^id3}] = result + assert [%{"id" => ^id3}, %{"id" => ^id2}] = result result = conn @@ -1941,7 +1941,31 @@ test "getting a list of blocks" do |> get("/api/v1/blocks?since_id=#{id1}&limit=1") |> json_response_and_validate_schema(200) - assert [%{"id" => ^id2}] = result + assert [%{"id" => ^id3}] = result + + conn_res = + conn + |> assign(:user, user) + |> get("/api/v1/blocks?limit=2") + + next_url = + ~r{<.+?(?/api[^>]+)>; rel=\"next\"} + |> Regex.named_captures(get_resp_header(conn_res, "link") |> Enum.at(0)) + |> Map.get("link") + + result = + conn_res + |> json_response_and_validate_schema(200) + + assert [%{"id" => ^id3}, %{"id" => ^id2}] = result + + result = + conn + |> assign(:user, user) + |> get(next_url) + |> json_response_and_validate_schema(200) + + assert [%{"id" => ^id1}] = result end test "account lookup", %{conn: conn} do From 01d396585e428ea1ca7e21868d7303a0bd8ffd6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9l=C3=A8ne?= Date: Mon, 25 Jul 2022 16:20:12 +0200 Subject: [PATCH 11/20] Emoji: implement full-qualifier using combinations This implements fully_qualify_emoji/1, which will return the fully-qualified version of an emoji if it knows of one, or return the emoji unmodified if not. This code generates combinations per emoji: for each FE0F, all possible combinations of the character being removed or staying will be generated. This is made as an attempt to find all partially-qualified and unqualified versions of a fully-qualified emoji. I have found *no cases* for which this would be a problem, after browsing the entire emoji list in emoji-test.txt. This is safe, and, sadly, most likely the sanest too. --- lib/pleroma/emoji.ex | 45 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex index 35f0da816b..3726ef1855 100644 --- a/lib/pleroma/emoji.ex +++ b/lib/pleroma/emoji.ex @@ -137,4 +137,49 @@ def is_unicode_emoji?(unquote(emoji)), do: true end def is_unicode_emoji?(_), do: false + + # FE0F is the emoji variation sequence. It is used for fully-qualifying + # emoji, and that includes emoji combinations. + # This code generates combinations per emoji: for each FE0F, all possible + # combinations of the character being removed or staying will be generated. + # This is made as an attempt to find all partially-qualified and unqualified + # versions of a fully-qualified emoji. + # I have found *no cases* for which this would be a problem, after browsing + # the entire emoji list in emoji-test.txt. This is safe, and, sadly, most + # likely sane too. + emoji_qualification_map = + emojis + |> Enum.filter(&String.contains?(&1, "\uFE0F")) + |> Enum.map(fn emoji -> + combinate = fn x, combinate -> + case x do + [] -> + [[]] + + ["\uFE0F" | tail] -> + combinate.(tail, combinate) + |> Enum.flat_map(fn x -> [x, ["\uFE0F" | x]] end) + + [codepoint | tail] -> + combinate.(tail, combinate) + |> Enum.map(fn x -> [codepoint | x] end) + end + end + + unqualified_list = + emoji + |> String.codepoints() + |> combinate.(combinate) + |> Enum.map(&List.to_string/1) + + {emoji, unqualified_list} + end) + + for {qualified, unqualified_list} <- emoji_qualification_map do + for unqualified <- unqualified_list do + def fully_qualify_emoji(unquote(unqualified)), do: unquote(qualified) + end + end + + def fully_qualify_emoji(emoji), do: emoji end From fb3f6e1975fc44414af66377061bf30ceee9f9b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9l=C3=A8ne?= Date: Mon, 25 Jul 2022 16:49:23 +0200 Subject: [PATCH 12/20] EmojiReactValidator: use new qualification method --- .../emoji_react_validator.ex | 3 +- test/fixtures/emoji-reaction-unqualified.json | 30 ------------------- .../emoji_react_handling_test.exs | 13 +++++--- 3 files changed, 10 insertions(+), 36 deletions(-) delete mode 100644 test/fixtures/emoji-reaction-unqualified.json diff --git a/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex b/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex index 2eb4f68421..0858281e54 100644 --- a/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex @@ -63,8 +63,7 @@ defp fix(data) do end defp fix_emoji_qualification(%{"content" => emoji} = data) do - # Emoji variation sequence - new_emoji = emoji <> "\uFE0F" + new_emoji = Pleroma.Emoji.fully_qualify_emoji(emoji) cond do Pleroma.Emoji.is_unicode_emoji?(emoji) -> diff --git a/test/fixtures/emoji-reaction-unqualified.json b/test/fixtures/emoji-reaction-unqualified.json deleted file mode 100644 index 722fd7092b..0000000000 --- a/test/fixtures/emoji-reaction-unqualified.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "type": "EmojiReact", - "signature": { - "type": "RsaSignature2017", - "signatureValue": "fdxMfQSMwbC6wP6sh6neS/vM5879K67yQkHTbiT5Npr5wAac0y6+o3Ij+41tN3rL6wfuGTosSBTHOtta6R4GCOOhCaCSLMZKypnp1VltCzLDoyrZELnYQIC8gpUXVmIycZbREk22qWUe/w7DAFaKK4UscBlHDzeDVcA0K3Se5Sluqi9/Zh+ldAnEzj/rSEPDjrtvf5wGNf3fHxbKSRKFt90JvKK6hS+vxKUhlRFDf6/SMETw+EhwJSNW4d10yMUakqUWsFv4Acq5LW7l+HpYMvlYY1FZhNde1+uonnCyuQDyvzkff8zwtEJmAXC4RivO/VVLa17SmqheJZfI8oluVg==", - "creator": "http://mastodon.example.org/users/admin#main-key", - "created": "2018-02-17T18:57:49Z" - }, - "object": "http://localtesting.pleroma.lol/objects/eb92579d-3417-42a8-8652-2492c2d4f454", - "content": "❤", - "nickname": "lain", - "id": "http://mastodon.example.org/users/admin#reactions/2", - "actor": "http://mastodon.example.org/users/admin", - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://w3id.org/security/v1", - { - "toot": "http://joinmastodon.org/ns#", - "sensitive": "as:sensitive", - "ostatus": "http://ostatus.org#", - "movedTo": "as:movedTo", - "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", - "inReplyToAtomUri": "ostatus:inReplyToAtomUri", - "conversation": "ostatus:conversation", - "atomUri": "ostatus:atomUri", - "Hashtag": "as:Hashtag", - "Emoji": "toot:Emoji" - } - ] -} 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 41d96fa665..9d99df27c8 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 @@ -42,11 +42,15 @@ test "it works for incoming unqualified emoji reactions" do other_user = insert(:user, local: false) {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) + # woman detective emoji, unqualified + unqualified_emoji = [0x1F575, 0x200D, 0x2640] |> List.to_string() + data = - File.read!("test/fixtures/emoji-reaction-unqualified.json") + File.read!("test/fixtures/emoji-reaction.json") |> Jason.decode!() |> Map.put("object", activity.data["object"]) |> Map.put("actor", other_user.ap_id) + |> Map.put("content", unqualified_emoji) {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) @@ -54,13 +58,14 @@ test "it works for incoming unqualified emoji reactions" do assert data["type"] == "EmojiReact" assert data["id"] == "http://mastodon.example.org/users/admin#reactions/2" assert data["object"] == activity.data["object"] - # heart emoji with added emoji variation sequence - assert data["content"] == "❤\uFE0F" + # woman detective emoji, fully qualified + emoji = [0x1F575, 0xFE0F, 0x200D, 0x2640, 0xFE0F] |> List.to_string() + assert data["content"] == emoji object = Object.get_by_ap_id(data["object"]) assert object.data["reaction_count"] == 1 - assert match?([["❤\uFE0F", _]], object.data["reactions"]) + assert match?([[emoji, _]], object.data["reactions"]) end test "it reject invalid emoji reactions" do From 5153eba3a89904f958e356aa086a6d02b4ca435e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 25 Jul 2022 19:53:01 +0200 Subject: [PATCH 13/20] Add authorized_fetch_mode to description.exs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- config/description.exs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/config/description.exs b/config/description.exs index b29348edfc..c6c6b1b5d2 100644 --- a/config/description.exs +++ b/config/description.exs @@ -1729,6 +1729,11 @@ type: :boolean, description: "Sign object fetches with HTTP signatures" }, + %{ + key: :authorized_fetch_mode, + type: :boolean, + description: "Require HTTP signatures for AP fetches" + }, %{ key: :note_replies_output_limit, type: :integer, From b99f5d61834ffd86f9e8aeca2b00c704f0a0467e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9l=C3=A8ne?= Date: Tue, 26 Jul 2022 01:38:59 +0200 Subject: [PATCH 14/20] Emoji: split qualification variation into a module --- lib/pleroma/emoji.ex | 35 ++------------------------ lib/pleroma/emoji/combinations.ex | 41 +++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 33 deletions(-) create mode 100644 lib/pleroma/emoji/combinations.ex diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex index 3726ef1855..dd65d56ae4 100644 --- a/lib/pleroma/emoji.ex +++ b/lib/pleroma/emoji.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Emoji do """ use GenServer + alias Pleroma.Emoji.Combinations alias Pleroma.Emoji.Loader require Logger @@ -138,42 +139,10 @@ def is_unicode_emoji?(unquote(emoji)), do: true def is_unicode_emoji?(_), do: false - # FE0F is the emoji variation sequence. It is used for fully-qualifying - # emoji, and that includes emoji combinations. - # This code generates combinations per emoji: for each FE0F, all possible - # combinations of the character being removed or staying will be generated. - # This is made as an attempt to find all partially-qualified and unqualified - # versions of a fully-qualified emoji. - # I have found *no cases* for which this would be a problem, after browsing - # the entire emoji list in emoji-test.txt. This is safe, and, sadly, most - # likely sane too. emoji_qualification_map = emojis |> Enum.filter(&String.contains?(&1, "\uFE0F")) - |> Enum.map(fn emoji -> - combinate = fn x, combinate -> - case x do - [] -> - [[]] - - ["\uFE0F" | tail] -> - combinate.(tail, combinate) - |> Enum.flat_map(fn x -> [x, ["\uFE0F" | x]] end) - - [codepoint | tail] -> - combinate.(tail, combinate) - |> Enum.map(fn x -> [codepoint | x] end) - end - end - - unqualified_list = - emoji - |> String.codepoints() - |> combinate.(combinate) - |> Enum.map(&List.to_string/1) - - {emoji, unqualified_list} - end) + |> Combinations.variate_emoji_qualification() for {qualified, unqualified_list} <- emoji_qualification_map do for unqualified <- unqualified_list do diff --git a/lib/pleroma/emoji/combinations.ex b/lib/pleroma/emoji/combinations.ex new file mode 100644 index 0000000000..c494664060 --- /dev/null +++ b/lib/pleroma/emoji/combinations.ex @@ -0,0 +1,41 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Emoji.Combinations do + # FE0F is the emoji variation sequence. It is used for fully-qualifying + # emoji, and that includes emoji combinations. + # This code generates combinations per emoji: for each FE0F, all possible + # combinations of the character being removed or staying will be generated. + # This is made as an attempt to find all partially-qualified and unqualified + # versions of a fully-qualified emoji. + # I have found *no cases* for which this would be a problem, after browsing + # the entire emoji list in emoji-test.txt. This is safe, and, sadly, most + # likely sane too. + + defp qualification_combinations([]), do: [[]] + + defp qualification_combinations(["\uFE0F" | tail]) do + tail + |> qualification_combinations() + |> Enum.flat_map(fn x -> [x, ["\uFE0F" | x]] end) + end + + defp qualification_combinations([codepoint | tail]) do + tail + |> qualification_combinations() + |> Enum.map(fn x -> [codepoint | x] end) + end + + def variate_emoji_qualification(emoji) when is_binary(emoji) do + emoji + |> String.codepoints() + |> qualification_combinations() + |> Enum.map(&List.to_string/1) + end + + def variate_emoji_qualification(emoji) when is_list(emoji) do + emoji + |> Enum.map(fn emoji -> {emoji, variate_emoji_qualification(emoji)} end) + end +end From 4bdd8e349c83331e5071257f547466fdd4b16f9f Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Tue, 26 Jul 2022 10:50:29 -0400 Subject: [PATCH 15/20] Extract translatable strings --- priv/gettext/config_descriptions.pot | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/priv/gettext/config_descriptions.pot b/priv/gettext/config_descriptions.pot index 2987f95fe3..9021fbfab2 100644 --- a/priv/gettext/config_descriptions.pot +++ b/priv/gettext/config_descriptions.pot @@ -5997,3 +5997,27 @@ msgstr "" msgctxt "config label at :web_push_encryption-:vapid_details > :subject" msgid "Subject" msgstr "" + +#, elixir-autogen, elixir-format +#: lib/pleroma/docs/translator.ex:5 +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 +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 +msgctxt "config label at :pleroma-:activitypub > :authorized_fetch_mode" +msgid "Authorized fetch mode" +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/pleroma/docs/translator.ex:5 +msgctxt "config label at :pleroma-:instance > :short_description" +msgid "Short description" +msgstr "" From 7167de592e3523459a1eb65d902085e828f962b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9l=C3=A8ne?= Date: Tue, 26 Jul 2022 23:15:09 +0200 Subject: [PATCH 16/20] Emoji: apply recommended tail call changes Behavior matches previous code. Co-authored-by: Tusooa Zhu --- lib/pleroma/emoji/combinations.ex | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/pleroma/emoji/combinations.ex b/lib/pleroma/emoji/combinations.ex index c494664060..981c735961 100644 --- a/lib/pleroma/emoji/combinations.ex +++ b/lib/pleroma/emoji/combinations.ex @@ -13,18 +13,22 @@ defmodule Pleroma.Emoji.Combinations do # the entire emoji list in emoji-test.txt. This is safe, and, sadly, most # likely sane too. - defp qualification_combinations([]), do: [[]] - - defp qualification_combinations(["\uFE0F" | tail]) do - tail - |> qualification_combinations() - |> Enum.flat_map(fn x -> [x, ["\uFE0F" | x]] end) + defp qualification_combinations(codepoints) do + qualification_combinations([[]], codepoints) end - defp qualification_combinations([codepoint | tail]) do - tail - |> qualification_combinations() - |> Enum.map(fn x -> [codepoint | x] end) + defp qualification_combinations(acc, []), do: acc + + defp qualification_combinations(acc, ["\uFE0F" | tail]) do + acc + |> Enum.flat_map(fn x -> [x, x ++ ["\uFE0F"]] end) + |> qualification_combinations(tail) + end + + defp qualification_combinations(acc, [codepoint | tail]) do + acc + |> Enum.map(&Kernel.++(&1, [codepoint])) + |> qualification_combinations(tail) end def variate_emoji_qualification(emoji) when is_binary(emoji) do From 5d3d6a58f72888b8714605032b417091a8891bb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sun, 31 Jul 2022 17:22:34 +0200 Subject: [PATCH 17/20] Use `duration` param for mute expiration duration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- lib/pleroma/user.ex | 8 ++--- .../api_spec/operations/account_operation.ex | 15 ++++++-- .../controllers/account_controller.ex | 4 +++ test/pleroma/user_test.exs | 2 +- .../controllers/account_controller_test.exs | 35 +++++++++++++++++++ .../mastodon_api/views/account_view_test.exs | 2 +- 6 files changed, 58 insertions(+), 8 deletions(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 18699f0c88..870e8c457b 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1480,12 +1480,12 @@ def get_recipients_from_activity(%Activity{recipients: to, actor: actor}) do {:ok, list(UserRelationship.t())} | {:error, String.t()} def mute(%User{} = muter, %User{} = mutee, params \\ %{}) do notifications? = Map.get(params, :notifications, true) - expires_in = Map.get(params, :expires_in, 0) + duration = Map.get(params, :duration, 0) expires_at = - if expires_in > 0 do + if duration > 0 do DateTime.utc_now() - |> DateTime.add(expires_in) + |> DateTime.add(duration) else nil end @@ -1499,7 +1499,7 @@ def mute(%User{} = muter, %User{} = mutee, params \\ %{}) do expires_at )) || {:ok, nil} do - if expires_in > 0 do + if duration > 0 do Pleroma.Workers.MuteExpireWorker.enqueue( "unmute_user", %{"muter_id" => muter.id, "mutee_id" => mutee.id}, diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index 4111d16133..97616f5e71 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -278,11 +278,17 @@ def mute_operation do %Schema{allOf: [BooleanLike], default: true}, "Mute notifications in addition to statuses? Defaults to `true`." ), + Operation.parameter( + :duration, + :query, + %Schema{type: :integer}, + "Expire the mute in `duration` seconds. Default 0 for infinity" + ), Operation.parameter( :expires_in, :query, %Schema{type: :integer, default: 0}, - "Expire the mute in `expires_in` seconds. Default 0 for infinity" + "Deprecated, use `duration` instead" ) ], responses: %{ @@ -877,10 +883,15 @@ defp mute_request do description: "Mute notifications in addition to statuses? Defaults to true.", default: true }, + duration: %Schema{ + type: :integer, + nullable: true, + description: "Expire the mute in `expires_in` seconds. Default 0 for infinity" + }, expires_in: %Schema{ type: :integer, nullable: true, - description: "Expire the mute in `expires_in` seconds. Default 0 for infinity", + description: "Deprecated, use `duration` instead", default: 0 } }, diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 2aeb339f04..bf931dc6b7 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -411,6 +411,10 @@ def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) d @doc "POST /api/v1/accounts/:id/mute" def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do + params = + params + |> Map.put_new(:duration, Map.get(params, :expires_in, 0)) + with {:ok, _user_relationships} <- User.mute(muter, muted, params) do render(conn, "relationship.json", user: muter, target: muted) else diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs index 408389c3a2..ce5510f243 100644 --- a/test/pleroma/user_test.exs +++ b/test/pleroma/user_test.exs @@ -1146,7 +1146,7 @@ test "expiring" do user = insert(:user) muted_user = insert(:user) - {:ok, _user_relationships} = User.mute(user, muted_user, %{expires_in: 60}) + {:ok, _user_relationships} = User.mute(user, muted_user, %{duration: 60}) assert User.mutes?(user, muted_user) worker = Pleroma.Workers.MuteExpireWorker diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs index ee9db4288e..50639e2466 100644 --- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs @@ -9,6 +9,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do alias Pleroma.Repo alias Pleroma.Tests.ObanHelpers alias Pleroma.User + alias Pleroma.UserRelationship alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.InternalFetchActor alias Pleroma.Web.CommonAPI @@ -1011,6 +1012,40 @@ test "without notifications", %{conn: conn} do assert %{"id" => _id, "muting" => false, "muting_notifications" => false} = json_response_and_validate_schema(conn, 200) end + + test "expiring", %{conn: conn, user: user} do + other_user = insert(:user) + + conn = + conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/v1/accounts/#{other_user.id}/mute", %{"duration" => "86400"}) + + assert %{"id" => _id, "muting" => true} = json_response_and_validate_schema(conn, 200) + + mute_expires_at = UserRelationship.get_mute_expire_date(user, other_user) + + assert DateTime.diff( + mute_expires_at, + DateTime.utc_now() |> DateTime.add(24 * 60 * 60) + ) in -3..3 + end + + test "falls back to expires_in", %{conn: conn, user: user} do + other_user = insert(:user) + + conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/v1/accounts/#{other_user.id}/mute", %{"expires_in" => "86400"}) + |> json_response_and_validate_schema(200) + + mute_expires_at = UserRelationship.get_mute_expire_date(user, other_user) + + assert DateTime.diff( + mute_expires_at, + DateTime.utc_now() |> DateTime.add(24 * 60 * 60) + ) in -3..3 + end end describe "pinned statuses" do diff --git a/test/pleroma/web/mastodon_api/views/account_view_test.exs b/test/pleroma/web/mastodon_api/views/account_view_test.exs index 8fa946d436..692ec8c921 100644 --- a/test/pleroma/web/mastodon_api/views/account_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/account_view_test.exs @@ -640,7 +640,7 @@ test "renders mute expiration date" do other_user = insert(:user) {:ok, _user_relationships} = - User.mute(user, other_user, %{notifications: true, expires_in: 24 * 60 * 60}) + User.mute(user, other_user, %{notifications: true, duration: 24 * 60 * 60}) %{ mute_expires_at: mute_expires_at From 5ef2dc317d49453153855f106fa098625b6e55ae Mon Sep 17 00:00:00 2001 From: Haelwenn Date: Sun, 31 Jul 2022 21:34:23 +0000 Subject: [PATCH 18/20] Change test case wording --- test/pleroma/web/twitter_api/util_controller_test.exs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/pleroma/web/twitter_api/util_controller_test.exs b/test/pleroma/web/twitter_api/util_controller_test.exs index 7d7eb39ff4..5dc72b1778 100644 --- a/test/pleroma/web/twitter_api/util_controller_test.exs +++ b/test/pleroma/web/twitter_api/util_controller_test.exs @@ -553,7 +553,7 @@ test "with proper permissions and invalid password", %{conn: conn} do assert json_response_and_validate_schema(conn, 200) == %{"error" => "Invalid password."} end - test "with proper permissions, valid password and target account does not alias this", + test "with proper permissions, valid password and target account does not alias it", %{ conn: conn } do @@ -592,7 +592,7 @@ test "with proper permissions, valid password and target account does not exist" } end - test "with proper permissions, valid password, remote target account aliases this and local cache does not exist", + test "with proper permissions, valid password, remote target account aliases it and local cache does not exist", %{} do user = insert(:user, ap_id: "https://lm.kazv.moe/users/testuser") %{user: _user, conn: conn} = oauth_access(["write:accounts"], user: user) @@ -610,7 +610,7 @@ test "with proper permissions, valid password, remote target account aliases thi assert json_response_and_validate_schema(conn, 200) == %{"status" => "success"} end - test "with proper permissions, valid password, remote target account aliases this and local cache does not alias this", + test "with proper permissions, valid password, remote target account aliases it and local cache does not aliases it", %{} do user = insert(:user, ap_id: "https://lm.kazv.moe/users/testuser") %{user: _user, conn: conn} = oauth_access(["write:accounts"], user: user) @@ -636,7 +636,7 @@ test "with proper permissions, valid password, remote target account aliases thi assert json_response_and_validate_schema(conn, 200) == %{"status" => "success"} end - test "with proper permissions, valid password, remote target account does not alias this and local cache aliases this", + test "with proper permissions, valid password, remote target account does not aliases it and local cache aliases it", %{ user: user, conn: conn @@ -665,7 +665,7 @@ test "with proper permissions, valid password, remote target account does not al } end - test "with proper permissions, valid password and target account aliases this", %{ + test "with proper permissions, valid password and target account aliases it", %{ conn: conn, user: user } do From cc533e6956de896ad4b9dedd1c2195709b491e3c Mon Sep 17 00:00:00 2001 From: tusooa Date: Sat, 23 Jul 2022 00:39:15 +0000 Subject: [PATCH 19/20] Translated using Weblate (Chinese (Simplified)) Currently translated at 18.9% (189 of 998 strings) Translation: Pleroma/Pleroma Backend (domain config_descriptions) Translate-URL: http://weblate.pleroma-dev.ebin.club/projects/pleroma/pleroma-backend-domain-config_descriptions/zh_Hans/ --- .../LC_MESSAGES/config_descriptions.po | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/priv/gettext/zh_Hans/LC_MESSAGES/config_descriptions.po b/priv/gettext/zh_Hans/LC_MESSAGES/config_descriptions.po index 8ac24948aa..b08c63b0c9 100644 --- a/priv/gettext/zh_Hans/LC_MESSAGES/config_descriptions.po +++ b/priv/gettext/zh_Hans/LC_MESSAGES/config_descriptions.po @@ -3,8 +3,8 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-07-21 04:21+0300\n" -"PO-Revision-Date: 2022-07-22 19:00+0000\n" -"Last-Translator: Yating Zhan \n" +"PO-Revision-Date: 2022-07-24 10:04+0000\n" +"Last-Translator: tusooa \n" "Language-Team: Chinese (Simplified) \n" "Language: zh_Hans\n" @@ -419,13 +419,13 @@ msgstr "包含不能直接被「Oban」解读的自定工人选项" #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-ConcurrentLimiter" msgid "Limits configuration for background tasks." -msgstr "" +msgstr "后台任务的限制的配置。" #: 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 "" +msgstr "[Oban](https://github.com/sorentwo/oban) 异步工作处理器的配置。" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -438,12 +438,15 @@ msgstr "验证码相关设定" msgctxt "config description at :pleroma-Pleroma.Captcha.Kocaptcha" msgid "Kocaptcha is a very simple captcha service with a single API endpoint, the source code is here: https://github.com/koto-bank/kocaptcha. The default endpoint (https://captcha.kotobank.ch) is hosted by the developer." msgstr "" +"Kocaptcha 是一个非常简单的验证码服务,只有一个 API 终点,源码在此: " +"https://github.com/koto-bank/kocaptcha 。默认终点( https://" +"captcha.kotobank.ch )由开发者托管。" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Emails.Mailer" msgid "Mailer-related settings" -msgstr "" +msgstr "邮递员相关设置" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -491,13 +494,13 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Uploaders.Local" msgid "Local uploader-related settings" -msgstr "" +msgstr "本地上传器相关设置" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Uploaders.S3" msgid "S3 uploader-related settings" -msgstr "" +msgstr "S3 上传器相关设置" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -509,7 +512,7 @@ msgstr "账户备份" #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Http" msgid "HTTP invalidate settings" -msgstr "" +msgstr "HTTP 无效化设置" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -557,19 +560,19 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config label at :ex_aws-:s3" msgid "S3" -msgstr "" +msgstr "S3" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config label at :logger-:console" msgid "Console Logger" -msgstr "" +msgstr "终端日志器" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config label at :logger-:ex_syslogger" msgid "ExSyslogger" -msgstr "" +msgstr "ExSyslogger" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format From 99d4823ab1a9bf646bf2b037f4e9427deb9ca264 Mon Sep 17 00:00:00 2001 From: Yating Zhan Date: Sat, 23 Jul 2022 08:50:32 +0000 Subject: [PATCH 20/20] Translated using Weblate (Chinese (Simplified)) Currently translated at 18.9% (189 of 998 strings) Translation: Pleroma/Pleroma Backend (domain config_descriptions) Translate-URL: http://weblate.pleroma-dev.ebin.club/projects/pleroma/pleroma-backend-domain-config_descriptions/zh_Hans/ --- .../LC_MESSAGES/config_descriptions.po | 132 +++++++++--------- 1 file changed, 68 insertions(+), 64 deletions(-) diff --git a/priv/gettext/zh_Hans/LC_MESSAGES/config_descriptions.po b/priv/gettext/zh_Hans/LC_MESSAGES/config_descriptions.po index b08c63b0c9..ff9ad52452 100644 --- a/priv/gettext/zh_Hans/LC_MESSAGES/config_descriptions.po +++ b/priv/gettext/zh_Hans/LC_MESSAGES/config_descriptions.po @@ -4,7 +4,7 @@ msgstr "" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-07-21 04:21+0300\n" "PO-Revision-Date: 2022-07-24 10:04+0000\n" -"Last-Translator: tusooa \n" +"Last-Translator: Yating Zhan \n" "Language-Team: Chinese (Simplified) \n" "Language: zh_Hans\n" @@ -596,7 +596,7 @@ msgstr "验证" #, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:connections_pool" msgid "Connections pool" -msgstr "" +msgstr "连接池" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -734,7 +734,7 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:mrf_hashtag" msgid "MRF Hashtag" -msgstr "" +msgstr "MRF 标签" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -878,7 +878,7 @@ msgstr "欢迎" #, elixir-autogen, elixir-format msgctxt "config label at :pleroma-:workers" msgid "Workers" -msgstr "" +msgstr "工人" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -1124,7 +1124,7 @@ msgstr "日志等级" #, elixir-autogen, elixir-format msgctxt "config description at :pleroma > :admin_token" msgid "Admin token" -msgstr "" +msgstr "管理令牌" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -1160,7 +1160,7 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:activitypub > :unfollow_blocked" msgid "Whether blocks result in people getting unfollowed" -msgstr "" +msgstr "屏蔽对象时是否同时取消对其的关注" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -1172,7 +1172,7 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:assets > :default_user_avatar" msgid "URL of the default user avatar" -msgstr "" +msgstr "默认用户头像的网址" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -1208,7 +1208,7 @@ msgstr "" #, 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 "" +msgstr "「Gun」等待连接时触发超时的上限。默认为5000ms。" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -1256,7 +1256,7 @@ msgstr "非活跃用户数量最低门槛" #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:email_notifications > :digest > :interval" msgid "Minimum interval between digest emails to one user" -msgstr "单个用户能收到摘要邮件的间隔频次" +msgstr "单个用户每次收到摘要邮件的间隔" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -1304,7 +1304,7 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:feed > :post_title > :max_length" msgid "Maximum number of characters before truncating title" -msgstr "不被折叠的用户名的字符上限" +msgstr "不被折叠的用户名的字数上限" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -1328,7 +1328,7 @@ msgstr "当被停用时,自动隐藏未被填写的标题栏" #, 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 "" +msgstr "输入背景的网址,若浏览已设定背景的用户资料时此处将不生效" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -1395,7 +1395,8 @@ msgstr "" #, 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 "" +msgstr "可见范围选项将只保留私信与用户默认,或是跟随被回复帖文的设定。这能够帮助 " +"Pleroma FE 的用户不会意外将对私信的回复设置为公开。" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -1425,7 +1426,7 @@ msgstr "" #, 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 "" +msgstr "回复的可见范围(仅关注者/不公开/公开)将默认跟随原贴文的设定" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -1443,7 +1444,7 @@ msgstr "是否展示该实例的自定义面板" #, 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 "" +msgstr "将面板与侧栏向右对齐" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -1455,19 +1456,19 @@ msgstr "" #, 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 "使用某个主题。styles.json 中已限定了可用主题" +msgstr "使用某个主题。styles.json 中已限定了可用的主题" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontends > :admin" msgid "Admin frontend" -msgstr "" +msgstr "管理员前端" #: 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 "" +msgstr "已安装的前端名称。只有包含了「名称」与「引用」数值才能被算作有效配置。" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -1509,7 +1510,7 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:frontends > :available > name" msgid "Name of the frontend." -msgstr "" +msgstr "前端名称。" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -1527,7 +1528,7 @@ msgstr "" #, 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 "" +msgstr "已安装的前端名称。只有包含了「名称」与「引用」数值才能被算作有效配置。" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -1545,13 +1546,13 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:gopher > :enabled" msgid "Enables the gopher interface" -msgstr "" +msgstr "启用 gopher 界面" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:gopher > :ip" msgid "IP address to bind to" -msgstr "" +msgstr "指定绑定IP地址" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -1569,7 +1570,7 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:hackney_pools > :federation > :max_connections" msgid "Number workers in the pool." -msgstr "" +msgstr "池内的工人数量。" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -1581,13 +1582,13 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:hackney_pools > :media" msgid "Settings for media pool." -msgstr "" +msgstr "媒体池设定。" #: 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 "" +msgstr "池内的工人数量。" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -1635,7 +1636,7 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:http > :proxy_url" msgid "Proxy URL" -msgstr "代理地址" +msgstr "代理网址" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -1683,7 +1684,7 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :account_activation_required" msgid "Require users to confirm their emails before signing in" -msgstr "" +msgstr "要求用户登陆时必须确认邮件" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -1695,13 +1696,13 @@ msgstr "用户登陆需要管理员同意" #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :account_field_name_length" msgid "An account field name maximum length. Default: 512." -msgstr "" +msgstr "单个用户信息名称的字数上限。默认为512。" #: 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 "" +msgstr "单个用户信息内容的字数上限。默认为2048。" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -1719,19 +1720,19 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :attachment_links" msgid "Enable to automatically add attachment link text to statuses" -msgstr "" +msgstr "启用此功能将自动添加附件链接至状态中" #: 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 "为一个会被新用户自动关注的(本地)用户设定昵称" +msgstr "为会被新用户自动关注的(本地)用户设定昵称" #: 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 "" +msgstr "为会自动关注每一个新用户的(本地)用户设定昵称" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -1761,7 +1762,7 @@ msgstr "创建账户的最低年龄限制。只有当需要输入生日时才生 #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :birthday_required" msgid "Require users to enter their birthday." -msgstr "" +msgstr "要求用户输入出生日期。" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -1791,7 +1792,7 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :external_user_synchronization" msgid "Enabling following/followers counters synchronization for external users" -msgstr "" +msgstr "为外部用户启用对关注者与正在关注数量的同步" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -1812,10 +1813,10 @@ msgid "Timeout (in days) of each external federation target being unreachable pr msgstr "" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format +#, elixir-autogen, elixir-format, fuzzy msgctxt "config description at :pleroma-:instance > :healthcheck" msgid "If enabled, system data will be shown on `/api/pleroma/healthcheck`" -msgstr "" +msgstr "若启用,「/api/pleroma/healthcheck」下将显示系统数据" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -1827,13 +1828,13 @@ msgstr "" #, 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 "" +msgstr "只有管理员邀请的用户方能注册(需要关闭「registrations_open」选项)" #: 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 "" +msgstr "贴文字数上限(内容警告/标题包含在内)" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -1845,13 +1846,13 @@ msgstr "" #, 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 "" +msgstr "用户资料中可展示的自定用户信息最大上限。默认为10。" #: 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 "" +msgstr "推荐账户的最大数量。设置为0将关闭该功能。" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -1923,7 +1924,7 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :name" msgid "Name of the instance" -msgstr "" +msgstr "实例名称" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -1941,13 +1942,13 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :poll_limits > :max_expiration" msgid "Maximum expiration time (in seconds)" -msgstr "" +msgstr "最大有效时间(以秒为单位)" #: 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 "单个选项的字符上限" +msgstr "单个选项的字数上限" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -1959,13 +1960,14 @@ msgstr "选项数量上限" #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :poll_limits > :min_expiration" msgid "Minimum expiration time (in seconds)" -msgstr "" +msgstr "最小有效时间(以秒为单位)" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format +#, elixir-autogen, elixir-format, fuzzy msgctxt "config description at :pleroma-:instance > :privileged_staff" msgid "Let moderators access sensitive data (e.g. updating user credentials, get password reset token, delete users, index and read private statuses and chats)" -msgstr "" +msgstr "允许管理员访问敏感信息(例,更新用户凭据、取得密码重置令牌、删除用户、能够索" +"引并阅览私密状态与聊天信息)" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -1989,13 +1991,13 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :registration_reason_length" msgid "Maximum registration reason length. Default: 500." -msgstr "注册申请理由的字符上限。默认为500。" +msgstr "申请注册理由的字数上限。默认为500。" #: 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 "" +msgstr "开放注册。若要启用邀请制注册则需关闭此项。" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -2014,12 +2016,14 @@ msgstr "" 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 "" +"启用后,只有处于私信最开头的用户名才会被提及。这将有助于防止意外提及不想要的" +"用户(例,“@admin 请留意 @bad_actor”)。默认下为关闭状态" #: lib/pleroma/docs/translator.ex:5 -#, elixir-autogen, elixir-format +#, elixir-autogen, elixir-format, fuzzy msgctxt "config description at :pleroma-:instance > :show_reactions" msgid "Let favourites and emoji reactions be viewed through the API." -msgstr "" +msgstr "允许通过此API来看见喜欢数量与表情反应。" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -2037,25 +2041,25 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:instance > :upload_limit" msgid "File size limit of uploads (except for avatar, background, banner)" -msgstr "" +msgstr "上传文件大小上限(不包括头像、背景与横幅)" #: 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 "用户自传的字符上限。默认为5000。" +msgstr "用户自传的字数上限。默认为5000。" #: 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 "用户名的字符上限。默认为100。" +msgstr "用户名的字数上限。默认为100。" #: 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 "" +msgstr "允许/不允许获取并展示实例图标" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -2151,13 +2155,13 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:manifest > :icons" msgid "Describe the icons of the app" -msgstr "" +msgstr "描述此应用的图标" #: 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 "" +msgstr "描述此应用的主题颜色" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -2187,13 +2191,13 @@ msgstr "" #, 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 "" +msgstr "图像的生成预览缩略图的长度上限(视频预览则始终保持原始尺寸)。" #: 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 "" +msgstr "图像的生成预览缩略图的宽度上限(视频预览则始终保持原始尺寸)。" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -2355,13 +2359,13 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:mrf_rejectnonpublic > :allow_direct" msgid "Whether to allow direct messages" -msgstr "" +msgstr "是否允许私信" #: 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 "" +msgstr "是否允许仅限关注者的帖文" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -2529,7 +2533,7 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config description at :pleroma-:pools > :media" msgid "Settings for media pool." -msgstr "" +msgstr "媒体池设定。" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -5475,7 +5479,7 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Captcha.Kocaptcha > :endpoint" msgid "Endpoint" -msgstr "" +msgstr "终点" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -5565,7 +5569,7 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SMTP-:password" msgid "Password" -msgstr "" +msgstr "密码" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -5697,7 +5701,7 @@ msgstr "链接颜色" #, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Emails.UserEmail > :styling > :text_color" msgid "Text color" -msgstr "" +msgstr "文本颜色" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format @@ -5955,7 +5959,7 @@ msgstr "" #, elixir-autogen, elixir-format msgctxt "config label at :pleroma-Pleroma.Workers.PurgeExpiredActivity > :enabled" msgid "Enabled" -msgstr "" +msgstr "已启用" #: lib/pleroma/docs/translator.ex:5 #, elixir-autogen, elixir-format