\n"
"Language-Team: Indonesian \n"
+"pleroma-backend-domain-errors/id/>\n"
"Language: id\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
-"X-Generator: Weblate 4.6.2\n"
+"X-Generator: Weblate 4.13.1\n"
## This file is a PO Template file.
##
@@ -41,7 +41,7 @@ msgstr "memiliki format yang tidak valid"
## From Ecto.Changeset.validate_subset/3
msgid "has an invalid entry"
-msgstr ""
+msgstr "ada yang tidak valid"
## From Ecto.Changeset.validate_exclusion/3
msgid "is reserved"
@@ -49,7 +49,7 @@ msgstr ""
## From Ecto.Changeset.validate_confirmation/3
msgid "does not match confirmation"
-msgstr ""
+msgstr "tidak sama dengan konfirmasi"
## From Ecto.Changeset.no_assoc_constraint/3
msgid "is still associated with this entry"
@@ -61,19 +61,19 @@ msgstr ""
## From Ecto.Changeset.validate_length/3
msgid "should be %{count} character(s)"
msgid_plural "should be %{count} character(s)"
-msgstr[0] "harus memiliki %{count} karakter"
+msgstr[0] "harus ada %{count} karakter"
msgid "should have %{count} item(s)"
msgid_plural "should have %{count} item(s)"
-msgstr[0] "harus memiliki %{count} item"
+msgstr[0] "harus ada %{count} item"
msgid "should be at least %{count} character(s)"
msgid_plural "should be at least %{count} character(s)"
-msgstr[0] "harus memiliki sekurang-kurangnya %{count} karakter"
+msgstr[0] "harus ada sekurang-kurangnya %{count} karakter"
msgid "should have at least %{count} item(s)"
msgid_plural "should have at least %{count} item(s)"
-msgstr[0] "harus memiliki sekurang-kurangnya %{count} item"
+msgstr[0] "harus ada sekurang-kurangnya %{count} item"
msgid "should be at most %{count} character(s)"
msgid_plural "should be at most %{count} character(s)"
@@ -112,7 +112,7 @@ msgstr "Sudah memilih"
#: lib/pleroma/web/oauth/oauth_controller.ex:359
#, elixir-format
msgid "Bad request"
-msgstr "Permintaan buruk (bad request)"
+msgstr "Permintaan buruk"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:426
#, elixir-format
@@ -133,7 +133,7 @@ msgstr "Tidak dapat mencari pengguna"
#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:61
#, elixir-format
msgid "Can't get favorites"
-msgstr "Tidak dapat mendapatkan favorit"
+msgstr "Tidak dapat mengambil favorit"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:438
#, elixir-format
@@ -143,7 +143,7 @@ msgstr "Tidak dapat menyukai objek"
#: lib/pleroma/web/common_api/utils.ex:563
#, elixir-format
msgid "Cannot post an empty status without attachments"
-msgstr "Tidak dapat memposting status kosong tanpa lampiran"
+msgstr "Tidak dapat mempos status kosong tanpa lampiran"
#: lib/pleroma/web/common_api/utils.ex:511
#, elixir-format
@@ -206,7 +206,7 @@ msgstr "CAPTCHA tidak valid"
#: lib/pleroma/web/oauth/oauth_controller.ex:568
#, elixir-format
msgid "Invalid credentials"
-msgstr "Kredensian tidak valid"
+msgstr "Kredensial tidak valid"
#: lib/pleroma/plugs/ensure_authenticated_plug.ex:38
#, elixir-format
@@ -279,12 +279,12 @@ msgstr ""
#: lib/pleroma/web/ostatus/ostatus_controller.ex:149
#, elixir-format
msgid "Something went wrong"
-msgstr "Sesuatu yang salah terjadi"
+msgstr "Ada sesuatu yang salah"
#: lib/pleroma/web/common_api/activity_draft.ex:107
#, elixir-format
msgid "The message visibility must be direct"
-msgstr "Visibilitas pesan harus langsung"
+msgstr "Ketampakan pesan harus langsung"
#: lib/pleroma/web/common_api/utils.ex:573
#, elixir-format
@@ -294,7 +294,7 @@ msgstr "Status lebih dari batas karakter"
#: lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex:31
#, elixir-format
msgid "This resource requires authentication."
-msgstr ""
+msgstr "Autentikasi diperlukan untuk hal ini."
#: lib/pleroma/plugs/rate_limiter/rate_limiter.ex:206
#, elixir-format
@@ -314,7 +314,7 @@ msgstr ""
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:485
#, elixir-format
msgid "You can't revoke your own admin status."
-msgstr ""
+msgstr "Anda tidak dapat mencabut status admin Anda."
#: lib/pleroma/web/oauth/oauth_controller.ex:221
#: lib/pleroma/web/oauth/oauth_controller.ex:308
@@ -382,7 +382,7 @@ msgstr "Gagal"
#: lib/pleroma/web/oauth/oauth_controller.ex:410
#, elixir-format
msgid "Failed to authenticate: %{message}."
-msgstr "Gagal mengotentikasi: %{message}."
+msgstr "Gagal mengautentikasi: %{message}."
#: lib/pleroma/web/oauth/oauth_controller.ex:441
#, elixir-format
@@ -392,7 +392,7 @@ msgstr "Gagal menyiapkan akun pengguna."
#: lib/pleroma/plugs/oauth_scopes_plug.ex:38
#, elixir-format
msgid "Insufficient permissions: %{permissions}."
-msgstr ""
+msgstr "Izin tidak cukup: %{permissions}."
#: lib/pleroma/plugs/uploaded_media.ex:104
#, elixir-format
@@ -418,12 +418,12 @@ msgstr ""
#: lib/pleroma/web/oauth/oauth_controller.ex:172
#, elixir-format
msgid "This action is outside the authorized scopes"
-msgstr ""
+msgstr "Tindakan ini diluar jangkauan yang terotorisasi"
#: lib/pleroma/web/oauth/fallback_controller.ex:14
#, elixir-format
msgid "Unknown error, please check the details and try again."
-msgstr "Kesalahan tidak dikenal, harap periksa keterangannya dan coba lagi."
+msgstr "Kesalahan tidak dikenal, harap periksa detailnya dan coba lagi."
#: lib/pleroma/web/oauth/oauth_controller.ex:119
#: lib/pleroma/web/oauth/oauth_controller.ex:158
@@ -444,7 +444,7 @@ msgstr ""
#: lib/pleroma/web/uploader_controller.ex:23
#, elixir-format
msgid "bad request"
-msgstr "permintaan buruk (bad request)"
+msgstr "permintaan buruk"
#: lib/pleroma/web/twitter_api/twitter_api.ex:103
#, elixir-format
@@ -469,7 +469,7 @@ msgstr "CAPTCHA Tidak Valid (Parameter kurang: %{name})"
#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:92
#, elixir-format
msgid "List not found"
-msgstr ""
+msgstr "Daftar tidak ditemukan"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:123
#, elixir-format
@@ -480,7 +480,7 @@ msgstr "Parameter kurang: %{name}"
#: lib/pleroma/web/oauth/oauth_controller.ex:321
#, elixir-format
msgid "Password reset is required"
-msgstr ""
+msgstr "Diperlukan atur ulang kata sandi"
#: lib/pleroma/tests/auth_test_controller.ex:9
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:6 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:6
@@ -522,7 +522,7 @@ msgstr ""
#: lib/pleroma/plugs/ensure_authenticated_plug.ex:28
#, elixir-format
msgid "Two-factor authentication enabled, you must use a access token."
-msgstr "Otentikasi dua-faktor diaktifkan, Anda harus menggunakan token akses."
+msgstr "Autentikasi dua langkah diaktifkan, Anda harus menggunakan token akses."
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:210
#, elixir-format
@@ -552,7 +552,7 @@ msgstr ""
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61
#, elixir-format
msgid "Web push subscription is disabled on this Pleroma instance"
-msgstr ""
+msgstr "Langganan push web dinonaktifkan untuk peladen Pleroma ini"
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:451
#, elixir-format
@@ -562,7 +562,7 @@ msgstr "Anda tidak bisa mencabut status admin/moderator Anda sendiri."
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:126
#, elixir-format
msgid "authorization required for timeline view"
-msgstr ""
+msgstr "Otorisasi diperlukan untuk tampilan linimasa"
#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:24
#, elixir-format
@@ -572,7 +572,7 @@ msgstr "Akses ditolak"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:282
#, elixir-format
msgid "This API requires an authenticated user"
-msgstr ""
+msgstr "API ini memerlukan pengguna terautentikasi"
#: lib/pleroma/plugs/user_is_admin_plug.ex:21
#, elixir-format
diff --git a/priv/gettext/id/LC_MESSAGES/posix_errors.po b/priv/gettext/id/LC_MESSAGES/posix_errors.po
new file mode 100644
index 000000000..f5ea93285
--- /dev/null
+++ b/priv/gettext/id/LC_MESSAGES/posix_errors.po
@@ -0,0 +1,163 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2025-08-11 18:40+0300\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: id\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Translate Toolkit 3.7.2\n"
+
+## This file is a PO Template file.
+##
+## `msgid`s here are often extracted from source code.
+## Add new translations manually only if they're dynamic
+## translations that can't be statically extracted.
+##
+## Run `mix gettext.extract` to bring this file up to
+## date. Leave `msgstr`s empty as changing them here as no
+## effect: edit them in PO (`.po`) files instead.
+msgid "eperm"
+msgstr ""
+
+msgid "eacces"
+msgstr ""
+
+msgid "eagain"
+msgstr ""
+
+msgid "ebadf"
+msgstr ""
+
+msgid "ebadmsg"
+msgstr ""
+
+msgid "ebusy"
+msgstr ""
+
+msgid "edeadlk"
+msgstr ""
+
+msgid "edeadlock"
+msgstr ""
+
+msgid "edquot"
+msgstr ""
+
+msgid "eexist"
+msgstr ""
+
+msgid "efault"
+msgstr ""
+
+msgid "efbig"
+msgstr ""
+
+msgid "eftype"
+msgstr ""
+
+msgid "eintr"
+msgstr ""
+
+msgid "einval"
+msgstr ""
+
+msgid "eio"
+msgstr ""
+
+msgid "eisdir"
+msgstr ""
+
+msgid "eloop"
+msgstr ""
+
+msgid "emfile"
+msgstr ""
+
+msgid "emlink"
+msgstr ""
+
+msgid "emultihop"
+msgstr ""
+
+msgid "enametoolong"
+msgstr ""
+
+msgid "enfile"
+msgstr ""
+
+msgid "enobufs"
+msgstr ""
+
+msgid "enodev"
+msgstr ""
+
+msgid "enolck"
+msgstr ""
+
+msgid "enolink"
+msgstr ""
+
+msgid "enoent"
+msgstr ""
+
+msgid "enomem"
+msgstr ""
+
+msgid "enospc"
+msgstr ""
+
+msgid "enosr"
+msgstr ""
+
+msgid "enostr"
+msgstr ""
+
+msgid "enosys"
+msgstr ""
+
+msgid "enotblk"
+msgstr ""
+
+msgid "enotdir"
+msgstr ""
+
+msgid "enotsup"
+msgstr ""
+
+msgid "enxio"
+msgstr ""
+
+msgid "eopnotsupp"
+msgstr ""
+
+msgid "eoverflow"
+msgstr ""
+
+msgid "epipe"
+msgstr ""
+
+msgid "erange"
+msgstr ""
+
+msgid "erofs"
+msgstr ""
+
+msgid "espipe"
+msgstr ""
+
+msgid "esrch"
+msgstr ""
+
+msgid "estale"
+msgstr ""
+
+msgid "etxtbsy"
+msgstr ""
+
+msgid "exdev"
+msgstr ""
diff --git a/priv/gettext/id/LC_MESSAGES/static_pages.po b/priv/gettext/id/LC_MESSAGES/static_pages.po
new file mode 100644
index 000000000..e2565888a
--- /dev/null
+++ b/priv/gettext/id/LC_MESSAGES/static_pages.po
@@ -0,0 +1,574 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2025-08-11 18:40+0300\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: id\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Translate Toolkit 3.7.2\n"
+
+## This file is a PO Template file.
+##
+## "msgid"s here are often extracted from source code.
+## Add new translations manually only if they're dynamic
+## translations that can't be statically extracted.
+##
+## Run "mix gettext.extract" to bring this file up to
+## date. Leave "msgstr"s empty as changing them here as no
+## effect: edit them in PO (.po) files instead.
+
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:9
+#, elixir-autogen, elixir-format
+msgctxt "remote follow authorization button"
+msgid "Authorize"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:2
+#, elixir-autogen, elixir-format
+msgctxt "remote follow error"
+msgid "Error fetching user"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:4
+#, elixir-autogen, elixir-format
+msgctxt "remote follow header"
+msgid "Remote follow"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:8
+#, elixir-autogen, elixir-format
+msgctxt "placeholder text for auth code entry"
+msgid "Authentication code"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:10
+#, elixir-autogen, elixir-format
+msgctxt "placeholder text for password entry"
+msgid "Password"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:8
+#, elixir-autogen, elixir-format
+msgctxt "placeholder text for username entry"
+msgid "Username"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:13
+#, elixir-autogen, elixir-format
+msgctxt "remote follow authorization button for login"
+msgid "Authorize"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:12
+#, elixir-autogen, elixir-format
+msgctxt "remote follow authorization button for mfa"
+msgid "Authorize"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:2
+#, elixir-autogen, elixir-format
+msgctxt "remote follow error"
+msgid "Error following account"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:4
+#, elixir-autogen, elixir-format
+msgctxt "remote follow header, need login"
+msgid "Log in to follow"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:4
+#, elixir-autogen, elixir-format
+msgctxt "remote follow mfa header"
+msgid "Two-factor authentication"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:4
+#, elixir-autogen, elixir-format
+msgctxt "remote follow success"
+msgid "Account followed!"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:7
+#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:7
+#, elixir-autogen, elixir-format
+msgctxt "placeholder text for account id"
+msgid "Your account ID, e.g. lain@quitter.se"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:8
+#, elixir-autogen, elixir-format
+msgctxt "remote follow authorization button for following with a remote account"
+msgid "Follow"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:2
+#, elixir-autogen, elixir-format
+msgctxt "remote follow error"
+msgid "Error: %{error}"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:4
+#, elixir-autogen, elixir-format
+msgctxt "remote follow header"
+msgid "Remotely follow %{nickname}"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:12
+#, elixir-autogen, elixir-format
+msgctxt "password reset button"
+msgid "Reset"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:4
+#, elixir-autogen, elixir-format
+msgctxt "password reset failed homepage link"
+msgid "Homepage"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:1
+#, elixir-autogen, elixir-format
+msgctxt "password reset failed message"
+msgid "Password reset failed"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:8
+#, elixir-autogen, elixir-format
+msgctxt "password reset form confirm password prompt"
+msgid "Confirmation"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:4
+#, elixir-autogen, elixir-format
+msgctxt "password reset form password prompt"
+msgid "Password"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex:1
+#, elixir-autogen, elixir-format
+msgctxt "password reset invalid token message"
+msgid "Invalid Token"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:2
+#, elixir-autogen, elixir-format
+msgctxt "password reset successful homepage link"
+msgid "Homepage"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:1
+#, elixir-autogen, elixir-format
+msgctxt "password reset successful message"
+msgid "Password changed!"
+msgstr ""
+
+#: lib/pleroma/web/templates/feed/feed/tag.atom.eex:12
+#: lib/pleroma/web/templates/feed/feed/tag.rss.eex:8
+#, elixir-autogen, elixir-format
+msgctxt "tag feed description"
+msgid "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse."
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:1
+#, elixir-autogen, elixir-format
+msgctxt "oauth authorization exists page title"
+msgid "Authorization exists"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:32
+#, elixir-autogen, elixir-format
+msgctxt "oauth authorize approve button"
+msgid "Approve"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:30
+#, elixir-autogen, elixir-format
+msgctxt "oauth authorize cancel button"
+msgid "Cancel"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:23
+#, elixir-autogen, elixir-format
+msgctxt "oauth authorize message"
+msgid "Application %{client_name} is requesting access to your account."
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:1
+#, elixir-autogen, elixir-format
+msgctxt "oauth authorized page title"
+msgid "Successfully authorized"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:1
+#, elixir-autogen, elixir-format
+msgctxt "oauth external provider page title"
+msgid "Sign in with external provider"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:13
+#, elixir-autogen, elixir-format
+msgctxt "oauth external provider sign in button"
+msgid "Sign in with %{strategy}"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:54
+#, elixir-autogen, elixir-format
+msgctxt "oauth login button"
+msgid "Log In"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:51
+#, elixir-autogen, elixir-format
+msgctxt "oauth login password prompt"
+msgid "Password"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:47
+#, elixir-autogen, elixir-format
+msgctxt "oauth login username prompt"
+msgid "Username"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:39
+#, elixir-autogen, elixir-format
+msgctxt "oauth register nickname prompt"
+msgid "Pleroma Handle"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:37
+#, elixir-autogen, elixir-format
+msgctxt "oauth register nickname unchangeable warning"
+msgid "Choose carefully! You won't be able to change this later. You will be able to change your display name, though."
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:18
+#, elixir-autogen, elixir-format
+msgctxt "oauth register page email prompt"
+msgid "Email"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:10
+#, elixir-autogen, elixir-format
+msgctxt "oauth register page fill form prompt"
+msgid "If you'd like to register a new account, please provide the details below."
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:35
+#, elixir-autogen, elixir-format
+msgctxt "oauth register page login button"
+msgid "Proceed as existing user"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:31
+#, elixir-autogen, elixir-format
+msgctxt "oauth register page login password prompt"
+msgid "Password"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:24
+#, elixir-autogen, elixir-format
+msgctxt "oauth register page login prompt"
+msgid "Alternatively, sign in to connect to existing account."
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:27
+#, elixir-autogen, elixir-format
+msgctxt "oauth register page login username prompt"
+msgid "Name or email"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:14
+#, elixir-autogen, elixir-format
+msgctxt "oauth register page nickname prompt"
+msgid "Nickname"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:22
+#, elixir-autogen, elixir-format
+msgctxt "oauth register page register button"
+msgid "Proceed as new user"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:8
+#, elixir-autogen, elixir-format
+msgctxt "oauth register page title"
+msgid "Registration Details"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:36
+#, elixir-autogen, elixir-format
+msgctxt "oauth register page title"
+msgid "This is the first time you visit! Please enter your Pleroma handle."
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex:2
+#, elixir-autogen, elixir-format
+msgctxt "oauth scopes message"
+msgid "The following permissions will be granted"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:2
+#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:2
+#, elixir-autogen, elixir-format
+msgctxt "oauth token code message"
+msgid "Token code is
%{token}"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:12
+#, elixir-autogen, elixir-format
+msgctxt "mfa auth code prompt"
+msgid "Authentication code"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:8
+#, elixir-autogen, elixir-format
+msgctxt "mfa auth page title"
+msgid "Two-factor authentication"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:23
+#, elixir-autogen, elixir-format
+msgctxt "mfa auth page use recovery code link"
+msgid "Enter a two-factor recovery code"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:20
+#, elixir-autogen, elixir-format
+msgctxt "mfa auth verify code button"
+msgid "Verify"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:8
+#, elixir-autogen, elixir-format
+msgctxt "mfa recover page title"
+msgid "Two-factor recovery"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:12
+#, elixir-autogen, elixir-format
+msgctxt "mfa recover recovery code prompt"
+msgid "Recovery code"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:23
+#, elixir-autogen, elixir-format
+msgctxt "mfa recover use 2fa code link"
+msgid "Enter a two-factor code"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:20
+#, elixir-autogen, elixir-format
+msgctxt "mfa recover verify recovery code button"
+msgid "Verify"
+msgstr ""
+
+#: lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex:8
+#, elixir-autogen, elixir-format
+msgctxt "static fe profile page remote follow button"
+msgid "Remote follow"
+msgstr ""
+
+#: lib/pleroma/web/templates/email/digest.html.eex:163
+#, elixir-autogen, elixir-format
+msgctxt "digest email header line"
+msgid "Hey %{nickname}, here is what you've missed!"
+msgstr ""
+
+#: lib/pleroma/web/templates/email/digest.html.eex:544
+#, elixir-autogen, elixir-format
+msgctxt "digest email receiver address"
+msgid "The email address you are subscribed as is %{email}. "
+msgstr ""
+
+#: lib/pleroma/web/templates/email/digest.html.eex:538
+#, elixir-autogen, elixir-format
+msgctxt "digest email sending reason"
+msgid "You have received this email because you have signed up to receive digest emails from %{instance} Pleroma instance."
+msgstr ""
+
+#: lib/pleroma/web/templates/email/digest.html.eex:547
+#, elixir-autogen, elixir-format
+msgctxt "digest email unsubscribe action"
+msgid "To unsubscribe, please go %{here}."
+msgstr ""
+
+#: lib/pleroma/web/templates/email/digest.html.eex:547
+#, elixir-autogen, elixir-format
+msgctxt "digest email unsubscribe action link text"
+msgid "here"
+msgstr ""
+
+#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_failure.html.eex:1
+#, elixir-autogen, elixir-format
+msgctxt "mailer unsubscribe failed message"
+msgid "UNSUBSCRIBE FAILURE"
+msgstr ""
+
+#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_success.html.eex:1
+#, elixir-autogen, elixir-format
+msgctxt "mailer unsubscribe successful message"
+msgid "UNSUBSCRIBE SUCCESSFUL"
+msgstr ""
+
+#: lib/pleroma/web/templates/email/digest.html.eex:385
+#, elixir-format
+msgctxt "new followers count header"
+msgid "%{count} New Follower"
+msgid_plural "%{count} New Followers"
+msgstr[0] ""
+msgstr[1] ""
+
+#: lib/pleroma/emails/user_email.ex:356
+#, elixir-autogen, elixir-format
+msgctxt "account archive email body - self-requested"
+msgid "You requested a full backup of your Pleroma account. It's ready for download:
\n%{download_url}
\n"
+msgstr ""
+
+#: lib/pleroma/emails/user_email.ex:384
+#, elixir-autogen, elixir-format
+msgctxt "account archive email subject"
+msgid "Your account archive is ready"
+msgstr ""
+
+#: lib/pleroma/emails/user_email.ex:188
+#, elixir-autogen, elixir-format
+msgctxt "approval pending email body"
+msgid "Awaiting Approval
\nYour account at %{instance_name} is being reviewed by staff. You will receive another email once your account is approved.
\n"
+msgstr ""
+
+#: lib/pleroma/emails/user_email.ex:202
+#, elixir-autogen, elixir-format
+msgctxt "approval pending email subject"
+msgid "Your account is awaiting approval"
+msgstr ""
+
+#: lib/pleroma/emails/user_email.ex:158
+#, elixir-autogen, elixir-format
+msgctxt "confirmation email body"
+msgid "Thank you for registering on %{instance_name}
\nEmail confirmation is required to activate the account.
\nPlease click the following link to activate your account.
\n"
+msgstr ""
+
+#: lib/pleroma/emails/user_email.ex:174
+#, elixir-autogen, elixir-format
+msgctxt "confirmation email subject"
+msgid "%{instance_name} account confirmation"
+msgstr ""
+
+#: lib/pleroma/emails/user_email.ex:310
+#, elixir-autogen, elixir-format
+msgctxt "digest email subject"
+msgid "Your digest from %{instance_name}"
+msgstr ""
+
+#: lib/pleroma/emails/user_email.ex:81
+#, elixir-autogen, elixir-format
+msgctxt "password reset email body"
+msgid "Reset your password at %{instance_name}
\nSomeone has requested password change for your account at %{instance_name}.
\nIf it was you, visit the following link to proceed: reset password.
\nIf it was someone else, nothing to worry about: your data is secure and your password has not been changed.
\n"
+msgstr ""
+
+#: lib/pleroma/emails/user_email.ex:98
+#, elixir-autogen, elixir-format
+msgctxt "password reset email subject"
+msgid "Password reset"
+msgstr ""
+
+#: lib/pleroma/emails/user_email.ex:215
+#, elixir-autogen, elixir-format
+msgctxt "successful registration email body"
+msgid "Hello @%{nickname},
\nYour account at %{instance_name} has been registered successfully.
\nNo further action is required to activate your account.
\n"
+msgstr ""
+
+#: lib/pleroma/emails/user_email.ex:231
+#, elixir-autogen, elixir-format
+msgctxt "successful registration email subject"
+msgid "Account registered on %{instance_name}"
+msgstr ""
+
+#: lib/pleroma/emails/user_email.ex:119
+#, elixir-autogen, elixir-format
+msgctxt "user invitation email body"
+msgid "You are invited to %{instance_name}
\n%{inviter_name} invites you to join %{instance_name}, an instance of Pleroma federated social networking platform.
\nClick the following link to register: accept invitation.
\n"
+msgstr ""
+
+#: lib/pleroma/emails/user_email.ex:136
+#, elixir-autogen, elixir-format
+msgctxt "user invitation email subject"
+msgid "Invitation to %{instance_name}"
+msgstr ""
+
+#: lib/pleroma/emails/user_email.ex:53
+#, elixir-autogen, elixir-format
+msgctxt "welcome email html body"
+msgid "Welcome to %{instance_name}!"
+msgstr ""
+
+#: lib/pleroma/emails/user_email.ex:41
+#, elixir-autogen, elixir-format
+msgctxt "welcome email subject"
+msgid "Welcome to %{instance_name}!"
+msgstr ""
+
+#: lib/pleroma/emails/user_email.ex:65
+#, elixir-autogen, elixir-format
+msgctxt "welcome email text body"
+msgid "Welcome to %{instance_name}!"
+msgstr ""
+
+#: lib/pleroma/emails/user_email.ex:368
+#, elixir-autogen, elixir-format
+msgctxt "account archive email body - admin requested"
+msgid "Admin @%{admin_nickname} requested a full backup of your Pleroma account. It's ready for download:
\n%{download_url}
\n"
+msgstr ""
+
+#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:123
+#, elixir-autogen, elixir-format
+msgctxt "remote follow error message - unknown error"
+msgid "Something went wrong."
+msgstr ""
+
+#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:67
+#, elixir-autogen, elixir-format
+msgctxt "remote follow error message - user not found"
+msgid "Could not find user"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:8
+#, elixir-autogen, elixir-format
+msgctxt "status interact authorization button"
+msgid "Interact"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:2
+#, elixir-autogen, elixir-format
+msgctxt "status interact error"
+msgid "Error: %{error}"
+msgstr ""
+
+#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:95
+#, elixir-autogen, elixir-format
+msgctxt "status interact error message - status not found"
+msgid "Could not find status"
+msgstr ""
+
+#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:144
+#, elixir-autogen, elixir-format
+msgctxt "status interact error message - unknown error"
+msgid "Something went wrong."
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:4
+#, elixir-autogen, elixir-format
+msgctxt "status interact header"
+msgid "Interacting with %{nickname}'s %{status_link}"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:4
+#, elixir-autogen, elixir-format
+msgctxt "status interact header - status link text"
+msgid "status"
+msgstr ""
diff --git a/priv/scrubbers/default.ex b/priv/scrubbers/default.ex
index 0defdc74e..cc5eba027 100644
--- a/priv/scrubbers/default.ex
+++ b/priv/scrubbers/default.ex
@@ -82,12 +82,59 @@ defmodule Pleroma.HTML.Scrubber.Default do
"recipients-inline",
"quote-inline",
"invisible",
- "ellipsis"
+ "ellipsis",
+ "mfm-tada",
+ "mfm-jelly",
+ "mfm-twitch",
+ "mfm-shake",
+ "mfm-spin",
+ "mfm-jump",
+ "mfm-bounce",
+ "mfm-flip",
+ "mfm-x2",
+ "mfm-x3",
+ "mfm-x4",
+ "mfm-scale",
+ "mfm-position",
+ "mfm-fg",
+ "mfm-bg",
+ "mfm-border",
+ "mfm-font",
+ "mfm-blur",
+ "mfm-rainbow",
+ "mfm-sparkle",
+ "mfm-rotate",
+ "mfm-ruby",
+ "mfm-unixtime",
+ # Exists in Akkoma but not Misskey?
+ "mfm-center"
])
Meta.allow_tag_with_this_attribute_values(:p, "class", ["quote-inline"])
- Meta.allow_tag_with_these_attributes(:span, ["lang"])
+ Meta.allow_tag_with_these_attributes(:span, [
+ "lang",
+ "data-mfm-speed",
+ "data-mfm-delay",
+ "data-mfm-left",
+ "data-mfm-alternate",
+ "data-mfm-x",
+ "data-mfm-y",
+ "data-mfm-h",
+ "data-mfm-v",
+ "data-mfm-color",
+ "data-mfm-width",
+ "data-mfm-style",
+ "data-mfm-radius",
+ "data-mfm-noclip",
+ "data-mfm-serif",
+ "data-mfm-monospace",
+ "data-mfm-cursive",
+ "data-mfm-fantasy",
+ "data-mfm-emoji",
+ "data-mfm-math",
+ "data-mfm-deg"
+ ])
Meta.allow_tag_with_this_attribute_values(:code, "class", ["inline"])
diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld
index 3569165a4..4e82d4b01 100644
--- a/priv/static/schemas/litepub-0.1.jsonld
+++ b/priv/static/schemas/litepub-0.1.jsonld
@@ -4,46 +4,55 @@
"https://w3id.org/security/v1",
"https://purl.archive.org/socialweb/webfinger",
{
- "Emoji": "toot:Emoji",
+ "as": "https://www.w3.org/ns/activitystreams#",
+ "ostatus": "http://ostatus.org#",
+ "schema": "http://schema.org#",
+ "vcard": "http://www.w3.org/2006/vcard/ns#",
+
+ "fedibird": "http://fedibird.com/ns#",
+ "litepub": "http://litepub.social/ns#",
+ "sm": "http://smithereen.software/ns#",
+ "toot": "http://joinmastodon.org/ns#",
+
+ "alsoKnownAs": {
+ "@id": "as:alsoKnownAs",
+ "@type": "@id"
+ },
"Hashtag": "as:Hashtag",
- "PropertyValue": "schema:PropertyValue",
+ "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+ "quoteUrl": "as:quoteUrl",
+ "sensitive": "as:sensitive",
+
"atomUri": "ostatus:atomUri",
"conversation": {
"@id": "ostatus:conversation",
"@type": "@id"
},
- "discoverable": "toot:discoverable",
- "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
- "capabilities": "litepub:capabilities",
- "ostatus": "http://ostatus.org#",
- "schema": "http://schema.org#",
- "toot": "http://joinmastodon.org/ns#",
- "fedibird": "http://fedibird.com/ns#",
+
+ "PropertyValue": "schema:PropertyValue",
"value": "schema:value",
- "sensitive": "as:sensitive",
- "litepub": "http://litepub.social/ns#",
- "invisible": "litepub:invisible",
+
+ "quoteUri": "fedibird:quoteUri",
+
+ "capabilities": "litepub:capabilities",
+ "ChatMessage": "litepub:ChatMessage",
"directMessage": "litepub:directMessage",
+ "EmojiReact": "litepub:EmojiReact",
+ "formerRepresentations": "litepub:formerRepresentations",
+ "invisible": "litepub:invisible",
"listMessage": {
"@id": "litepub:listMessage",
"@type": "@id"
},
- "quoteUrl": "as:quoteUrl",
- "quoteUri": "fedibird:quoteUri",
"oauthRegistrationEndpoint": {
"@id": "litepub:oauthRegistrationEndpoint",
"@type": "@id"
},
- "EmojiReact": "litepub:EmojiReact",
- "ChatMessage": "litepub:ChatMessage",
- "alsoKnownAs": {
- "@id": "as:alsoKnownAs",
- "@type": "@id"
- },
- "vcard": "http://www.w3.org/2006/vcard/ns#",
- "formerRepresentations": "litepub:formerRepresentations",
- "sm": "http://smithereen.software/ns#",
- "nonAnonymous": "sm:nonAnonymous"
+
+ "nonAnonymous": "sm:nonAnonymous",
+
+ "discoverable": "toot:discoverable",
+ "Emoji": "toot:Emoji"
}
]
-}
+}
\ No newline at end of file
diff --git a/test/mix/tasks/pleroma/config_test.exs b/test/mix/tasks/pleroma/config_test.exs
index ef1adc235..f672d8c13 100644
--- a/test/mix/tasks/pleroma/config_test.exs
+++ b/test/mix/tasks/pleroma/config_test.exs
@@ -144,7 +144,13 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
quarantined_instances: [],
managed_config: true,
static_dir: "instance/static/",
- allowed_post_formats: ["text/plain", "text/html", "text/markdown", "text/bbcode"],
+ allowed_post_formats: [
+ "text/plain",
+ "text/html",
+ "text/markdown",
+ "text/bbcode",
+ "text/x.misskeymarkdown"
+ ],
autofollowed_nicknames: [],
max_pinned_statuses: 1,
attachment_links: false,
diff --git a/test/pleroma/dependency_version_test.exs b/test/pleroma/dependency_version_test.exs
new file mode 100644
index 000000000..0c9c1e939
--- /dev/null
+++ b/test/pleroma/dependency_version_test.exs
@@ -0,0 +1,16 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.DependencyVersionTest do
+ use ExUnit.Case, async: true
+
+ test "uses majic 1.2" do
+ majic_version =
+ :majic
+ |> Application.spec(:vsn)
+ |> to_string()
+
+ assert Version.match?(majic_version, "~> 1.2")
+ end
+end
diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs
index 47f6f5f76..de88e5002 100644
--- a/test/pleroma/integration/mastodon_websocket_test.exs
+++ b/test/pleroma/integration/mastodon_websocket_test.exs
@@ -11,6 +11,7 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
alias Pleroma.Integration.WebsocketClient
alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.OAuth
@moduletag needs_streamer: true, capture_log: true
@@ -31,6 +32,48 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
WebsocketClient.start_link(self(), path, headers)
end
+ defp raw_websocket_handshake(qs, headers) do
+ uri = URI.parse(@path <> qs)
+ port = uri.port || 80
+ path = uri.path <> if(uri.query, do: "?" <> uri.query, else: "")
+
+ default_headers = [
+ {"host", "#{uri.host}:#{port}"},
+ {"upgrade", "websocket"},
+ {"connection", "Upgrade"},
+ {"sec-websocket-key", Base.encode64(:crypto.strong_rand_bytes(16))},
+ {"sec-websocket-version", "13"}
+ ]
+
+ request = [
+ "GET #{path} HTTP/1.1\r\n",
+ Enum.map(default_headers ++ headers, fn {name, value} -> "#{name}: #{value}\r\n" end),
+ "\r\n"
+ ]
+
+ with {:ok, socket} <-
+ :gen_tcp.connect(String.to_charlist(uri.host), port, [:binary, active: false], 1_000),
+ :ok <- :gen_tcp.send(socket, request),
+ {:ok, response} <- :gen_tcp.recv(socket, 0, 1_000) do
+ :gen_tcp.close(socket)
+ {:ok, parse_http_response(response)}
+ end
+ end
+
+ defp parse_http_response(response) do
+ [headers | _] = String.split(response, "\r\n\r\n", parts: 2)
+ [status_line | header_lines] = String.split(headers, "\r\n")
+ [_, status | _] = String.split(status_line, " ")
+
+ headers =
+ Enum.map(header_lines, fn line ->
+ [name, value] = String.split(line, ":", parts: 2)
+ {String.downcase(name), String.trim(value)}
+ end)
+
+ %{status: String.to_integer(status), headers: headers}
+ end
+
defp decode_json(json) do
with {:ok, %{"event" => event, "payload" => payload_text}} <- Jason.decode(json),
{:ok, payload} <- Jason.decode(payload_text) do
@@ -85,9 +128,7 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
assert json["payload"]
assert {:ok, json} = Jason.decode(json["payload"])
- view_json =
- Pleroma.Web.MastodonAPI.StatusView.render("show.json", activity: activity, for: nil)
- |> atom_key_to_string()
+ view_json = atom_key_to_string(StatusView.render("show.json", activity: activity, for: nil))
assert json == view_json
end
@@ -114,10 +155,7 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
assert json["payload"]
assert {:ok, json} = Jason.decode(json["payload"])
- view_json =
- Pleroma.Web.MastodonAPI.StatusView.render("show.json", activity: activity, for: nil)
- |> Jason.encode!()
- |> Jason.decode!()
+ view_json = atom_key_to_string(StatusView.render("show.json", activity: activity, for: nil))
assert json == view_json
end
@@ -279,6 +317,54 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
end)
end
+ test "echoes the Sec-WebSocket-Protocol token in the handshake", %{token: token} do
+ assert {:ok, %{status: 101, headers: headers}} =
+ raw_websocket_handshake("?stream=user", [
+ {"sec-websocket-protocol", token.token}
+ ])
+
+ assert {"sec-websocket-protocol", token.token} in headers
+ end
+
+ test "echoes the selected Sec-WebSocket-Protocol token", %{token: token} do
+ assert {:ok, %{status: 101, headers: headers}} =
+ raw_websocket_handshake("?stream=user", [
+ {"sec-websocket-protocol", "#{token.token}, phoenix"}
+ ])
+
+ assert {"sec-websocket-protocol", token.token} in headers
+ end
+
+ test "does not echo an invalid Sec-WebSocket-Protocol token", %{token: token} do
+ assert {:ok, %{status: 401, headers: headers}} =
+ raw_websocket_handshake("?stream=user", [
+ {"sec-websocket-protocol", "invalid"}
+ ])
+
+ refute {"sec-websocket-protocol", token.token} in headers
+ refute List.keymember?(headers, "sec-websocket-protocol", 0)
+ end
+
+ test "prefers sec-websocket-protocol token over query access_token", %{
+ token: token,
+ user: user
+ } do
+ assert {:ok, state} =
+ Pleroma.Web.MastodonAPI.WebsocketHandler.connect(%{
+ params: %{"stream" => "user", "access_token" => "invalid"},
+ connect_info: %{
+ sec_websocket_headers: [
+ {"sec-websocket-version", "13"},
+ {"sec-websocket-protocol", token.token}
+ ]
+ }
+ })
+
+ assert state.user.id == user.id
+ assert state.oauth_token.id == token.id
+ assert state.topics != []
+ end
+
test "accepts valid token on client-sent event", %{token: token} do
assert {:ok, pid} = start_socket()
@@ -430,12 +516,12 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
assert {:ok, json} = Jason.decode(json["payload"])
view_json =
- Pleroma.Web.MastodonAPI.StatusView.render("show.json",
- activity: activity,
- for: reading_user
+ atom_key_to_string(
+ StatusView.render("show.json",
+ activity: activity,
+ for: reading_user
+ )
)
- |> Jason.encode!()
- |> Jason.decode!()
assert json == view_json
end
@@ -458,12 +544,12 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
activity = Pleroma.Activity.normalize(activity)
view_json =
- Pleroma.Web.MastodonAPI.StatusView.render("show.json",
- activity: activity,
- for: reading_user
+ atom_key_to_string(
+ StatusView.render("show.json",
+ activity: activity,
+ for: reading_user
+ )
)
- |> Jason.encode!()
- |> Jason.decode!()
assert {:ok, %{"event" => "status.update", "payload" => ^view_json}} = decode_json(raw_json)
end
diff --git a/test/pleroma/signature_test.exs b/test/pleroma/signature_test.exs
index 572d7acc3..0c7c4c840 100644
--- a/test/pleroma/signature_test.exs
+++ b/test/pleroma/signature_test.exs
@@ -11,6 +11,7 @@ defmodule Pleroma.SignatureTest do
import Mock
alias Pleroma.Signature
+ alias Pleroma.StubbedHTTPSignaturesMock, as: HTTPSignaturesMock
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
@@ -103,6 +104,18 @@ defmodule Pleroma.SignatureTest do
end
end
+ describe "validate_signature/1" do
+ test "treats HTTP signature errors as failed validation" do
+ conn = %Plug.Conn{method: "GET", request_path: "/inbox", req_headers: []}
+
+ Mox.expect(HTTPSignaturesMock, :validate_conn, fn _conn ->
+ {:error, :request_target_header}
+ end)
+
+ assert Signature.validate_signature(conn) == false
+ end
+ end
+
describe "key_id_to_actor_id/1" do
test "it properly deduces the actor id for misskey" do
assert Signature.key_id_to_actor_id("https://example.com/users/1234/publickey") ==
diff --git a/test/pleroma/user/search_test.exs b/test/pleroma/user/search_test.exs
new file mode 100644
index 000000000..c1aca90bc
--- /dev/null
+++ b/test/pleroma/user/search_test.exs
@@ -0,0 +1,195 @@
+# Pleroma: A lightweight social networking server
+# Copyright © Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.User.SearchTest do
+ use Pleroma.DataCase, async: false
+
+ import Pleroma.Factory
+
+ alias Pleroma.Instances
+ alias Pleroma.Repo
+ alias Pleroma.User
+
+ describe "search/2 mention suggestions" do
+ test "prioritizes followed/follower users before others" do
+ user = insert(:user)
+
+ related =
+ insert(:user,
+ local: false,
+ nickname: "hj@real.example",
+ ap_id: "https://real.example/users/hj",
+ last_status_at: ~N[2020-01-01 00:00:00]
+ )
+
+ other = insert(:user, nickname: "hj", last_status_at: ~N[2020-01-02 00:00:00])
+
+ {:ok, _related, _user} = User.follow(related, user)
+
+ results = User.search("hj", for_user: user) |> Enum.map(& &1.id)
+
+ assert results == [related.id, other.id]
+ end
+
+ test "orders followed/follower users by most recent activity" do
+ user = insert(:user)
+
+ older =
+ insert(:user,
+ local: false,
+ nickname: "ali@remote.example",
+ ap_id: "https://remote.example/users/ali",
+ last_status_at: ~N[2020-01-01 00:00:00]
+ )
+
+ newer =
+ insert(:user,
+ local: false,
+ nickname: "alia@remote.example",
+ ap_id: "https://remote.example/users/alia",
+ last_status_at: ~N[2020-01-02 00:00:00]
+ )
+
+ {:ok, _user, _older} = User.follow(user, older)
+ {:ok, _user, _newer} = User.follow(user, newer)
+
+ assert [newer.id, older.id] ==
+ User.search("ali", for_user: user)
+ |> Enum.map(& &1.id)
+ end
+
+ test "groups followed/follower users first and sorts them by recency" do
+ user = insert(:user)
+
+ following_newest =
+ insert(:user,
+ local: false,
+ nickname: "mentiontesta@related.example",
+ ap_id: "https://related.example/users/mentiontesta",
+ last_status_at: ~N[2020-01-03 00:00:00]
+ )
+
+ follower_middle =
+ insert(:user,
+ local: false,
+ nickname: "mentiontestb@related.example",
+ ap_id: "https://related.example/users/mentiontestb",
+ last_status_at: ~N[2020-01-02 00:00:00]
+ )
+
+ mutual_oldest =
+ insert(:user,
+ local: false,
+ nickname: "mentiontestc@related.example",
+ ap_id: "https://related.example/users/mentiontestc",
+ last_status_at: ~N[2020-01-01 00:00:00]
+ )
+
+ unrelated_newer =
+ insert(:user,
+ local: false,
+ nickname: "mentiontestd@unrelated.example",
+ ap_id: "https://unrelated.example/users/mentiontestd",
+ last_status_at: ~N[2020-01-04 00:00:00]
+ )
+
+ {:ok, _user, _following_newest} = User.follow(user, following_newest)
+ {:ok, _follower_middle, _user} = User.follow(follower_middle, user)
+
+ {:ok, _user, _mutual_oldest} = User.follow(user, mutual_oldest)
+ {:ok, _mutual_oldest, _user} = User.follow(mutual_oldest, user)
+
+ results = User.search("mentiontest", for_user: user)
+
+ assert Enum.map(results, & &1.id) ==
+ [following_newest.id, follower_middle.id, mutual_oldest.id, unrelated_newer.id]
+ end
+
+ test "uses last_active_at when last_status_at is missing" do
+ user = insert(:user)
+
+ older =
+ insert(:user,
+ local: false,
+ nickname: "activefallbacka@remote.example",
+ ap_id: "https://remote.example/users/activefallbacka",
+ last_status_at: nil,
+ last_active_at: ~N[2020-01-01 00:00:00]
+ )
+
+ newer =
+ insert(:user,
+ local: false,
+ nickname: "activefallbackb@remote.example",
+ ap_id: "https://remote.example/users/activefallbackb",
+ last_status_at: nil,
+ last_active_at: ~N[2020-01-02 00:00:00]
+ )
+
+ {:ok, _user, _older} = User.follow(user, older)
+ {:ok, _user, _newer} = User.follow(user, newer)
+
+ assert [newer.id, older.id] ==
+ User.search("activefallback", for_user: user)
+ |> Enum.map(& &1.id)
+ end
+
+ test "does not return deactivated users even if related" do
+ user = insert(:user)
+
+ active =
+ insert(:user,
+ local: false,
+ nickname: "deactivatedtesta@remote.example",
+ ap_id: "https://remote.example/users/deactivatedtesta",
+ last_status_at: ~N[2020-01-02 00:00:00]
+ )
+
+ deactivated =
+ insert(:user,
+ local: false,
+ nickname: "deactivatedtestb@remote.example",
+ ap_id: "https://remote.example/users/deactivatedtestb",
+ last_status_at: ~N[2020-01-03 00:00:00]
+ )
+
+ {:ok, _user, _active} = User.follow(user, active)
+ {:ok, _user, _deactivated} = User.follow(user, deactivated)
+ Repo.update!(Ecto.Changeset.change(deactivated, is_active: false))
+
+ results = User.search("deactivatedtest", for_user: user) |> Enum.map(& &1.id)
+
+ assert results == [active.id]
+ end
+
+ test "does not return users from unreachable instances" do
+ user = insert(:user)
+
+ {:ok, _instance} = Instances.set_unreachable("dead.example")
+
+ dead =
+ insert(:user,
+ local: false,
+ nickname: "ali@dead.example",
+ ap_id: "https://dead.example/users/ali",
+ last_status_at: ~N[2020-01-02 00:00:00]
+ )
+
+ alive =
+ insert(:user,
+ local: false,
+ nickname: "ali@alive.example",
+ ap_id: "https://alive.example/users/ali",
+ last_status_at: ~N[2020-01-02 00:00:00]
+ )
+
+ {:ok, _user, _alive} = User.follow(user, alive)
+ {:ok, _user, _dead} = User.follow(user, dead)
+
+ results = User.search("ali", for_user: user) |> Enum.map(& &1.id)
+
+ assert results == [alive.id]
+ end
+ end
+end
diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs
index 3988c3912..68e39b3a0 100644
--- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs
+++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs
@@ -950,6 +950,50 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
refute Activity.get_by_ap_id(data["id"])
end
+ test "does not process post with Host header not for us", %{conn: conn} do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+ object_id = "https://one.com/objects/inbox-forged-note"
+
+ data = %{
+ "type" => "Create",
+ "actor" => alice.ap_id,
+ "id" => "https://one.com/activities/inbox-forged-create",
+ "context" => "https://one.com/contexts/inbox-forged-create",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => %{
+ "type" => "Note",
+ "id" => object_id,
+ "actor" => alice.ap_id,
+ "attributedTo" => alice.ap_id,
+ "context" => "https://one.com/contexts/inbox-forged-create",
+ "content" => "forged post",
+ "published" => "2024-07-25T13:33:31Z",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => []
+ }
+ }
+
+ # Plug will complain when replacing raw host header with put_req_header.
+ # The Plug way is updating conn.host, but that isn't the raw header
+ # and that isn't used in the EnsureHostMatchesPlug, because it doesn't include the port.
+ conn =
+ conn
+ |> assign_valid_signature_for_actor(alice)
+ |> delete_req_header("host")
+ |> put_req_header("content-type", "application/activity+json")
+
+ conn = %{conn | req_headers: conn.req_headers ++ [{"host", "invalid.example.com"}]}
+ conn = post(conn, "/inbox", data)
+
+ assert "Host header does not match this instance" == conn.resp_body
+ assert 400 == conn.status
+ assert true == conn.halted
+
+ refute Activity.get_by_ap_id(data["id"])
+ refute Object.get_by_ap_id(object_id)
+ end
+
test "accept follow activity", %{conn: conn} do
clear_config([:instance, :federating], true)
relay = Relay.get_actor()
diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
index c32811c5b..bf9c70fb6 100644
--- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
+++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
@@ -149,6 +149,171 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest
%{valid?: true} = ArticleNotePageValidator.cast_and_validate(note)
end
+ test "a Misskey MFM note is rendered from source content" do
+ user = insert(:user, ap_id: "https://misskey.example/users/alice")
+
+ note = %{
+ "id" => "https://misskey.example/notes/1",
+ "type" => "Note",
+ "actor" => user.ap_id,
+ "attributedTo" => user.ap_id,
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "content" => "original content",
+ "context" => Utils.generate_context_id(),
+ "source" => %{
+ "content" => "$[spin.speed=1s mfm goes here] ",
+ "mediaType" => "text/x.misskeymarkdown"
+ }
+ }
+
+ %{valid?: true, changes: %{content: content, source: source}} =
+ ArticleNotePageValidator.cast_and_validate(note)
+
+ assert source["mediaType"] == "text/x.misskeymarkdown"
+ assert content =~ ~s(class="mfm-spin")
+ assert content =~ ~s(data-mfm-speed="1s")
+ assert content =~ "mfm goes here"
+ refute content =~ "original content"
+ refute content =~ "",
+ "htmlMfm" => true,
+ "context" => Utils.generate_context_id(),
+ "source" => %{
+ "content" => String.duplicate("x", 5_001),
+ "mediaType" => "text/x.misskeymarkdown"
+ }
+ }
+
+ %{valid?: true, changes: %{content: content, htmlMfm: true, source: source}} =
+ ArticleNotePageValidator.cast_and_validate(note)
+
+ assert content == "already renderedalert('xss')"
+ refute Map.has_key?(source, "content")
+ end
+
test "a Note with validated likes collection validates" do
insert(:user, ap_id: "https://pol.social/users/mkljczk")
diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs
index c1e01557d..bedb466a4 100644
--- a/test/pleroma/web/activity_pub/transmogrifier_test.exs
+++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs
@@ -86,6 +86,43 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert activity.data["cc"] == [user.ap_id]
end
+ test "it rejects Flag activities when both reporter and reported account are remote" do
+ reporter = insert(:user, local: false, domain: "mastodon.cat")
+ reported = insert(:user, local: false, domain: "nicecrew.digital")
+
+ message = %{
+ "@context" => "https://www.w3.org/ns/activitystreams",
+ "actor" => reporter.ap_id,
+ "content" => "blocked AND reported!!!",
+ "object" => [reported.ap_id, "https://nicecrew.digital/objects/report-status"],
+ "type" => "Flag"
+ }
+
+ assert {:reject, reason} = Transmogrifier.handle_incoming(message)
+ assert reason =~ "third-party report"
+ refute "Flag" |> Pleroma.Activity.Queries.by_type() |> Pleroma.Repo.one()
+ end
+
+ test "it accepts Flag activities with just actor id as object" do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ message = %{
+ "@context" => "https://www.w3.org/ns/activitystreams",
+ "cc" => [user.ap_id],
+ "object" => user.ap_id,
+ "type" => "Flag",
+ "content" => "blocked AND reported!!!",
+ "actor" => other_user.ap_id
+ }
+
+ assert {:ok, activity} = Transmogrifier.handle_incoming(message)
+
+ assert activity.data["content"] == "blocked AND reported!!!"
+ assert activity.data["actor"] == other_user.ap_id
+ assert activity.data["cc"] == [user.ap_id]
+ end
+
test "it accepts Move activities" do
old_user = insert(:user)
new_user = insert(:user)
diff --git a/test/pleroma/web/activity_pub/utils_test.exs b/test/pleroma/web/activity_pub/utils_test.exs
index 3b77f0867..93234a015 100644
--- a/test/pleroma/web/activity_pub/utils_test.exs
+++ b/test/pleroma/web/activity_pub/utils_test.exs
@@ -180,7 +180,8 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
"https://www.w3.org/ns/activitystreams",
"http://localhost:4001/schemas/litepub-0.1.jsonld",
%{
- "@language" => "und"
+ "@language" => "und",
+ "htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm"
}
]
}
@@ -192,7 +193,8 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
"https://www.w3.org/ns/activitystreams",
"http://localhost:4001/schemas/litepub-0.1.jsonld",
%{
- "@language" => "pl"
+ "@language" => "pl",
+ "htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm"
}
]
}
diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs
index 017fac696..5d0ad4572 100644
--- a/test/pleroma/web/common_api_test.exs
+++ b/test/pleroma/web/common_api_test.exs
@@ -709,6 +709,47 @@ defmodule Pleroma.Web.CommonAPITest do
assert object.data["source"]["content"] == post
end
+ test "it renders MFM posts and marks their ActivityPub representation" do
+ user = insert(:user)
+
+ post = "$[spin.speed=1s 13:37]
"
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: post,
+ content_type: "text/x.misskeymarkdown"
+ })
+
+ object = Object.normalize(activity, fetch: false)
+
+ assert object.data["htmlMfm"] == true
+
+ assert object.data["source"] == %{
+ "content" => post,
+ "mediaType" => "text/x.misskeymarkdown"
+ }
+
+ assert object.data["content"] =~ ~s(class="mfm-spin")
+ assert object.data["content"] =~ ~s(data-mfm-speed="1s")
+ assert object.data["content"] =~ "13:37"
+ refute object.data["content"] =~ "scrub-this"
+ end
+
+ test "it falls back safely for malformed MFM" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "$[spin malformed",
+ content_type: "text/x.misskeymarkdown"
+ })
+
+ object = Object.normalize(activity, fetch: false)
+
+ refute object.data["content"] =~ ~s(class="mfm-spin")
+ assert object.data["content"] =~ "malformed"
+ end
+
test "it does not allow replies to direct messages that are not direct messages themselves" do
user = insert(:user)
diff --git a/test/pleroma/web/mastodon_api/views/poll_view_test.exs b/test/pleroma/web/mastodon_api/views/poll_view_test.exs
index 16281393d..6cb5934de 100644
--- a/test/pleroma/web/mastodon_api/views/poll_view_test.exs
+++ b/test/pleroma/web/mastodon_api/views/poll_view_test.exs
@@ -180,4 +180,179 @@ defmodule Pleroma.Web.MastodonAPI.PollViewTest do
assert result[:pleroma][:non_anonymous] == true
end
+
+ test "prefers votersCount over voters list when both are present" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "Which flavor?",
+ poll: %{options: ["chocolate", "vanilla"], expires_in: 20}
+ })
+
+ object = Object.normalize(activity, fetch: false)
+
+ voter = insert(:user)
+ {:ok, _, object} = CommonAPI.vote(object, voter, [0])
+
+ assert object.data["votersCount"] == 1
+ assert length(object.data["voters"]) == 1
+
+ object = %{
+ object
+ | data: Map.put(object.data, "votersCount", 42)
+ }
+
+ result = PollView.render("show.json", %{object: object})
+
+ assert result[:voters_count] == 42
+ end
+
+ test "falls back to voters list when votersCount is absent" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "Which flavor?",
+ poll: %{options: ["chocolate", "vanilla"], expires_in: 20}
+ })
+
+ object = Object.normalize(activity, fetch: false)
+
+ voter = insert(:user)
+ {:ok, _, object} = CommonAPI.vote(object, voter, [0])
+
+ assert length(object.data["voters"]) == 1
+
+ data = Map.delete(object.data, "votersCount")
+ object = %{object | data: data}
+
+ result = PollView.render("show.json", %{object: object})
+
+ assert result[:voters_count] == 1
+ end
+
+ test "returns 0 when both votersCount and voters are absent" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "Which flavor?",
+ poll: %{options: ["chocolate", "vanilla"], expires_in: 20}
+ })
+
+ object = Object.normalize(activity, fetch: false)
+
+ data =
+ object.data
+ |> Map.delete("votersCount")
+ |> Map.delete("voters")
+
+ object = %{object | data: data}
+
+ result = PollView.render("show.json", %{object: object})
+
+ assert result[:voters_count] == 0
+ end
+
+ test "returns 0 when voters list is empty" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "Which flavor?",
+ poll: %{options: ["chocolate", "vanilla"], expires_in: 20}
+ })
+
+ object = Object.normalize(activity, fetch: false)
+
+ data =
+ object.data
+ |> Map.delete("votersCount")
+ |> Map.put("voters", [])
+
+ object = %{object | data: data}
+
+ result = PollView.render("show.json", %{object: object})
+
+ assert result[:voters_count] == 0
+ end
+
+ test "does not inflate votersCount when same voter picks multiple options" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "Pick several",
+ poll: %{options: ["a", "b", "c"], expires_in: 20, multiple: true}
+ })
+
+ object = Object.normalize(activity, fetch: false)
+
+ voter = insert(:user)
+ {:ok, _, object} = CommonAPI.vote(object, voter, [0, 2])
+
+ assert object.data["votersCount"] == 1
+ assert length(object.data["voters"]) == 1
+ end
+
+ test "preserves votersCount from remote source when existing voter picks another option" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "Pick several",
+ poll: %{options: ["a", "b"], expires_in: 20, multiple: true}
+ })
+
+ object = Object.normalize(activity, fetch: false)
+
+ voter = insert(:user)
+ {:ok, _, object} = CommonAPI.vote(object, voter, [0, 1])
+
+ object = %{object | data: Map.put(object.data, "votersCount", 14)}
+
+ result = PollView.render("show.json", %{object: object})
+
+ assert result[:voters_count] == 14
+ end
+
+ test "returns 0 when votersCount is explicitly 0" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "Pick one",
+ poll: %{options: ["a", "b"], expires_in: 20}
+ })
+
+ object = Object.normalize(activity, fetch: false)
+
+ object = %{object | data: Map.put(object.data, "votersCount", 0)}
+
+ result = PollView.render("show.json", %{object: object})
+
+ assert result[:voters_count] == 0
+ end
+
+ test "falls back to voters list when votersCount is nil" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "Pick one",
+ poll: %{options: ["a", "b"], expires_in: 20}
+ })
+
+ object = Object.normalize(activity, fetch: false)
+
+ voter = insert(:user)
+ {:ok, _, object} = CommonAPI.vote(object, voter, [0])
+
+ object = %{object | data: Map.put(object.data, "votersCount", nil)}
+
+ result = PollView.render("show.json", %{object: object})
+
+ assert result[:voters_count] == length(object.data["voters"])
+ end
end
diff --git a/test/pleroma/web/plugs/ensure_host_matches_plug_test.exs b/test/pleroma/web/plugs/ensure_host_matches_plug_test.exs
new file mode 100644
index 000000000..8ace74dfb
--- /dev/null
+++ b/test/pleroma/web/plugs/ensure_host_matches_plug_test.exs
@@ -0,0 +1,121 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2026 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.EnsureHostMatchesPlugTest do
+ use Pleroma.Web.ConnCase
+
+ alias Pleroma.Web.Endpoint
+ alias Pleroma.Web.Plugs.EnsureHostMatchesPlug
+
+ import Plug.Conn
+ import Tesla.Mock
+
+ setup do
+ mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
+ :ok
+ end
+
+ defp set_host(conn, host), do: %{conn | req_headers: conn.req_headers ++ [{"host", host}]}
+
+ describe "EnsureHostMatchesPlug" do
+ setup do
+ conn = build_conn(:post, "/cofe") |> assign(:valid_signature, true)
+ [conn: conn]
+ end
+
+ test "gracefully handles no Host header", %{conn: conn} do
+ conn = EnsureHostMatchesPlug.call(conn, %{})
+
+ assert conn.status == 400
+ assert conn.halted == true
+ assert conn.resp_body == "Host header not provided"
+ end
+
+ test "gracefully handles empty Host header", %{conn: conn} do
+ conn =
+ conn
+ |> set_host("")
+ |> EnsureHostMatchesPlug.call(%{})
+
+ assert conn.status == 400
+ assert conn.halted == true
+ assert conn.resp_body == "Host header not provided"
+ end
+
+ test "it rejects Host header not matching Endpoint URL", %{conn: conn} do
+ conn =
+ conn
+ |> set_host("invalid.example.com")
+ |> EnsureHostMatchesPlug.call(%{})
+
+ assert conn.status == 400
+ assert conn.halted == true
+ assert conn.resp_body == "Host header does not match this instance"
+ end
+
+ test "it rejects Host header not matching Endpoint with port", %{conn: conn} do
+ endpoint = URI.parse(Endpoint.url())
+
+ conn =
+ conn
+ |> set_host("invalid.example.com:#{endpoint.port}")
+ |> EnsureHostMatchesPlug.call(%{})
+
+ assert conn.status == 400
+ assert conn.halted == true
+ assert conn.resp_body == "Host header does not match this instance"
+ end
+
+ test "it rejects Host header not matching Endpoint port", %{conn: conn} do
+ endpoint = URI.parse(Endpoint.url())
+
+ conn =
+ conn
+ |> set_host("#{endpoint.host}:25")
+ |> EnsureHostMatchesPlug.call(%{})
+
+ assert conn.status == 400
+ assert conn.halted == true
+ assert conn.resp_body == "Host header does not match this instance"
+ end
+
+ test "it rejects multiple Host headers", %{conn: conn} do
+ conn =
+ conn
+ |> set_host("host1.example.com")
+ |> set_host("host2.example.com")
+ |> EnsureHostMatchesPlug.call(%{})
+
+ assert conn.status == 400
+ assert conn.halted == true
+ assert conn.resp_body == "More than one Host header provided"
+ end
+
+ test "it works for Host header without port", %{conn: conn} do
+ endpoint = URI.parse(Endpoint.url())
+
+ conn =
+ conn
+ |> set_host("#{endpoint.host}")
+ |> EnsureHostMatchesPlug.call(%{})
+
+ assert conn.halted == false
+ assert Map.get(conn.assigns, :valid_host_header, nil)
+ end
+
+ test "it works for Host header with port same as Endpoint", %{
+ conn: conn
+ } do
+ endpoint = URI.parse(Endpoint.url())
+
+ conn =
+ conn
+ |> set_host("#{endpoint.host}:#{endpoint.port}")
+ |> EnsureHostMatchesPlug.call(%{})
+
+ assert conn.halted == false
+ assert Map.get(conn.assigns, :valid_host_header, nil)
+ end
+ end
+end
diff --git a/test/pleroma/web/plugs/remote_ip_test.exs b/test/pleroma/web/plugs/remote_ip_test.exs
index 37b751370..19e786f8a 100644
--- a/test/pleroma/web/plugs/remote_ip_test.exs
+++ b/test/pleroma/web/plugs/remote_ip_test.exs
@@ -106,4 +106,38 @@ defmodule Pleroma.Web.Plugs.RemoteIpTest do
assert conn.remote_ip == {1, 1, 1, 1}
end
+
+ test "reserved ranges are configurable" do
+ clear_config([RemoteIp, :reserved], [])
+
+ conn =
+ conn(:get, "/")
+ |> put_req_header("x-forwarded-for", "1.1.1.1, 10.0.0.3")
+ |> RemoteIp.call(nil)
+
+ assert conn.remote_ip == {10, 0, 0, 3}
+ end
+
+ test "clients override reserved ranges" do
+ clear_config([RemoteIp, :clients], ["10.0.0.0/8"])
+
+ conn =
+ conn(:get, "/")
+ |> put_req_header("x-forwarded-for", "1.1.1.1, 10.0.0.3")
+ |> RemoteIp.call(nil)
+
+ assert conn.remote_ip == {10, 0, 0, 3}
+ end
+
+ test "clients override proxies" do
+ clear_config([RemoteIp, :clients], ["10.0.0.3"])
+ clear_config([RemoteIp, :proxies], ["10.0.0.0/8"])
+
+ conn =
+ conn(:get, "/")
+ |> put_req_header("x-forwarded-for", "1.1.1.1, 10.0.0.3")
+ |> RemoteIp.call(nil)
+
+ assert conn.remote_ip == {10, 0, 0, 3}
+ end
end
diff --git a/test/pleroma/web/rich_media/backfill_test.exs b/test/pleroma/web/rich_media/backfill_test.exs
index 6d221fcf5..071f9b48a 100644
--- a/test/pleroma/web/rich_media/backfill_test.exs
+++ b/test/pleroma/web/rich_media/backfill_test.exs
@@ -5,12 +5,22 @@
defmodule Pleroma.Web.RichMedia.BackfillTest do
use Pleroma.DataCase
+ alias Pleroma.Tests.ObanHelpers
+ alias Pleroma.Web.CommonAPI
alias Pleroma.Web.RichMedia.Backfill
alias Pleroma.Web.RichMedia.Card
import Mox
+ import Pleroma.Factory
- setup_all do: clear_config([:rich_media, :enabled], true)
+ setup do
+ clear_config([:rich_media, :enabled], true)
+
+ Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig)
+ Mox.stub_with(Pleroma.CachexMock, Pleroma.NullCache)
+
+ :ok
+ end
test "sets a negative cache entry for an error" do
url = "https://bad.example.com/"
@@ -23,4 +33,139 @@ defmodule Pleroma.Web.RichMedia.BackfillTest do
Backfill.run(%{"url" => url})
end
+
+ test "sets a warm_cache entry" do
+ url = "https://good.example.com/"
+ url_hash = Card.url_to_hash(url)
+
+ Tesla.Mock.mock(fn %{url: ^url} ->
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: ""
+ }}
+ end)
+
+ Pleroma.CachexMock
+ |> expect(:put, fn :rich_media_cache,
+ ^url_hash,
+ %Pleroma.Web.RichMedia.Card{url_hash: ^url_hash} ->
+ {:ok, true}
+ end)
+
+ Backfill.run(%{"url" => url})
+ end
+
+ test "streams out update when stream == true" do
+ url = "https://example.com"
+ user = insert(:user)
+
+ Tesla.Mock.mock(fn %{url: ^url} ->
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: ""
+ }}
+ end)
+
+ {:ok, activity} = CommonAPI.post(user, %{status: "#cofe #{url}"})
+
+ Pleroma.CachexMock
+ |> expect(:put, fn :rich_media_cache, _, _ -> {:ok, true} end)
+
+ Pleroma.Web.ActivityPub.ActivityPubMock
+ |> expect(:stream_out, fn %Pleroma.Activity{id: id} ->
+ assert id == activity.id
+ :ok
+ end)
+
+ Backfill.run(%{"url" => url, "activity_id" => activity.id, "stream" => true})
+ end
+
+ test "does not stream out update when stream == false" do
+ url = "https://example.com"
+ user = insert(:user)
+
+ Tesla.Mock.mock(fn %{url: ^url} ->
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: ""
+ }}
+ end)
+
+ {:ok, activity} = CommonAPI.post(user, %{status: "#cofe #{url}"})
+
+ Pleroma.CachexMock
+ |> expect(:put, fn :rich_media_cache, _, _ -> {:ok, true} end)
+
+ Pleroma.Web.ActivityPub.ActivityPubMock
+ |> deny(:stream_out, 1)
+
+ Backfill.run(%{"url" => url, "activity_id" => "#{activity.data["id"]}", "stream" => false})
+ end
+
+ # NOTE: Below two MastoAPI tests cover almost the same code paths.
+ # index.json will always prefetch rich media, while show.json will try to get the card and
+ # fetch it when it isn't cached (both use Card.get_by_activity in the end).
+ # So if index.json doesn't fetch the rich media, show.json will when it renders the post,
+ # hence why index.json test will only call ActivityPub.stream_out twice,
+ # if streaming is re-enabled for in both.
+ test "does not stream out in MastoAPI StatusView index" do
+ url = "https://example.com"
+ user = insert(:user)
+
+ Tesla.Mock.mock(fn %{url: ^url} ->
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: ""
+ }}
+ end)
+
+ # CommonAPI federation processing will stream out once as a new post
+ Pleroma.Web.ActivityPub.ActivityPubMock
+ |> expect(:stream_out, 1, fn _ -> :ok end)
+
+ {:ok, activity} = CommonAPI.post(user, %{status: "#cofe #{url}"})
+ ObanHelpers.perform_all()
+
+ # Clear cache to force backfill below
+ Pleroma.Activity.HTML.invalidate_cache_for(activity.id)
+ Pleroma.Web.RichMedia.Card.delete(url)
+
+ Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
+ activities: [activity],
+ as: :activity
+ })
+
+ ObanHelpers.perform_all()
+ end
+
+ test "does not stream out in MastoAPI StatusView show" do
+ url = "https://example.com"
+ user = insert(:user)
+
+ Tesla.Mock.mock(fn %{url: ^url} ->
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: ""
+ }}
+ end)
+
+ # CommonAPI federation processing will stream out once as a new post
+ Pleroma.Web.ActivityPub.ActivityPubMock
+ |> expect(:stream_out, 1, fn _ -> :ok end)
+
+ {:ok, activity} = CommonAPI.post(user, %{status: "#cofe #{url}"})
+ ObanHelpers.perform_all()
+
+ # Clear cache to force backfill below
+ Pleroma.Activity.HTML.invalidate_cache_for(activity.id)
+ Pleroma.Web.RichMedia.Card.delete(url)
+
+ Pleroma.Web.MastodonAPI.StatusView.render("show.json", activity: activity)
+ ObanHelpers.perform_all()
+ end
end
diff --git a/test/pleroma/web/static_fe/static_fe_controller_test.exs b/test/pleroma/web/static_fe/static_fe_controller_test.exs
index 2fae83305..68ded6906 100644
--- a/test/pleroma/web/static_fe/static_fe_controller_test.exs
+++ b/test/pleroma/web/static_fe/static_fe_controller_test.exs
@@ -28,6 +28,15 @@ defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do
assert html_response(conn, 200) =~ user.nickname
end
+ test "renders profile HTML inside the default app layout", %{conn: conn, user: user} do
+ conn = get(conn, "/users/#{user.nickname}")
+
+ html = html_response(conn, 200)
+ assert html =~ ""
+ assert html =~ ~s(class="instance-header")
+ assert html =~ ~s()
+ end
+
test "404 when user not found", %{conn: conn} do
conn = get(conn, "/users/limpopo")
diff --git a/test/pleroma/workers/signature_retry_worker_test.exs b/test/pleroma/workers/signature_retry_worker_test.exs
index 94dd5f6c1..3806ecac9 100644
--- a/test/pleroma/workers/signature_retry_worker_test.exs
+++ b/test/pleroma/workers/signature_retry_worker_test.exs
@@ -16,12 +16,13 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do
alias Pleroma.Signature
alias Pleroma.User
alias Pleroma.Web.ActivityPub.UserView
+ alias Pleroma.Web.Endpoint
alias Pleroma.Web.Federator
alias Pleroma.Workers.SignatureRetryWorker
defp signature_headers_for(%User{} = signer) do
[
- {"host", "local.test"},
+ {"host", "#{URI.parse(Endpoint.url()).host}"},
{"date", "Thu, 25 Jul 2024 13:33:31 GMT"},
{"digest", "SHA-256=fake-digest"},
{"content-type", "application/activity+json"},
@@ -245,6 +246,66 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do
refute Activity.get_by_ap_id(create["id"])
end
+ test "cancels when the Host header does not match Endpoint" do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+
+ create = %{
+ "type" => "Create",
+ "actor" => alice.ap_id,
+ "id" => "https://one.com/activities/invalid-signature-create",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => %{
+ "type" => "Note",
+ "id" => "https://one.com/objects/invalid-signature-note",
+ "actor" => alice.ap_id,
+ "attributedTo" => alice.ap_id,
+ "content" => "forged post",
+ "published" => "2024-07-25T13:33:31Z",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => []
+ }
+ }
+
+ expect_signature_from(alice)
+
+ headers =
+ [
+ {"host", "invalid.example.com"},
+ {"date", "Thu, 25 Jul 2024 13:33:31 GMT"},
+ {"digest", "SHA-256=fake-digest"},
+ {"content-type", "application/activity+json"},
+ {
+ "signature",
+ "keyId=\"#{alice.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\""
+ }
+ ]
+
+ assert {:ok, oban_job} =
+ Federator.incoming_failed_signature_ap_doc(%{
+ method: "POST",
+ req_headers: headers,
+ request_path: "/inbox",
+ params: create,
+ query_string: ""
+ })
+
+ log =
+ capture_log([level: :warning], fn ->
+ assert {:cancel, :host_header_mismatch} = SignatureRetryWorker.perform(oban_job)
+ end)
+
+ assert log =~ "Failed-signature inbox retry rejected"
+ assert log =~ "reason=:host_header_mismatch"
+ assert log =~ "payload_actor=\"https://one.com/users/alice\""
+ assert log =~ "signature_actor=\"https://one.com/users/alice\""
+ assert log =~ "activity_id=\"https://one.com/activities/invalid-signature-create\""
+ assert log =~ "type=\"Create\""
+ assert log =~ "request_path=\"/inbox\""
+
+ refute Activity.get_by_ap_id(create["id"])
+ end
+
test "processes the activity after refetching a valid matching signature" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
@@ -309,11 +370,11 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do
"content-type" => "application/activity+json",
date: date,
digest: digest,
- host: "local.test"
+ host: "#{URI.parse(Endpoint.url()).host}"
})
req_headers = [
- ["host", "local.test"],
+ ["host", "#{URI.parse(Endpoint.url()).host}"],
["date", date],
["digest", digest],
["content-type", "application/activity+json"],
diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex
index f010fec33..c01516169 100644
--- a/test/support/conn_case.ex
+++ b/test/support/conn_case.ex
@@ -119,7 +119,10 @@ defmodule Pleroma.Web.ConnCase do
DataCase.stub_pipeline()
Mox.verify_on_exit!()
+ endpoint = URI.parse(Pleroma.Web.Endpoint.url())
+ conn = Phoenix.ConnTest.build_conn()
+ conn = %{conn | req_headers: [{"host", "#{endpoint.host}:#{endpoint.port}"}]}
- {:ok, conn: Phoenix.ConnTest.build_conn()}
+ {:ok, conn: conn}
end
end