Merge branch 'documentation-migration' of akkoma.dev:AkkomaGang/akkoma into documenta...
authorFloatingGhost <hannah@coffee-and-dreams.uk>
Fri, 1 Jul 2022 12:15:28 +0000 (13:15 +0100)
committerFloatingGhost <hannah@coffee-and-dreams.uk>
Fri, 1 Jul 2022 12:15:28 +0000 (13:15 +0100)
117 files changed:
.woodpecker/.release.yml
.woodpecker/.test.yml
CHANGELOG.md
config/config.exs
config/description.exs
config/test.exs
docs/administration/updating.md
docs/configuration/cheatsheet.md
docs/configuration/search.md [new file with mode: 0644]
docs/development/API/differences_in_mastoapi_responses.md
lib/mix/pleroma.ex
lib/mix/tasks/pleroma/search.ex
lib/mix/tasks/pleroma/search/meilisearch.ex [new file with mode: 0644]
lib/pleroma/activity.ex
lib/pleroma/application.ex
lib/pleroma/elasticsearch/document_mappings/activity.ex [deleted file]
lib/pleroma/elasticsearch/document_mappings/hashtag.ex [deleted file]
lib/pleroma/elasticsearch/document_mappings/user.ex [deleted file]
lib/pleroma/elasticsearch/store.ex [deleted file]
lib/pleroma/emails/user_email.ex
lib/pleroma/emoji-test.txt
lib/pleroma/hashtag.ex
lib/pleroma/notification.ex
lib/pleroma/search.ex
lib/pleroma/search/builtin.ex [deleted file]
lib/pleroma/search/database_search.ex [moved from lib/pleroma/activity/search.ex with 92% similarity]
lib/pleroma/search/elasticsearch.ex
lib/pleroma/search/elasticsearch/cluster.ex [new file with mode: 0644]
lib/pleroma/search/elasticsearch/document_mappings/activity.ex [new file with mode: 0644]
lib/pleroma/search/elasticsearch/hashtag_parser.ex [deleted file]
lib/pleroma/search/elasticsearch/store.ex [new file with mode: 0644]
lib/pleroma/search/elasticsearch/user_paser.ex [deleted file]
lib/pleroma/search/meilisearch.ex [new file with mode: 0644]
lib/pleroma/search/search_backend.ex [new file with mode: 0644]
lib/pleroma/telemetry/logger.ex
lib/pleroma/user.ex
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex
lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex
lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
lib/pleroma/web/activity_pub/pipeline.ex
lib/pleroma/web/activity_pub/side_effects.ex
lib/pleroma/web/activity_pub/side_effects/handling.ex
lib/pleroma/web/api_spec/operations/account_operation.ex
lib/pleroma/web/common_api.ex
lib/pleroma/web/feed/feed_view.ex
lib/pleroma/web/gettext.ex
lib/pleroma/web/mastodon_api/controllers/account_controller.ex
lib/pleroma/web/mastodon_api/controllers/search_controller.ex
lib/pleroma/web/mastodon_api/controllers/status_controller.ex
lib/pleroma/web/o_auth/mfa_view.ex
lib/pleroma/web/o_auth/o_auth_view.ex
lib/pleroma/web/plugs/set_locale_plug.ex
lib/pleroma/web/templates/email/digest.html.eex
lib/pleroma/web/templates/feed/feed/tag.atom.eex
lib/pleroma/web/templates/feed/feed/tag.rss.eex
lib/pleroma/web/templates/layout/app.html.eex
lib/pleroma/web/templates/layout/email.html.eex
lib/pleroma/web/templates/mailer/subscription/unsubscribe_failure.html.eex
lib/pleroma/web/templates/mailer/subscription/unsubscribe_success.html.eex
lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex
lib/pleroma/web/templates/o_auth/mfa/totp.html.eex
lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex
lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex
lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex
lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex
lib/pleroma/web/templates/o_auth/o_auth/register.html.eex
lib/pleroma/web/templates/o_auth/o_auth/show.html.eex
lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex
lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex
lib/pleroma/web/templates/twitter_api/password/reset.html.eex
lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex
lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex
lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex
lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex
lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex
lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex
lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex
lib/pleroma/web/twitter_api/twitter_api.ex
lib/pleroma/web/twitter_api/views/password_view.ex
lib/pleroma/web/twitter_api/views/remote_follow_view.ex
lib/pleroma/web/twitter_api/views/util_view.ex
lib/pleroma/web/views/email_view.ex
lib/pleroma/web/views/mailer/subscription_view.ex
lib/pleroma/workers/search_indexing_worker.ex [new file with mode: 0644]
mix.exs
mix.lock
priv/es-mappings/activity.json
priv/gettext/default.pot [new file with mode: 0644]
priv/gettext/en_test/LC_MESSAGES/default.po [new file with mode: 0644]
priv/gettext/en_test/LC_MESSAGES/errors.po [new file with mode: 0644]
priv/gettext/en_test/LC_MESSAGES/posix_errors.po [new file with mode: 0644]
priv/gettext/en_test/LC_MESSAGES/static_pages.po [new file with mode: 0644]
priv/gettext/errors.pot
priv/gettext/posix_errors.pot
priv/gettext/static_pages.pot [new file with mode: 0644]
priv/repo/migrations/20220302013920_add_language_to_users.exs [new file with mode: 0644]
test/fixtures/owncast-note-with-attachment.json [new file with mode: 0644]
test/mix/tasks/pleroma/digest_test.exs
test/pleroma/emails/user_email_test.exs
test/pleroma/emoji_test.exs
test/pleroma/notification_test.exs
test/pleroma/search/database_search_test.exs [moved from test/pleroma/activity/search_test.exs with 81% similarity]
test/pleroma/search/elasticsearch_test.exs [new file with mode: 0644]
test/pleroma/search/meilisearch_test.exs [new file with mode: 0644]
test/pleroma/user_test.exs
test/pleroma/web/activity_pub/mrf/anti_followbot_policy_test.exs
test/pleroma/web/activity_pub/mrf/steal_emoji_policy_test.exs
test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
test/pleroma/web/activity_pub/pipeline_test.exs
test/pleroma/web/admin_api/controllers/report_controller_test.exs
test/pleroma/web/gettext_test.exs [new file with mode: 0644]
test/pleroma/web/mastodon_api/controllers/account_controller_test.exs
test/pleroma/web/mastodon_api/controllers/status_controller_test.exs
test/pleroma/web/o_auth/app_test.exs
test/pleroma/web/plugs/set_locale_plug_test.exs
test/support/elasticsearch_mock.ex [new file with mode: 0644]

index 335f3c8e8826442671677c2e5f56e31af66472a7..28043aa655d5792964178589cdebf90fb1aa7a16 100644 (file)
@@ -16,7 +16,9 @@ pipeline:
   glibc:
     when:
       event:
-        - tag
+        - push
+      branch:
+        - develop
     secrets:
     - SCW_ACCESS_KEY
     - SCW_SECRET_KEY
@@ -44,7 +46,9 @@ pipeline:
   musl:
     when:
       event:
-        - tag
+        - push
+      branch:
+        - develop
     secrets:
     - SCW_ACCESS_KEY
     - SCW_SECRET_KEY
index cef7436433bc540d05854968ef5d93385af6a9b7..6724d363ddc1e42248a9cc1bef415a317380efb4 100644 (file)
@@ -11,6 +11,7 @@ pipeline:
     when:
       event:
       - push
+      - pull_request
     environment:
       MIX_ENV: test
     commands:
@@ -25,6 +26,7 @@ pipeline:
     when:
       event:
       - push
+      - pull_request
     environment:
       MIX_ENV: test
       POSTGRES_DB: pleroma_test
index 9513a54fa796cb4f1e066fab5a42b787f39b636d..2119c8e21633f392b97659bc1fe651773046956f 100644 (file)
@@ -54,6 +54,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Readded mastoFE
 - Added support for custom emoji reactions
 - Added `emoji_url` in notifications to allow for custom emoji rendering
+- Make backend-rendered pages translatable. This includes emails. Pages returned as a HTTP response are translated using the language specified in the `userLanguage` cookie, or the `Accept-Language` header. Emails are translated using the `language` field when registering. This language can be changed by `PATCH /api/v1/accounts/update_credentials` with the `language` field.
 
 ### Fixed
 - Subscription(Bell) Notifications: Don't create from Pipeline Ingested replies
@@ -116,6 +117,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Improved Twittercard and OpenGraph meta tag generation including thumbnails and image dimension metadata when available.
 - AdminAPI: sort users so the newest are at the top.
 - ActivityPub Client-to-Server(C2S): Limitation on the type of Activity/Object are lifted as they are now passed through ObjectValidators
+- MRF (`AntiFollowbotPolicy`): Bot accounts are now also considered followbots. Users can still allow bots to follow them by first following the bot.
 
 ### Added
 
index cecbea9b3f306a5e1e3dbbbe551f32a65c946479..eb39155df4b66ac3f222f079ff993e5a30a514ab 100644 (file)
@@ -568,7 +568,8 @@ config :pleroma, Oban,
     remote_fetcher: 2,
     attachments_cleanup: 1,
     new_users_digest: 1,
-    mute_expire: 5
+    mute_expire: 5,
+    search_indexing: 10
   ],
   plugins: [Oban.Plugins.Pruner],
   crontab: [
@@ -579,7 +580,8 @@ config :pleroma, Oban,
 config :pleroma, :workers,
   retries: [
     federator_incoming: 5,
-    federator_outgoing: 5
+    federator_outgoing: 5,
+    search_indexing: 2
   ]
 
 config :pleroma, Pleroma.Formatter,
@@ -842,17 +844,32 @@ config :pleroma, Pleroma.User.Backup,
 
 config :pleroma, ConcurrentLimiter, [
   {Pleroma.Web.RichMedia.Helpers, [max_running: 5, max_waiting: 5]},
-  {Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy, [max_running: 5, max_waiting: 5]}
+  {Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy, [max_running: 5, max_waiting: 5]},
+  {Pleroma.Search, [max_running: 30, max_waiting: 50]}
 ]
 
-config :pleroma, :search, provider: Pleroma.Search.Builtin
-
-config :pleroma, :telemetry,
-  slow_queries_logging: [
-    enabled: false,
-    min_duration: 500_000,
-    exclude_sources: [nil, "oban_jobs"]
-  ]
+config :pleroma, Pleroma.Search, module: Pleroma.Search.DatabaseSearch
+
+config :pleroma, Pleroma.Search.Meilisearch,
+  url: "http://127.0.0.1:7700/",
+  private_key: nil,
+  initial_indexing_chunk_size: 100_000
+
+config :pleroma, Pleroma.Search.Elasticsearch.Cluster,
+  url: "http://localhost:9200",
+  username: "elastic",
+  password: "changeme",
+  api: Elasticsearch.API.HTTP,
+  json_library: Jason,
+  indexes: %{
+    activities: %{
+      settings: "priv/es-mappings/activity.json",
+      store: Pleroma.Search.Elasticsearch.Store,
+      sources: [Pleroma.Activity],
+      bulk_page_size: 1000,
+      bulk_wait_interval: 15_000
+    }
+  }
 
 # Import environment specific config. This must remain at the bottom
 # of this file so it overrides the configuration defined above.
index 4ca79ad510da851432f7750b1bf65f20624d2e66..9401bed5c506cb60990f3b091b9310d8deeab405 100644 (file)
@@ -3358,5 +3358,133 @@ config :pleroma, :config_description, [
         ]
       }
     ]
+  },
+  %{
+    group: :pleroma,
+    key: Pleroma.Search,
+    type: :group,
+    description: "General search settings.",
+    children: [
+      %{
+        key: :module,
+        type: :keyword,
+        description: "Selected search module.",
+        suggestion: [Pleroma.Search.DatabaseSearch, Pleroma.Search.Meilisearch]
+      }
+    ]
+  },
+  %{
+    group: :pleroma,
+    key: Pleroma.Search.Meilisearch,
+    type: :group,
+    description: "Meilisearch settings.",
+    children: [
+      %{
+        key: :url,
+        type: :string,
+        description: "Meilisearch URL.",
+        suggestion: ["http://127.0.0.1:7700/"]
+      },
+      %{
+        key: :private_key,
+        type: :string,
+        description:
+          "Private key for meilisearch authentication, or `nil` to disable private key authentication.",
+        suggestion: [nil]
+      },
+      %{
+        key: :initial_indexing_chunk_size,
+        type: :int,
+        description:
+          "Amount of posts in a batch when running the initial indexing operation. Should probably not be more than 100000" <>
+            " since there's a limit on maximum insert size",
+        suggestion: [100_000]
+      }
+    ]
+  },
+  %{
+    group: :pleroma,
+    key: Pleroma.Search.Elasticsearch.Cluster,
+    type: :group,
+    description: "Elasticsearch settings.",
+    children: [
+      %{
+        key: :url,
+        type: :string,
+        description: "Elasticsearch URL.",
+        suggestion: ["http://127.0.0.1:9200/"]
+      },
+      %{
+        key: :username,
+        type: :string,
+        description: "Username to connect to ES. Set to nil if your cluster is unauthenticated.",
+        suggestion: ["elastic"]
+      },
+      %{
+        key: :password,
+        type: :string,
+        description: "Password to connect to ES. Set to nil if your cluster is unauthenticated.",
+        suggestion: ["changeme"]
+      },
+      %{
+        key: :api,
+        type: :module,
+        description:
+          "The API module used by Elasticsearch. Should always be Elasticsearch.API.HTTP",
+        suggestion: [Elasticsearch.API.HTTP]
+      },
+      %{
+        key: :json_library,
+        type: :module,
+        description:
+          "The JSON module used to encode/decode when communicating with Elasticsearch",
+        suggestion: [Jason]
+      },
+      %{
+        key: :indexes,
+        type: :map,
+        description: "The indices to set up in Elasticsearch",
+        children: [
+          %{
+            key: :activities,
+            type: :map,
+            description: "Config for the index to use for activities",
+            children: [
+              %{
+                key: :settings,
+                type: :string,
+                description:
+                  "Path to the file containing index settings for the activities index. Should contain a mapping.",
+                suggestion: ["priv/es-mappings/activity.json"]
+              },
+              %{
+                key: :store,
+                type: :module,
+                description: "The internal store module",
+                suggestion: [Pleroma.Search.Elasticsearch.Store]
+              },
+              %{
+                key: :sources,
+                type: {:list, :module},
+                description: "The internal types to use for this index",
+                suggestion: [[Pleroma.Activity]]
+              },
+              %{
+                key: :bulk_page_size,
+                type: :int,
+                description: "Size for bulk put requests, mostly used on building the index",
+                suggestion: [5000]
+              },
+              %{
+                key: :bulk_wait_interval,
+                type: :int,
+                description: "Time to wait between bulk put requests (in ms)",
+                suggestion: [15_000]
+              }
+            ]
+          }
+        ]
+      }
+    ]
   }
 ]
index a5bf3a4d13f752316cbd76b45fbfb559116e8ddf..7fbababdf061f93e13966b48512901ed8b6717c2 100644 (file)
@@ -134,6 +134,10 @@ config :pleroma, :side_effects,
   ap_streamer: Pleroma.Web.ActivityPub.ActivityPubMock,
   logger: Pleroma.LoggerMock
 
+config :pleroma, Pleroma.Search, module: Pleroma.Search.DatabaseSearch
+
+config :pleroma, Pleroma.Search.Meilisearch, url: "http://127.0.0.1:7700/", private_key: nil
+
 # Reduce recompilation time
 # https://dashbit.co/blog/speeding-up-re-compilation-of-elixir-projects
 config :phoenix, :plug_init_mode, :runtime
index ef2c9218c339827aa736f1aaba4fffdea72a9fe8..01d3b9b0efff79fa88882f518525d5ec5500d1f5 100644 (file)
@@ -17,11 +17,11 @@ su pleroma -s $SHELL -lc "./bin/pleroma_ctl migrate"
 ## For from source installations (using git)
 
 1. Go to the working directory of Pleroma (default is `/opt/pleroma`)
-2. Run `git pull`. This pulls the latest changes from upstream.
+2. Run `git pull` [^1]. This pulls the latest changes from upstream.
 3. Run `mix deps.get` [^1]. This pulls in any new dependencies.
 4. Stop the Pleroma service.
 5. Run `mix ecto.migrate` [^1] [^2]. This task performs database migrations, if there were any.
 6. Start the Pleroma service.
 
-[^1]: Depending on which install guide you followed (for example on Debian/Ubuntu), you want to run `mix` tasks as `pleroma` user by adding `sudo -Hu pleroma` before the command.
+[^1]: Depending on which install guide you followed (for example on Debian/Ubuntu), you want to run `git` and `mix` tasks as `pleroma` user by adding `sudo -Hu pleroma` before the command.
 [^2]: Prefix with `MIX_ENV=prod` to run it using the production config file.
index 3b8f8cc52f6d4445b67c065a688b52263a3ea881..50281f4516f7df1b7e24a669ee7bde90a44f42c2 100644 (file)
@@ -125,6 +125,8 @@ To add configuration to your config file, you can copy it from the base config.
     * `Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy`: Sets a default expiration on all posts made by users of the local instance. Requires `Pleroma.Workers.PurgeExpiredActivity` to be enabled for processing the scheduled delections.
     * `Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy`: Makes all bot posts to disappear from public timelines.
     * `Pleroma.Web.ActivityPub.MRF.FollowBotPolicy`: Automatically follows newly discovered users from the specified bot account. Local accounts, locked accounts, and users with "#nobot" in their bio are respected and excluded from being followed.
+    * `Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy`: Drops follow requests from followbots. Users can still allow bots to follow them by first following the bot.
+    * `Pleroma.Web.ActivityPub.MRF.KeywordPolicy`: Rejects or removes from the federated timeline or replaces keywords. (See [`:mrf_keyword`](#mrf_keyword)).
 * `transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo).
 * `transparency_exclusions`: Exclude specific instance names from MRF transparency.  The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.
 
diff --git a/docs/configuration/search.md b/docs/configuration/search.md
new file mode 100644 (file)
index 0000000..e1f23b5
--- /dev/null
@@ -0,0 +1,163 @@
+# Configuring search
+
+{! backend/administration/CLI_tasks/general_cli_task_info.include !}
+
+## Built-in search
+
+To use built-in search that has no external dependencies, set the search module to `Pleroma.Activity`:
+
+> config :pleroma, Pleroma.Search, module: Pleroma.Search.DatabaseSearch
+
+While it has no external dependencies, it has problems with performance and relevancy.
+
+## Meilisearch
+
+Note that it's quite a bit more memory hungry than PostgreSQL (around 4-5G for ~1.2 million
+posts while idle and up to 7G while indexing initially). The disk usage for this additional index is also
+around 4 gigabytes. Like [RUM](./cheatsheet.md#rum-indexing-for-full-text-search) indexes, it offers considerably
+higher performance and ordering by timestamp in a reasonable amount of time.
+Additionally, the search results seem to be more accurate.
+
+Due to high memory usage, it may be best to set it up on a different machine, if running pleroma on a low-resource
+computer, and use private key authentication to secure the remote search instance.
+
+To use [meilisearch](https://www.meilisearch.com/), set the search module to `Pleroma.Search.Meilisearch`:
+
+> config :pleroma, Pleroma.Search, module: Pleroma.Search.Meilisearch
+
+You then need to set the address of the meilisearch instance, and optionally the private key for authentication. You might
+also want to change the `initial_indexing_chunk_size` to be smaller if you're server is not very powerful, but not higher than `100_000`,
+because meilisearch will refuse to process it if it's too big. However, in general you want this to be as big as possible, because meilisearch
+indexes faster when it can process many posts in a single batch.
+
+> config :pleroma, Pleroma.Search.Meilisearch,
+>    url: "http://127.0.0.1:7700/",
+>    private_key: "private key",
+>    initial_indexing_chunk_size: 100_000
+
+Information about setting up meilisearch can be found in the
+[official documentation](https://docs.meilisearch.com/learn/getting_started/installation.html).
+You probably want to start it with `MEILI_NO_ANALYTICS=true` environment variable to disable analytics.
+At least version 0.25.0 is required, but you are strongly adviced to use at least 0.26.0, as it introduces
+the `--enable-auto-batching` option which drastically improves performance. Without this option, the search
+is hardly usable on a somewhat big instance.
+
+### Private key authentication (optional)
+
+To set the private key, use the `MEILI_MASTER_KEY` environment variable when starting. After setting the _master key_,
+you have to get the _private key_, which is actually used for authentication.
+
+=== "OTP"
+    ```sh
+    ./bin/pleroma_ctl search.meilisearch show-keys <your master key here>
+    ```
+
+=== "From Source"
+    ```sh
+    mix pleroma.search.meilisearch show-keys <your master key here>
+    ```
+
+You will see a "Default Admin API Key", this is the key you actually put into your configuration file.
+
+### Initial indexing
+
+After setting up the configuration, you'll want to index all of your already existsing posts. Only public posts are indexed.  You'll only
+have to do it one time, but it might take a while, depending on the amount of posts your instance has seen. This is also a fairly RAM
+consuming process for `meilisearch`, and it will take a lot of RAM when running if you have a lot of posts (seems to be around 5G for ~1.2
+million posts while idle and up to 7G while indexing initially, but your experience may be different).
+
+The sequence of actions is as follows:
+
+1. First, change the configuration to use `Pleroma.Search.Meilisearch` as the search backend
+2. Restart your instance, at this point it can be used while the search indexing is running, though search won't return anything
+3. Start the initial indexing process (as described below with `index`),
+   and wait until the task says it sent everything from the database to index
+4. Wait until everything is actually indexed (by checking with `stats` as described below),
+   at this point you don't have to do anything, just wait a while.
+
+To start the initial indexing, run the `index` command:
+
+=== "OTP"
+    ```sh
+    ./bin/pleroma_ctl search.meilisearch index
+    ```
+
+=== "From Source"
+    ```sh
+    mix pleroma.search.meilisearch index
+    ```
+
+This will show you the total amount of posts to index, and then show you the amount of posts indexed currently, until the numbers eventually
+become the same. The posts are indexed in big batches and meilisearch will take some time to actually index them, even after you have
+inserted all the posts into it. Depending on the amount of posts, this may be as long as several hours. To get information about the status
+of indexing and how many posts have actually been indexed, use the `stats` command:
+
+=== "OTP"
+    ```sh
+    ./bin/pleroma_ctl search.meilisearch stats
+    ```
+
+=== "From Source"
+    ```sh
+    mix pleroma.search.meilisearch stats
+    ```
+
+### Clearing the index
+
+In case you need to clear the index (for example, to re-index from scratch, if that needs to happen for some reason), you can
+use the `clear` command:
+
+=== "OTP"
+    ```sh
+    ./bin/pleroma_ctl search.meilisearch clear
+    ```
+
+=== "From Source"
+    ```sh
+    mix pleroma.search.meilisearch clear
+    ```
+
+This will clear **all** the posts from the search index. Note, that deleted posts are also removed from index by the instance itself, so
+there is no need to actually clear the whole index, unless you want **all** of it gone. That said, the index does not hold any information
+that cannot be re-created from the database, it should also generally be a lot smaller than the size of your database. Still, the size
+depends on the amount of text in posts.
+
+## Elasticsearch
+
+As with meilisearch, this can be rather memory-hungry, but it is very good at what it does.
+
+To use [elasticsearch](https://www.elastic.co/), set the search module to `Pleroma.Search.Elasticsearch`:
+
+> config :pleroma, Pleroma.Search, module: Pleroma.Search.Elasticsearch
+
+You then need to set the URL and authentication credentials if relevant.
+
+> config :pleroma, Pleroma.Search.Elasticsearch.Cluster,
+>    url: "http://127.0.0.1:9200/",
+>    username: "elastic",
+>    password: "changeme",
+
+### Initial indexing
+
+After setting up the configuration, you'll want to index all of your already existsing posts. Only public posts are indexed.  You'll only
+have to do it one time, but it might take a while, depending on the amount of posts your instance has seen. 
+
+The sequence of actions is as follows:
+
+1. First, change the configuration to use `Pleroma.Search.Elasticsearch` as the search backend
+2. Restart your instance, at this point it can be used while the search indexing is running, though search won't return anything
+3. Start the initial indexing process (as described below with `index`),
+   and wait until the task says it sent everything from the database to index
+4. Wait until the index tasks exits
+
+To start the initial indexing, run the `build` command:
+
+=== "OTP"
+```sh
+./bin/pleroma_ctl search import activities
+```
+
+=== "From Source"
+```sh
+mix pleroma.search import activities
+```
index 518aca11429e0e49cdece9d5246b82b7bfe432e7..def718b954a17d95092186f86f887e575881c5f1 100644 (file)
@@ -241,6 +241,7 @@ Additional parameters can be added to the JSON body/Form data:
 - `discoverable` - if true, external services (search bots) etc. are allowed to index / list the account (regardless of this setting, user will still appear in regular search results).
 - `actor_type` - the type of this account.
 - `accepts_chat_messages` - if false, this account will reject all chat messages.
+- `language` - user's preferred language for receiving emails (digest, confirmation, etc.)
 
 All images (avatar, banner and background) can be reset to the default by sending an empty string ("") instead of a file.
 
@@ -292,6 +293,7 @@ Has these additional parameters (which are the same as in Pleroma-API):
 - `captcha_token`: optional, contains provider-specific captcha token
 - `captcha_answer_data`: optional, contains provider-specific captcha data
 - `token`: invite token required when the registrations aren't public.
+- `language`: optional, user's preferred language for receiving emails (digest, confirmation, etc.), default to the language set in the `userLanguage` cookies or `Accept-Language` header.
 
 ## Instance
 
index 2b6c7d6bbd13f364fefa396e66a778d6b480f2d6..02c40850a7430b7c4c0b74463df007c08b57cc64 100644 (file)
@@ -57,7 +57,8 @@ defmodule Mix.Pleroma do
         {Majic.Pool,
          [name: Pleroma.MajicPool, pool_size: Pleroma.Config.get([:majic_pool, :size], 2)]}
       ] ++
-        http_children(adapter)
+        http_children(adapter) ++
+        elasticsearch_children()
 
     cachex_children = Enum.map(@cachex_children, &Pleroma.Application.build_cachex(&1, []))
 
@@ -136,4 +137,14 @@ defmodule Mix.Pleroma do
   end
 
   defp http_children(_), do: []
+
+  def elasticsearch_children do
+    config = Pleroma.Config.get([Pleroma.Search, :module])
+
+    if config == Pleroma.Search.Elasticsearch do
+      [Pleroma.Search.Elasticsearch.Cluster]
+    else
+      []
+    end
+  end
 end
index 1fd880eab61fe9a4037c59432494fe702e29a865..102bc5b63bed69fd7fcdd0b73c2e038948ed3687 100644 (file)
@@ -5,60 +5,16 @@
 defmodule Mix.Tasks.Pleroma.Search do
   use Mix.Task
   import Mix.Pleroma
-  import Ecto.Query
-  alias Pleroma.Activity
-  alias Pleroma.Pagination
-  alias Pleroma.User
-  alias Pleroma.Hashtag
 
   @shortdoc "Manages elasticsearch"
 
   def run(["import", "activities" | _rest]) do
     start_pleroma()
 
-    from(a in Activity, where: not ilike(a.actor, "%/relay"))
-    |> where([a], fragment("(? ->> 'type'::text) = 'Create'", a.data))
-    |> Activity.with_preloaded_object()
-    |> Activity.with_preloaded_user_actor()
-    |> get_all(:activities)
-  end
-
-  def run(["import", "users" | _rest]) do
-    start_pleroma()
-
-    from(u in User, where: u.nickname not in ["internal.fetch", "relay"])
-    |> get_all(:users)
-  end
-
-  def run(["import", "hashtags" | _rest]) do
-    start_pleroma()
-
-    from(h in Hashtag)
-    |> Pleroma.Repo.all()
-    |> Pleroma.Elasticsearch.bulk_post(:hashtags)
-  end
-
-  defp get_all(query, index, max_id \\ nil) do
-    params = %{limit: 1000}
-
-    params =
-      if max_id == nil do
-        params
-      else
-        Map.put(params, :max_id, max_id)
-      end
-
-    res =
-      query
-      |> Pagination.fetch_paginated(params)
-
-    if res == [] do
-      :ok
-    else
-      res
-      |> Pleroma.Elasticsearch.bulk_post(index)
-
-      get_all(query, index, List.last(res).id)
-    end
+    Elasticsearch.Index.Bulk.upload(
+      Pleroma.Search.Elasticsearch.Cluster,
+      "activities",
+      Pleroma.Config.get([Pleroma.Search.Elasticsearch.Cluster, :indexes, :activities])
+    )
   end
 end
diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex
new file mode 100644 (file)
index 0000000..d4a83c3
--- /dev/null
@@ -0,0 +1,144 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Mix.Tasks.Pleroma.Search.Meilisearch do
+  require Pleroma.Constants
+
+  import Mix.Pleroma
+  import Ecto.Query
+
+  import Pleroma.Search.Meilisearch,
+    only: [meili_post: 2, meili_put: 2, meili_get: 1, meili_delete!: 1]
+
+  def run(["index"]) do
+    start_pleroma()
+
+    meili_version =
+      (
+        {:ok, result} = meili_get("/version")
+
+        result["pkgVersion"]
+      )
+
+    # The ranking rule syntax was changed but nothing about that is mentioned in the changelog
+    if not Version.match?(meili_version, ">= 0.25.0") do
+      raise "Meilisearch <0.24.0 not supported"
+    end
+
+    {:ok, _} =
+      meili_post(
+        "/indexes/objects/settings/ranking-rules",
+        [
+          "published:desc",
+          "words",
+          "exactness",
+          "proximity",
+          "typo",
+          "attribute",
+          "sort"
+        ]
+      )
+
+    {:ok, _} =
+      meili_post(
+        "/indexes/objects/settings/searchable-attributes",
+        [
+          "content"
+        ]
+      )
+
+    IO.puts("Created indices. Starting to insert posts.")
+
+    chunk_size = Pleroma.Config.get([Pleroma.Search.Meilisearch, :initial_indexing_chunk_size])
+
+    Pleroma.Repo.transaction(
+      fn ->
+        query =
+          from(Pleroma.Object,
+            # Only index public and unlisted posts which are notes and have some text
+            where:
+              fragment("data->>'type' = 'Note'") and
+                (fragment("data->'to' \\? ?", ^Pleroma.Constants.as_public()) or
+                   fragment("data->'cc' \\? ?", ^Pleroma.Constants.as_public())),
+            order_by: [desc: fragment("data->'published'")]
+          )
+
+        count = query |> Pleroma.Repo.aggregate(:count, :data)
+        IO.puts("Entries to index: #{count}")
+
+        Pleroma.Repo.stream(
+          query,
+          timeout: :infinity
+        )
+        |> Stream.map(&Pleroma.Search.Meilisearch.object_to_search_data/1)
+        |> Stream.filter(fn o -> not is_nil(o) end)
+        |> Stream.chunk_every(chunk_size)
+        |> Stream.transform(0, fn objects, acc ->
+          new_acc = acc + Enum.count(objects)
+
+          # Reset to the beginning of the line and rewrite it
+          IO.write("\r")
+          IO.write("Indexed #{new_acc} entries")
+
+          {[objects], new_acc}
+        end)
+        |> Stream.each(fn objects ->
+          result =
+            meili_put(
+              "/indexes/objects/documents",
+              objects
+            )
+
+          with {:ok, res} <- result do
+            if not Map.has_key?(res, "uid") do
+              IO.puts("\nFailed to index: #{inspect(result)}")
+            end
+          else
+            e -> IO.puts("\nFailed to index due to network error: #{inspect(e)}")
+          end
+        end)
+        |> Stream.run()
+      end,
+      timeout: :infinity
+    )
+
+    IO.write("\n")
+  end
+
+  def run(["clear"]) do
+    start_pleroma()
+
+    meili_delete!("/indexes/objects/documents")
+  end
+
+  def run(["show-keys", master_key]) do
+    start_pleroma()
+
+    endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url])
+
+    {:ok, result} =
+      Pleroma.HTTP.get(
+        Path.join(endpoint, "/keys"),
+        [{"Authorization", "Bearer #{master_key}"}]
+      )
+
+    decoded = Jason.decode!(result.body)
+
+    if decoded["results"] do
+      Enum.each(decoded["results"], fn %{"description" => desc, "key" => key} ->
+        IO.puts("#{desc}: #{key}")
+      end)
+    else
+      IO.puts("Error fetching the keys, check the master key is correct: #{inspect(decoded)}")
+    end
+  end
+
+  def run(["stats"]) do
+    start_pleroma()
+
+    {:ok, result} = meili_get("/indexes/objects/stats")
+    IO.puts("Number of entries: #{result["numberOfDocuments"]}")
+    IO.puts("Indexing? #{result["isIndexing"]}")
+  end
+end
index 4106feef6d5baac44bc62c422ad4d3df2d6e2980..abfe778d24c0498358e02288570fcccd112131aa 100644 (file)
@@ -367,7 +367,7 @@ defmodule Pleroma.Activity do
     from(activity in query, where: activity.actor not in subquery(deactivated_users_query))
   end
 
-  defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search
+  defdelegate search(user, query, options \\ []), to: Pleroma.Search.DatabaseSearch
 
   def direct_conversation_id(activity, for_user) do
     alias Pleroma.Conversation.Participation
index d37454d2c7cabedfbd308aff385b47e3a2ef09b5..b709e737bf2345dd1b032967274230cda8caeb22 100644 (file)
@@ -105,6 +105,7 @@ defmodule Pleroma.Application do
           {Oban, Config.get(Oban)},
           Pleroma.Web.Endpoint
         ] ++
+        elasticsearch_children() ++
         task_children(@mix_env) ++
         dont_run_in_test(@mix_env) ++
         shout_child(shout_enabled?())
@@ -303,11 +304,25 @@ defmodule Pleroma.Application do
 
   defp http_children(_, _), do: []
 
+  def elasticsearch_children do
+    config = Config.get([Pleroma.Search, :module])
+
+    if config == Pleroma.Search.Elasticsearch do
+      [Pleroma.Search.Elasticsearch.Cluster]
+    else
+      []
+    end
+  end
+
   @spec limiters_setup() :: :ok
   def limiters_setup do
     config = Config.get(ConcurrentLimiter, [])
 
-    [Pleroma.Web.RichMedia.Helpers, Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy]
+    [
+      Pleroma.Web.RichMedia.Helpers,
+      Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy,
+      Pleroma.Search
+    ]
     |> Enum.each(fn module ->
       mod_config = Keyword.get(config, module, [])
 
diff --git a/lib/pleroma/elasticsearch/document_mappings/activity.ex b/lib/pleroma/elasticsearch/document_mappings/activity.ex
deleted file mode 100644 (file)
index a028c6f..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-# Akkoma: A lightweight social networking server
-# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Elasticsearch.DocumentMappings.Activity do
-  alias Pleroma.Object
-
-  def id(obj), do: obj.id
-
-  def encode(%{object: %{data: %{"type" => "Note"}}} = activity) do
-    %{
-      _timestamp: activity.inserted_at,
-      user: activity.user_actor.nickname,
-      content: activity.object.data["content"],
-      instance: URI.parse(activity.user_actor.ap_id).host,
-      hashtags: Object.hashtags(activity.object)
-    }
-  end
-end
diff --git a/lib/pleroma/elasticsearch/document_mappings/hashtag.ex b/lib/pleroma/elasticsearch/document_mappings/hashtag.ex
deleted file mode 100644 (file)
index 7391983..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-# Akkoma: A lightweight social networking server
-# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Elasticsearch.DocumentMappings.Hashtag do
-  def id(obj), do: obj.id
-
-  def encode(%{timestamp: _} = hashtag) do
-    %{
-      hashtag: hashtag.name,
-      timestamp: hashtag.timestamp
-    }
-  end
-
-  def encode(hashtag) do
-    %{
-      hashtag: hashtag.name,
-      timestamp: hashtag.inserted_at
-    }
-  end
-end
diff --git a/lib/pleroma/elasticsearch/document_mappings/user.ex b/lib/pleroma/elasticsearch/document_mappings/user.ex
deleted file mode 100644 (file)
index d5cfca6..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-# Akkoma: A lightweight social networking server
-# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Elasticsearch.DocumentMappings.User do
-  def id(obj), do: obj.id
-
-  def encode(%{actor_type: "Person"} = user) do
-    %{
-      timestamp: user.inserted_at,
-      instance: URI.parse(user.ap_id).host,
-      nickname: user.nickname,
-      bio: user.bio,
-      display_name: user.name
-    }
-  end
-end
diff --git a/lib/pleroma/elasticsearch/store.ex b/lib/pleroma/elasticsearch/store.ex
deleted file mode 100644 (file)
index 98c88a7..0000000
+++ /dev/null
@@ -1,256 +0,0 @@
-# Akkoma: A lightweight social networking server
-# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Elasticsearch do
-  alias Pleroma.Activity
-  alias Pleroma.User
-  alias Pleroma.Object
-  alias Pleroma.Elasticsearch.DocumentMappings
-  alias Pleroma.Config
-  require Logger
-
-  defp url do
-    Config.get([:elasticsearch, :url])
-  end
-
-  defp enabled? do
-    Config.get([:search, :provider]) == Pleroma.Search.Elasticsearch
-  end
-
-  def delete_by_id(:activity, id) do
-    if enabled?() do
-      Elastix.Document.delete(url(), "activities", "activity", id)
-    end
-  end
-
-  def put_by_id(:activity, id) do
-    id
-    |> Activity.get_by_id_with_object()
-    |> maybe_put_into_elasticsearch()
-  end
-
-  def maybe_put_into_elasticsearch({:ok, item}) do
-    maybe_put_into_elasticsearch(item)
-  end
-
-  def maybe_put_into_elasticsearch(
-        %{data: %{"type" => "Create"}, object: %{data: %{"type" => "Note"}}} = activity
-      ) do
-    if enabled?() do
-      actor = Pleroma.Activity.user_actor(activity)
-
-      activity
-      |> Map.put(:user_actor, actor)
-      |> put()
-    end
-  end
-
-  def maybe_put_into_elasticsearch(%User{actor_type: "Person"} = user) do
-    if enabled?() do
-      put(user)
-    end
-  end
-
-  def maybe_put_into_elasticsearch(_) do
-    {:ok, :skipped}
-  end
-
-  def maybe_bulk_post(data, type) do
-    if enabled?() do
-      bulk_post(data, type)
-    end
-  end
-
-  def put(%Activity{} = activity) do
-    with {:ok, _} <-
-           Elastix.Document.index(
-             url(),
-             "activities",
-             "activity",
-             DocumentMappings.Activity.id(activity),
-             DocumentMappings.Activity.encode(activity)
-           ) do
-      activity
-      |> Map.get(:object)
-      |> Object.hashtags()
-      |> Enum.map(fn x ->
-        %{id: x, name: x, timestamp: DateTime.to_iso8601(DateTime.utc_now())}
-      end)
-      |> bulk_post(:hashtags)
-    else
-      {:error, %{reason: err}} ->
-        Logger.error("Could not put activity: #{err}")
-        :skipped
-    end
-  end
-
-  def put(%User{} = user) do
-    with {:ok, _} <-
-           Elastix.Document.index(
-             url(),
-             "users",
-             "user",
-             DocumentMappings.User.id(user),
-             DocumentMappings.User.encode(user)
-           ) do
-      :ok
-    else
-      {:error, %{reason: err}} ->
-        Logger.error("Could not put user: #{err}")
-        :skipped
-    end
-  end
-
-  def bulk_post(data, :activities) do
-    d =
-      data
-      |> Enum.filter(fn x ->
-        t =
-          x.object
-          |> Map.get(:data, %{})
-          |> Map.get("type", "")
-
-        t == "Note"
-      end)
-      |> Enum.map(fn d ->
-        [
-          %{index: %{_id: DocumentMappings.Activity.id(d)}},
-          DocumentMappings.Activity.encode(d)
-        ]
-      end)
-      |> List.flatten()
-
-    with {:ok, %{body: %{"errors" => false}}} <-
-           Elastix.Bulk.post(
-             url(),
-             d,
-             index: "activities",
-             type: "activity"
-           ) do
-      :ok
-    else
-      {:error, %{reason: err}} ->
-        Logger.error("Could not bulk put activity: #{err}")
-        :skipped
-
-      {:ok, %{body: _}} ->
-        :skipped
-    end
-  end
-
-  def bulk_post(data, :users) do
-    d =
-      data
-      |> Enum.filter(fn x -> x.actor_type == "Person" end)
-      |> Enum.map(fn d ->
-        [
-          %{index: %{_id: DocumentMappings.User.id(d)}},
-          DocumentMappings.User.encode(d)
-        ]
-      end)
-      |> List.flatten()
-
-    with {:ok, %{body: %{"errors" => false}}} <-
-           Elastix.Bulk.post(
-             url(),
-             d,
-             index: "users",
-             type: "user"
-           ) do
-      :ok
-    else
-      {:error, %{reason: err}} ->
-        Logger.error("Could not bulk put users: #{err}")
-        :skipped
-
-      {:ok, %{body: _}} ->
-        :skipped
-    end
-  end
-
-  def bulk_post(data, :hashtags) when is_list(data) do
-    d =
-      data
-      |> Enum.map(fn d ->
-        [
-          %{index: %{_id: DocumentMappings.Hashtag.id(d)}},
-          DocumentMappings.Hashtag.encode(d)
-        ]
-      end)
-      |> List.flatten()
-
-    with {:ok, %{body: %{"errors" => false}}} <-
-           Elastix.Bulk.post(
-             url(),
-             d,
-             index: "hashtags",
-             type: "hashtag"
-           ) do
-      :ok
-    else
-      {:error, %{reason: err}} ->
-        Logger.error("Could not bulk put hashtags: #{err}")
-        :skipped
-
-      {:ok, %{body: _}} ->
-        :skipped
-    end
-  end
-
-  def bulk_post(_, :hashtags), do: {:ok, nil}
-
-  def search(_, _, _, :skip), do: []
-
-  def search(:raw, index, type, q) do
-    with {:ok, raw_results} <- Elastix.Search.search(url(), index, [type], q) do
-      results =
-        raw_results
-        |> Map.get(:body, %{})
-        |> Map.get("hits", %{})
-        |> Map.get("hits", [])
-
-      {:ok, results}
-    else
-      {:error, e} ->
-        Logger.error(e)
-        {:error, e}
-    end
-  end
-
-  def search(:activities, q) do
-    with {:ok, results} <- search(:raw, "activities", "activity", q) do
-      results
-      |> Enum.map(fn result -> result["_id"] end)
-      |> Pleroma.Activity.all_by_ids_with_object()
-      |> Enum.sort(&(&1.inserted_at >= &2.inserted_at))
-    else
-      e ->
-        Logger.error(e)
-        []
-    end
-  end
-
-  def search(:users, q) do
-    with {:ok, results} <- search(:raw, "users", "user", q) do
-      results
-      |> Enum.map(fn result -> result["_id"] end)
-      |> Pleroma.User.get_all_by_ids()
-    else
-      e ->
-        Logger.error(e)
-        []
-    end
-  end
-
-  def search(:hashtags, q) do
-    with {:ok, results} <- search(:raw, "hashtags", "hashtag", q) do
-      results
-      |> Enum.map(fn result -> result["_source"]["hashtag"] end)
-    else
-      e ->
-        Logger.error(e)
-        []
-    end
-  end
-end
index e38c681bae9ad3cacaf5e826558d58e58c3d2eba..24adfabd717c885368c18ee733a2a42e1721e32b 100644 (file)
@@ -5,9 +5,12 @@
 defmodule Pleroma.Emails.UserEmail do
   @moduledoc "User emails"
 
+  require Pleroma.Web.Gettext
+
   alias Pleroma.Config
   alias Pleroma.User
   alias Pleroma.Web.Endpoint
+  alias Pleroma.Web.Gettext
   alias Pleroma.Web.Router
 
   import Swoosh.Email
@@ -27,29 +30,75 @@ defmodule Pleroma.Emails.UserEmail do
 
   @spec welcome(User.t(), map()) :: Swoosh.Email.t()
   def welcome(user, opts \\ %{}) do
-    new()
-    |> to(recipient(user))
-    |> from(Map.get(opts, :sender, sender()))
-    |> subject(Map.get(opts, :subject, "Welcome to #{instance_name()}!"))
-    |> html_body(Map.get(opts, :html, "Welcome to #{instance_name()}!"))
-    |> text_body(Map.get(opts, :text, "Welcome to #{instance_name()}!"))
+    Gettext.with_locale_or_default user.language do
+      new()
+      |> to(recipient(user))
+      |> from(Map.get(opts, :sender, sender()))
+      |> subject(
+        Map.get(
+          opts,
+          :subject,
+          Gettext.dpgettext(
+            "static_pages",
+            "welcome email subject",
+            "Welcome to %{instance_name}!",
+            instance_name: instance_name()
+          )
+        )
+      )
+      |> html_body(
+        Map.get(
+          opts,
+          :html,
+          Gettext.dpgettext(
+            "static_pages",
+            "welcome email html body",
+            "Welcome to %{instance_name}!",
+            instance_name: instance_name()
+          )
+        )
+      )
+      |> text_body(
+        Map.get(
+          opts,
+          :text,
+          Gettext.dpgettext(
+            "static_pages",
+            "welcome email text body",
+            "Welcome to %{instance_name}!",
+            instance_name: instance_name()
+          )
+        )
+      )
+    end
   end
 
   def password_reset_email(user, token) when is_binary(token) do
-    password_reset_url = Router.Helpers.reset_password_url(Endpoint, :reset, token)
-
-    html_body = """
-    <h3>Reset your password at #{instance_name()}</h3>
-    <p>Someone has requested password change for your account at #{instance_name()}.</p>
-    <p>If it was you, visit the following link to proceed: <a href="#{password_reset_url}">reset password</a>.</p>
-    <p>If it was someone else, nothing to worry about: your data is secure and your password has not been changed.</p>
-    """
-
-    new()
-    |> to(recipient(user))
-    |> from(sender())
-    |> subject("Password reset")
-    |> html_body(html_body)
+    Gettext.with_locale_or_default user.language do
+      password_reset_url = Router.Helpers.reset_password_url(Endpoint, :reset, token)
+
+      html_body =
+        Gettext.dpgettext(
+          "static_pages",
+          "password reset email body",
+          """
+          <h3>Reset your password at %{instance_name}</h3>
+          <p>Someone has requested password change for your account at %{instance_name}.</p>
+          <p>If it was you, visit the following link to proceed: <a href="%{password_reset_url}">reset password</a>.</p>
+          <p>If it was someone else, nothing to worry about: your data is secure and your password has not been changed.</p>
+          """,
+          instance_name: instance_name(),
+          password_reset_url: password_reset_url
+        )
+
+      new()
+      |> to(recipient(user))
+      |> from(sender())
+      |> subject(
+        Gettext.dpgettext("static_pages", "password reset email subject", "Password reset")
+      )
+      |> html_body(html_body)
+    end
   end
 
   def user_invitation_email(
@@ -58,73 +107,136 @@ defmodule Pleroma.Emails.UserEmail do
         to_email,
         to_name \\ nil
       ) do
-    registration_url =
-      Router.Helpers.redirect_url(
-        Endpoint,
-        :registration_page,
-        user_invite_token.token
-      )
+    Gettext.with_locale_or_default user.language do
+      registration_url =
+        Router.Helpers.redirect_url(
+          Endpoint,
+          :registration_page,
+          user_invite_token.token
+        )
+
+      html_body =
+        Gettext.dpgettext(
+          "static_pages",
+          "user invitation email body",
+          """
+          <h3>You are invited to %{instance_name}</h3>
+          <p>%{inviter_name} invites you to join %{instance_name}, an instance of Pleroma federated social networking platform.</p>
+          <p>Click the following link to register: <a href="%{registration_url}">accept invitation</a>.</p>
+          """,
+          instance_name: instance_name(),
+          inviter_name: user.name,
+          registration_url: registration_url
+        )
 
-    html_body = """
-    <h3>You are invited to #{instance_name()}</h3>
-    <p>#{user.name} invites you to join #{instance_name()}, an instance of Pleroma federated social networking platform.</p>
-    <p>Click the following link to register: <a href="#{registration_url}">accept invitation</a>.</p>
-    """
-
-    new()
-    |> to(recipient(to_email, to_name))
-    |> from(sender())
-    |> subject("Invitation to #{instance_name()}")
-    |> html_body(html_body)
+      new()
+      |> to(recipient(to_email, to_name))
+      |> from(sender())
+      |> subject(
+        Gettext.dpgettext(
+          "static_pages",
+          "user invitation email subject",
+          "Invitation to %{instance_name}",
+          instance_name: instance_name()
+        )
+      )
+      |> html_body(html_body)
+    end
   end
 
   def account_confirmation_email(user) do
-    confirmation_url =
-      Router.Helpers.confirm_email_url(
-        Endpoint,
-        :confirm_email,
-        user.id,
-        to_string(user.confirmation_token)
-      )
+    Gettext.with_locale_or_default user.language do
+      confirmation_url =
+        Router.Helpers.confirm_email_url(
+          Endpoint,
+          :confirm_email,
+          user.id,
+          to_string(user.confirmation_token)
+        )
+
+      html_body =
+        Gettext.dpgettext(
+          "static_pages",
+          "confirmation email body",
+          """
+          <h3>Thank you for registering on %{instance_name}</h3>
+          <p>Email confirmation is required to activate the account.</p>
+          <p>Please click the following link to <a href="%{confirmation_url}">activate your account</a>.</p>
+          """,
+          instance_name: instance_name(),
+          confirmation_url: confirmation_url
+        )
 
-    html_body = """
-    <h3>Thank you for registering on #{instance_name()}</h3>
-    <p>Email confirmation is required to activate the account.</p>
-    <p>Please click the following link to <a href="#{confirmation_url}">activate your account</a>.</p>
-    """
-
-    new()
-    |> to(recipient(user))
-    |> from(sender())
-    |> subject("#{instance_name()} account confirmation")
-    |> html_body(html_body)
+      new()
+      |> to(recipient(user))
+      |> from(sender())
+      |> subject(
+        Gettext.dpgettext(
+          "static_pages",
+          "confirmation email subject",
+          "%{instance_name} account confirmation",
+          instance_name: instance_name()
+        )
+      )
+      |> html_body(html_body)
+    end
   end
 
   def approval_pending_email(user) do
-    html_body = """
-    <h3>Awaiting Approval</h3>
-    <p>Your account at #{instance_name()} is being reviewed by staff. You will receive another email once your account is approved.</p>
-    """
-
-    new()
-    |> to(recipient(user))
-    |> from(sender())
-    |> subject("Your account is awaiting approval")
-    |> html_body(html_body)
+    Gettext.with_locale_or_default user.language do
+      html_body =
+        Gettext.dpgettext(
+          "static_pages",
+          "approval pending email body",
+          """
+          <h3>Awaiting Approval</h3>
+          <p>Your account at %{instance_name} is being reviewed by staff. You will receive another email once your account is approved.</p>
+          """,
+          instance_name: instance_name()
+        )
+
+      new()
+      |> to(recipient(user))
+      |> from(sender())
+      |> subject(
+        Gettext.dpgettext(
+          "static_pages",
+          "approval pending email subject",
+          "Your account is awaiting approval"
+        )
+      )
+      |> html_body(html_body)
+    end
   end
 
   def successful_registration_email(user) do
-    html_body = """
-    <h3>Hello @#{user.nickname},</h3>
-    <p>Your account at #{instance_name()} has been registered successfully.</p>
-    <p>No further action is required to activate your account.</p>
-    """
-
-    new()
-    |> to(recipient(user))
-    |> from(sender())
-    |> subject("Account registered on #{instance_name()}")
-    |> html_body(html_body)
+    Gettext.with_locale_or_default user.language do
+      html_body =
+        Gettext.dpgettext(
+          "static_pages",
+          "successful registration email body",
+          """
+          <h3>Hello @%{nickname},</h3>
+          <p>Your account at %{instance_name} has been registered successfully.</p>
+          <p>No further action is required to activate your account.</p>
+          """,
+          nickname: user.nickname,
+          instance_name: instance_name()
+        )
+
+      new()
+      |> to(recipient(user))
+      |> from(sender())
+      |> subject(
+        Gettext.dpgettext(
+          "static_pages",
+          "successful registration email subject",
+          "Account registered on %{instance_name}",
+          instance_name: instance_name()
+        )
+      )
+      |> html_body(html_body)
+    end
   end
 
   @doc """
@@ -134,69 +246,78 @@ defmodule Pleroma.Emails.UserEmail do
   """
   @spec digest_email(User.t()) :: Swoosh.Email.t() | nil
   def digest_email(user) do
-    notifications = Pleroma.Notification.for_user_since(user, user.last_digest_emailed_at)
-
-    mentions =
-      notifications
-      |> Enum.filter(&(&1.activity.data["type"] == "Create"))
-      |> Enum.map(fn notification ->
-        object = Pleroma.Object.normalize(notification.activity, fetch: false)
-
-        if not is_nil(object) do
-          object = update_in(object.data["content"], &format_links/1)
-
-          %{
-            data: notification,
-            object: object,
-            from: User.get_by_ap_id(notification.activity.actor)
-          }
-        end
-      end)
-      |> Enum.filter(& &1)
-
-    followers =
-      notifications
-      |> Enum.filter(&(&1.activity.data["type"] == "Follow"))
-      |> Enum.map(fn notification ->
-        from = User.get_by_ap_id(notification.activity.actor)
-
-        if not is_nil(from) do
-          %{
-            data: notification,
-            object: Pleroma.Object.normalize(notification.activity, fetch: false),
-            from: User.get_by_ap_id(notification.activity.actor)
-          }
-        end
-      end)
-      |> Enum.filter(& &1)
-
-    unless Enum.empty?(mentions) do
-      styling = Config.get([__MODULE__, :styling])
-      logo = Config.get([__MODULE__, :logo])
-
-      html_data = %{
-        instance: instance_name(),
-        user: user,
-        mentions: mentions,
-        followers: followers,
-        unsubscribe_link: unsubscribe_url(user, "digest"),
-        styling: styling
-      }
-
-      logo_path =
-        if is_nil(logo) do
-          Path.join(:code.priv_dir(:pleroma), "static/static/logo.svg")
-        else
-          Path.join(Config.get([:instance, :static_dir]), logo)
-        end
-
-      new()
-      |> to(recipient(user))
-      |> from(sender())
-      |> subject("Your digest from #{instance_name()}")
-      |> put_layout(false)
-      |> render_body("digest.html", html_data)
-      |> attachment(Swoosh.Attachment.new(logo_path, filename: "logo.svg", type: :inline))
+    Gettext.with_locale_or_default user.language do
+      notifications = Pleroma.Notification.for_user_since(user, user.last_digest_emailed_at)
+
+      mentions =
+        notifications
+        |> Enum.filter(&(&1.activity.data["type"] == "Create"))
+        |> Enum.map(fn notification ->
+          object = Pleroma.Object.normalize(notification.activity, fetch: false)
+
+          if not is_nil(object) do
+            object = update_in(object.data["content"], &format_links/1)
+
+            %{
+              data: notification,
+              object: object,
+              from: User.get_by_ap_id(notification.activity.actor)
+            }
+          end
+        end)
+        |> Enum.filter(& &1)
+
+      followers =
+        notifications
+        |> Enum.filter(&(&1.activity.data["type"] == "Follow"))
+        |> Enum.map(fn notification ->
+          from = User.get_by_ap_id(notification.activity.actor)
+
+          if not is_nil(from) do
+            %{
+              data: notification,
+              object: Pleroma.Object.normalize(notification.activity, fetch: false),
+              from: User.get_by_ap_id(notification.activity.actor)
+            }
+          end
+        end)
+        |> Enum.filter(& &1)
+
+      unless Enum.empty?(mentions) do
+        styling = Config.get([__MODULE__, :styling])
+        logo = Config.get([__MODULE__, :logo])
+
+        html_data = %{
+          instance: instance_name(),
+          user: user,
+          mentions: mentions,
+          followers: followers,
+          unsubscribe_link: unsubscribe_url(user, "digest"),
+          styling: styling
+        }
+
+        logo_path =
+          if is_nil(logo) do
+            Path.join(:code.priv_dir(:pleroma), "static/static/logo.svg")
+          else
+            Path.join(Config.get([:instance, :static_dir]), logo)
+          end
+
+        new()
+        |> to(recipient(user))
+        |> from(sender())
+        |> subject(
+          Gettext.dpgettext(
+            "static_pages",
+            "digest email subject",
+            "Your digest from %{instance_name}",
+            instance_name: instance_name()
+          )
+        )
+        |> put_layout(false)
+        |> render_body("digest.html", html_data)
+        |> attachment(Swoosh.Attachment.new(logo_path, filename: "logo.svg", type: :inline))
+      end
     end
   end
 
@@ -226,27 +347,47 @@ defmodule Pleroma.Emails.UserEmail do
 
   def backup_is_ready_email(backup, admin_user_id \\ nil) do
     %{user: user} = Pleroma.Repo.preload(backup, :user)
-    download_url = Pleroma.Web.PleromaAPI.BackupView.download_url(backup)
-
-    html_body =
-      if is_nil(admin_user_id) do
-        """
-        <p>You requested a full backup of your Pleroma account. It's ready for download:</p>
-        <p><a href="#{download_url}">#{download_url}</a></p>
-        """
-      else
-        admin = Pleroma.Repo.get(User, admin_user_id)
-
-        """
-        <p>Admin @#{admin.nickname} requested a full backup of your Pleroma account. It's ready for download:</p>
-        <p><a href="#{download_url}">#{download_url}</a></p>
-        """
-      end
 
-    new()
-    |> to(recipient(user))
-    |> from(sender())
-    |> subject("Your account archive is ready")
-    |> html_body(html_body)
+    Gettext.with_locale_or_default user.language do
+      download_url = Pleroma.Web.PleromaAPI.BackupView.download_url(backup)
+
+      html_body =
+        if is_nil(admin_user_id) do
+          Gettext.dpgettext(
+            "static_pages",
+            "account archive email body - self-requested",
+            """
+            <p>You requested a full backup of your Pleroma account. It's ready for download:</p>
+            <p><a href="%{download_url}">%{download_url}</a></p>
+            """,
+            download_url: download_url
+          )
+        else
+          admin = Pleroma.Repo.get(User, admin_user_id)
+
+          Gettext.dpgettext(
+            "static_pages",
+            "account archive email body - admin requested",
+            """
+            <p>Admin @%{admin_nickname} requested a full backup of your Pleroma account. It's ready for download:</p>
+            <p><a href="%{download_url}">%{download_url}</a></p>
+            """,
+            admin_nickname: admin.nickname,
+            download_url: download_url
+          )
+        end
+
+      new()
+      |> to(recipient(user))
+      |> from(sender())
+      |> subject(
+        Gettext.dpgettext(
+          "static_pages",
+          "account archive email subject",
+          "Your account archive is ready"
+        )
+      )
+      |> html_body(html_body)
+    end
   end
 end
index d3c6d12bd5905ef3244844f6dd510637bfb3568c..dd54933661c85c7337a71a35eee93e9094ba2fe2 100644 (file)
@@ -1,11 +1,11 @@
 # emoji-test.txt
-# Date: 2020-09-12, 22:19:50 GMT
-# © 2020 Unicode®, Inc.
+# Date: 2021-08-26, 17:22:23 GMT
+# © 2021 Unicode®, Inc.
 # Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries.
 # For terms of use, see http://www.unicode.org/terms_of_use.html
 #
 # Emoji Keyboard/Display Test Data for UTS #51
-# Version: 13.1
+# Version: 14.0
 #
 # For documentation and usage, see http://www.unicode.org/reports/tr51
 #
@@ -43,6 +43,7 @@
 1F602                                                  ; fully-qualified     # 😂 E0.6 face with tears of joy
 1F642                                                  ; fully-qualified     # 🙂 E1.0 slightly smiling face
 1F643                                                  ; fully-qualified     # 🙃 E1.0 upside-down face
+1FAE0                                                  ; fully-qualified     # 🫠 E14.0 melting face
 1F609                                                  ; fully-qualified     # 😉 E0.6 winking face
 1F60A                                                  ; fully-qualified     # 😊 E0.6 smiling face with smiling eyes
 1F607                                                  ; fully-qualified     # 😇 E1.0 smiling face with halo
 1F911                                                  ; fully-qualified     # 🤑 E1.0 money-mouth face
 
 # subgroup: face-hand
-1F917                                                  ; fully-qualified     # 🤗 E1.0 hugging face
+1F917                                                  ; fully-qualified     # 🤗 E1.0 smiling face with open hands
 1F92D                                                  ; fully-qualified     # 🤭 E5.0 face with hand over mouth
+1FAE2                                                  ; fully-qualified     # 🫢 E14.0 face with open eyes and hand over mouth
+1FAE3                                                  ; fully-qualified     # 🫣 E14.0 face with peeking eye
 1F92B                                                  ; fully-qualified     # 🤫 E5.0 shushing face
 1F914                                                  ; fully-qualified     # 🤔 E1.0 thinking face
+1FAE1                                                  ; fully-qualified     # 🫡 E14.0 saluting face
 
 # subgroup: face-neutral-skeptical
 1F910                                                  ; fully-qualified     # 🤐 E1.0 zipper-mouth face
@@ -79,6 +83,7 @@
 1F610                                                  ; fully-qualified     # 😐 E0.7 neutral face
 1F611                                                  ; fully-qualified     # 😑 E1.0 expressionless face
 1F636                                                  ; fully-qualified     # 😶 E1.0 face without mouth
+1FAE5                                                  ; fully-qualified     # 🫥 E14.0 dotted line face
 1F636 200D 1F32B FE0F                                  ; fully-qualified     # 😶‍🌫️ E13.1 face in clouds
 1F636 200D 1F32B                                       ; minimally-qualified # 😶‍🌫 E13.1 face in clouds
 1F60F                                                  ; fully-qualified     # 😏 E0.6 smirking face
 1F975                                                  ; fully-qualified     # 🥵 E11.0 hot face
 1F976                                                  ; fully-qualified     # 🥶 E11.0 cold face
 1F974                                                  ; fully-qualified     # 🥴 E11.0 woozy face
-1F635                                                  ; fully-qualified     # 😵 E0.6 knocked-out face
+1F635                                                  ; fully-qualified     # 😵 E0.6 face with crossed-out eyes
 1F635 200D 1F4AB                                       ; fully-qualified     # 😵‍💫 E13.1 face with spiral eyes
 1F92F                                                  ; fully-qualified     # 🤯 E5.0 exploding head
 
 
 # subgroup: face-concerned
 1F615                                                  ; fully-qualified     # 😕 E1.0 confused face
+1FAE4                                                  ; fully-qualified     # 🫤 E14.0 face with diagonal mouth
 1F61F                                                  ; fully-qualified     # 😟 E1.0 worried face
 1F641                                                  ; fully-qualified     # 🙁 E1.0 slightly frowning face
 2639 FE0F                                              ; fully-qualified     # ☹️ E0.7 frowning face
 1F632                                                  ; fully-qualified     # 😲 E0.6 astonished face
 1F633                                                  ; fully-qualified     # 😳 E0.6 flushed face
 1F97A                                                  ; fully-qualified     # 🥺 E11.0 pleading face
+1F979                                                  ; fully-qualified     # 🥹 E14.0 face holding back tears
 1F626                                                  ; fully-qualified     # 😦 E1.0 frowning face with open mouth
 1F627                                                  ; fully-qualified     # 😧 E1.0 anguished face
 1F628                                                  ; fully-qualified     # 😨 E0.6 fearful face
 1F4AD                                                  ; fully-qualified     # 💭 E1.0 thought balloon
 1F4A4                                                  ; fully-qualified     # 💤 E0.6 zzz
 
-# Smileys & Emotion subtotal:          170
-# Smileys & Emotion subtotal:          170     w/o modifiers
+# Smileys & Emotion subtotal:          177
+# Smileys & Emotion subtotal:          177     w/o modifiers
 
 # group: People & Body
 
 1F596 1F3FD                                            ; fully-qualified     # 🖖🏽 E1.0 vulcan salute: medium skin tone
 1F596 1F3FE                                            ; fully-qualified     # 🖖🏾 E1.0 vulcan salute: medium-dark skin tone
 1F596 1F3FF                                            ; fully-qualified     # 🖖🏿 E1.0 vulcan salute: dark skin tone
+1FAF1                                                  ; fully-qualified     # 🫱 E14.0 rightwards hand
+1FAF1 1F3FB                                            ; fully-qualified     # 🫱🏻 E14.0 rightwards hand: light skin tone
+1FAF1 1F3FC                                            ; fully-qualified     # 🫱🏼 E14.0 rightwards hand: medium-light skin tone
+1FAF1 1F3FD                                            ; fully-qualified     # 🫱🏽 E14.0 rightwards hand: medium skin tone
+1FAF1 1F3FE                                            ; fully-qualified     # 🫱🏾 E14.0 rightwards hand: medium-dark skin tone
+1FAF1 1F3FF                                            ; fully-qualified     # 🫱🏿 E14.0 rightwards hand: dark skin tone
+1FAF2                                                  ; fully-qualified     # 🫲 E14.0 leftwards hand
+1FAF2 1F3FB                                            ; fully-qualified     # 🫲🏻 E14.0 leftwards hand: light skin tone
+1FAF2 1F3FC                                            ; fully-qualified     # 🫲🏼 E14.0 leftwards hand: medium-light skin tone
+1FAF2 1F3FD                                            ; fully-qualified     # 🫲🏽 E14.0 leftwards hand: medium skin tone
+1FAF2 1F3FE                                            ; fully-qualified     # 🫲🏾 E14.0 leftwards hand: medium-dark skin tone
+1FAF2 1F3FF                                            ; fully-qualified     # 🫲🏿 E14.0 leftwards hand: dark skin tone
+1FAF3                                                  ; fully-qualified     # 🫳 E14.0 palm down hand
+1FAF3 1F3FB                                            ; fully-qualified     # 🫳🏻 E14.0 palm down hand: light skin tone
+1FAF3 1F3FC                                            ; fully-qualified     # 🫳🏼 E14.0 palm down hand: medium-light skin tone
+1FAF3 1F3FD                                            ; fully-qualified     # 🫳🏽 E14.0 palm down hand: medium skin tone
+1FAF3 1F3FE                                            ; fully-qualified     # 🫳🏾 E14.0 palm down hand: medium-dark skin tone
+1FAF3 1F3FF                                            ; fully-qualified     # 🫳🏿 E14.0 palm down hand: dark skin tone
+1FAF4                                                  ; fully-qualified     # 🫴 E14.0 palm up hand
+1FAF4 1F3FB                                            ; fully-qualified     # 🫴🏻 E14.0 palm up hand: light skin tone
+1FAF4 1F3FC                                            ; fully-qualified     # 🫴🏼 E14.0 palm up hand: medium-light skin tone
+1FAF4 1F3FD                                            ; fully-qualified     # 🫴🏽 E14.0 palm up hand: medium skin tone
+1FAF4 1F3FE                                            ; fully-qualified     # 🫴🏾 E14.0 palm up hand: medium-dark skin tone
+1FAF4 1F3FF                                            ; fully-qualified     # 🫴🏿 E14.0 palm up hand: dark skin tone
 
 # subgroup: hand-fingers-partial
 1F44C                                                  ; fully-qualified     # 👌 E0.6 OK hand
 1F91E 1F3FD                                            ; fully-qualified     # 🤞🏽 E3.0 crossed fingers: medium skin tone
 1F91E 1F3FE                                            ; fully-qualified     # 🤞🏾 E3.0 crossed fingers: medium-dark skin tone
 1F91E 1F3FF                                            ; fully-qualified     # 🤞🏿 E3.0 crossed fingers: dark skin tone
+1FAF0                                                  ; fully-qualified     # 🫰 E14.0 hand with index finger and thumb crossed
+1FAF0 1F3FB                                            ; fully-qualified     # 🫰🏻 E14.0 hand with index finger and thumb crossed: light skin tone
+1FAF0 1F3FC                                            ; fully-qualified     # 🫰🏼 E14.0 hand with index finger and thumb crossed: medium-light skin tone
+1FAF0 1F3FD                                            ; fully-qualified     # 🫰🏽 E14.0 hand with index finger and thumb crossed: medium skin tone
+1FAF0 1F3FE                                            ; fully-qualified     # 🫰🏾 E14.0 hand with index finger and thumb crossed: medium-dark skin tone
+1FAF0 1F3FF                                            ; fully-qualified     # 🫰🏿 E14.0 hand with index finger and thumb crossed: dark skin tone
 1F91F                                                  ; fully-qualified     # 🤟 E5.0 love-you gesture
 1F91F 1F3FB                                            ; fully-qualified     # 🤟🏻 E5.0 love-you gesture: light skin tone
 1F91F 1F3FC                                            ; fully-qualified     # 🤟🏼 E5.0 love-you gesture: medium-light skin tone
 261D 1F3FD                                             ; fully-qualified     # ☝🏽 E1.0 index pointing up: medium skin tone
 261D 1F3FE                                             ; fully-qualified     # ☝🏾 E1.0 index pointing up: medium-dark skin tone
 261D 1F3FF                                             ; fully-qualified     # ☝🏿 E1.0 index pointing up: dark skin tone
+1FAF5                                                  ; fully-qualified     # 🫵 E14.0 index pointing at the viewer
+1FAF5 1F3FB                                            ; fully-qualified     # 🫵🏻 E14.0 index pointing at the viewer: light skin tone
+1FAF5 1F3FC                                            ; fully-qualified     # 🫵🏼 E14.0 index pointing at the viewer: medium-light skin tone
+1FAF5 1F3FD                                            ; fully-qualified     # 🫵🏽 E14.0 index pointing at the viewer: medium skin tone
+1FAF5 1F3FE                                            ; fully-qualified     # 🫵🏾 E14.0 index pointing at the viewer: medium-dark skin tone
+1FAF5 1F3FF                                            ; fully-qualified     # 🫵🏿 E14.0 index pointing at the viewer: dark skin tone
 
 # subgroup: hand-fingers-closed
 1F44D                                                  ; fully-qualified     # 👍 E0.6 thumbs up
 1F64C 1F3FD                                            ; fully-qualified     # 🙌🏽 E1.0 raising hands: medium skin tone
 1F64C 1F3FE                                            ; fully-qualified     # 🙌🏾 E1.0 raising hands: medium-dark skin tone
 1F64C 1F3FF                                            ; fully-qualified     # 🙌🏿 E1.0 raising hands: dark skin tone
+1FAF6                                                  ; fully-qualified     # 🫶 E14.0 heart hands
+1FAF6 1F3FB                                            ; fully-qualified     # 🫶🏻 E14.0 heart hands: light skin tone
+1FAF6 1F3FC                                            ; fully-qualified     # 🫶🏼 E14.0 heart hands: medium-light skin tone
+1FAF6 1F3FD                                            ; fully-qualified     # 🫶🏽 E14.0 heart hands: medium skin tone
+1FAF6 1F3FE                                            ; fully-qualified     # 🫶🏾 E14.0 heart hands: medium-dark skin tone
+1FAF6 1F3FF                                            ; fully-qualified     # 🫶🏿 E14.0 heart hands: dark skin tone
 1F450                                                  ; fully-qualified     # 👐 E0.6 open hands
 1F450 1F3FB                                            ; fully-qualified     # 👐🏻 E1.0 open hands: light skin tone
 1F450 1F3FC                                            ; fully-qualified     # 👐🏼 E1.0 open hands: medium-light skin tone
 1F932 1F3FE                                            ; fully-qualified     # 🤲🏾 E5.0 palms up together: medium-dark skin tone
 1F932 1F3FF                                            ; fully-qualified     # 🤲🏿 E5.0 palms up together: dark skin tone
 1F91D                                                  ; fully-qualified     # 🤝 E3.0 handshake
+1F91D 1F3FB                                            ; fully-qualified     # 🤝🏻 E3.0 handshake: light skin tone
+1F91D 1F3FC                                            ; fully-qualified     # 🤝🏼 E3.0 handshake: medium-light skin tone
+1F91D 1F3FD                                            ; fully-qualified     # 🤝🏽 E3.0 handshake: medium skin tone
+1F91D 1F3FE                                            ; fully-qualified     # 🤝🏾 E3.0 handshake: medium-dark skin tone
+1F91D 1F3FF                                            ; fully-qualified     # 🤝🏿 E3.0 handshake: dark skin tone
+1FAF1 1F3FB 200D 1FAF2 1F3FC                           ; fully-qualified     # 🫱🏻‍🫲🏼 E14.0 handshake: light skin tone, medium-light skin tone
+1FAF1 1F3FB 200D 1FAF2 1F3FD                           ; fully-qualified     # 🫱🏻‍🫲🏽 E14.0 handshake: light skin tone, medium skin tone
+1FAF1 1F3FB 200D 1FAF2 1F3FE                           ; fully-qualified     # 🫱🏻‍🫲🏾 E14.0 handshake: light skin tone, medium-dark skin tone
+1FAF1 1F3FB 200D 1FAF2 1F3FF                           ; fully-qualified     # 🫱🏻‍🫲🏿 E14.0 handshake: light skin tone, dark skin tone
+1FAF1 1F3FC 200D 1FAF2 1F3FB                           ; fully-qualified     # 🫱🏼‍🫲🏻 E14.0 handshake: medium-light skin tone, light skin tone
+1FAF1 1F3FC 200D 1FAF2 1F3FD                           ; fully-qualified     # 🫱🏼‍🫲🏽 E14.0 handshake: medium-light skin tone, medium skin tone
+1FAF1 1F3FC 200D 1FAF2 1F3FE                           ; fully-qualified     # 🫱🏼‍🫲🏾 E14.0 handshake: medium-light skin tone, medium-dark skin tone
+1FAF1 1F3FC 200D 1FAF2 1F3FF                           ; fully-qualified     # 🫱🏼‍🫲🏿 E14.0 handshake: medium-light skin tone, dark skin tone
+1FAF1 1F3FD 200D 1FAF2 1F3FB                           ; fully-qualified     # 🫱🏽‍🫲🏻 E14.0 handshake: medium skin tone, light skin tone
+1FAF1 1F3FD 200D 1FAF2 1F3FC                           ; fully-qualified     # 🫱🏽‍🫲🏼 E14.0 handshake: medium skin tone, medium-light skin tone
+1FAF1 1F3FD 200D 1FAF2 1F3FE                           ; fully-qualified     # 🫱🏽‍🫲🏾 E14.0 handshake: medium skin tone, medium-dark skin tone
+1FAF1 1F3FD 200D 1FAF2 1F3FF                           ; fully-qualified     # 🫱🏽‍🫲🏿 E14.0 handshake: medium skin tone, dark skin tone
+1FAF1 1F3FE 200D 1FAF2 1F3FB                           ; fully-qualified     # 🫱🏾‍🫲🏻 E14.0 handshake: medium-dark skin tone, light skin tone
+1FAF1 1F3FE 200D 1FAF2 1F3FC                           ; fully-qualified     # 🫱🏾‍🫲🏼 E14.0 handshake: medium-dark skin tone, medium-light skin tone
+1FAF1 1F3FE 200D 1FAF2 1F3FD                           ; fully-qualified     # 🫱🏾‍🫲🏽 E14.0 handshake: medium-dark skin tone, medium skin tone
+1FAF1 1F3FE 200D 1FAF2 1F3FF                           ; fully-qualified     # 🫱🏾‍🫲🏿 E14.0 handshake: medium-dark skin tone, dark skin tone
+1FAF1 1F3FF 200D 1FAF2 1F3FB                           ; fully-qualified     # 🫱🏿‍🫲🏻 E14.0 handshake: dark skin tone, light skin tone
+1FAF1 1F3FF 200D 1FAF2 1F3FC                           ; fully-qualified     # 🫱🏿‍🫲🏼 E14.0 handshake: dark skin tone, medium-light skin tone
+1FAF1 1F3FF 200D 1FAF2 1F3FD                           ; fully-qualified     # 🫱🏿‍🫲🏽 E14.0 handshake: dark skin tone, medium skin tone
+1FAF1 1F3FF 200D 1FAF2 1F3FE                           ; fully-qualified     # 🫱🏿‍🫲🏾 E14.0 handshake: dark skin tone, medium-dark skin tone
 1F64F                                                  ; fully-qualified     # 🙏 E0.6 folded hands
 1F64F 1F3FB                                            ; fully-qualified     # 🙏🏻 E1.0 folded hands: light skin tone
 1F64F 1F3FC                                            ; fully-qualified     # 🙏🏼 E1.0 folded hands: medium-light skin tone
 1F441                                                  ; unqualified         # 👁 E0.7 eye
 1F445                                                  ; fully-qualified     # 👅 E0.6 tongue
 1F444                                                  ; fully-qualified     # 👄 E0.6 mouth
+1FAE6                                                  ; fully-qualified     # 🫦 E14.0 biting lip
 
 # subgroup: person
 1F476                                                  ; fully-qualified     # 👶 E0.6 baby
 1F477 1F3FE 200D 2640                                  ; minimally-qualified # 👷🏾‍♀ E4.0 woman construction worker: medium-dark skin tone
 1F477 1F3FF 200D 2640 FE0F                             ; fully-qualified     # 👷🏿‍♀️ E4.0 woman construction worker: dark skin tone
 1F477 1F3FF 200D 2640                                  ; minimally-qualified # 👷🏿‍♀ E4.0 woman construction worker: dark skin tone
+1FAC5                                                  ; fully-qualified     # 🫅 E14.0 person with crown
+1FAC5 1F3FB                                            ; fully-qualified     # 🫅🏻 E14.0 person with crown: light skin tone
+1FAC5 1F3FC                                            ; fully-qualified     # 🫅🏼 E14.0 person with crown: medium-light skin tone
+1FAC5 1F3FD                                            ; fully-qualified     # 🫅🏽 E14.0 person with crown: medium skin tone
+1FAC5 1F3FE                                            ; fully-qualified     # 🫅🏾 E14.0 person with crown: medium-dark skin tone
+1FAC5 1F3FF                                            ; fully-qualified     # 🫅🏿 E14.0 person with crown: dark skin tone
 1F934                                                  ; fully-qualified     # 🤴 E3.0 prince
 1F934 1F3FB                                            ; fully-qualified     # 🤴🏻 E3.0 prince: light skin tone
 1F934 1F3FC                                            ; fully-qualified     # 🤴🏼 E3.0 prince: medium-light skin tone
 1F930 1F3FD                                            ; fully-qualified     # 🤰🏽 E3.0 pregnant woman: medium skin tone
 1F930 1F3FE                                            ; fully-qualified     # 🤰🏾 E3.0 pregnant woman: medium-dark skin tone
 1F930 1F3FF                                            ; fully-qualified     # 🤰🏿 E3.0 pregnant woman: dark skin tone
+1FAC3                                                  ; fully-qualified     # 🫃 E14.0 pregnant man
+1FAC3 1F3FB                                            ; fully-qualified     # 🫃🏻 E14.0 pregnant man: light skin tone
+1FAC3 1F3FC                                            ; fully-qualified     # 🫃🏼 E14.0 pregnant man: medium-light skin tone
+1FAC3 1F3FD                                            ; fully-qualified     # 🫃🏽 E14.0 pregnant man: medium skin tone
+1FAC3 1F3FE                                            ; fully-qualified     # 🫃🏾 E14.0 pregnant man: medium-dark skin tone
+1FAC3 1F3FF                                            ; fully-qualified     # 🫃🏿 E14.0 pregnant man: dark skin tone
+1FAC4                                                  ; fully-qualified     # 🫄 E14.0 pregnant person
+1FAC4 1F3FB                                            ; fully-qualified     # 🫄🏻 E14.0 pregnant person: light skin tone
+1FAC4 1F3FC                                            ; fully-qualified     # 🫄🏼 E14.0 pregnant person: medium-light skin tone
+1FAC4 1F3FD                                            ; fully-qualified     # 🫄🏽 E14.0 pregnant person: medium skin tone
+1FAC4 1F3FE                                            ; fully-qualified     # 🫄🏾 E14.0 pregnant person: medium-dark skin tone
+1FAC4 1F3FF                                            ; fully-qualified     # 🫄🏿 E14.0 pregnant person: dark skin tone
 1F931                                                  ; fully-qualified     # 🤱 E5.0 breast-feeding
 1F931 1F3FB                                            ; fully-qualified     # 🤱🏻 E5.0 breast-feeding: light skin tone
 1F931 1F3FC                                            ; fully-qualified     # 🤱🏼 E5.0 breast-feeding: medium-light skin tone
 1F9DF 200D 2642                                        ; minimally-qualified # 🧟‍♂ E5.0 man zombie
 1F9DF 200D 2640 FE0F                                   ; fully-qualified     # 🧟‍♀️ E5.0 woman zombie
 1F9DF 200D 2640                                        ; minimally-qualified # 🧟‍♀ E5.0 woman zombie
+1F9CC                                                  ; fully-qualified     # 🧌 E14.0 troll
 
 # subgroup: person-activity
 1F486                                                  ; fully-qualified     # 💆 E0.6 person getting massage
 1FAC2                                                  ; fully-qualified     # 🫂 E13.0 people hugging
 1F463                                                  ; fully-qualified     # 👣 E0.6 footprints
 
-# People & Body subtotal:              2899
-# People & Body subtotal:              494     w/o modifiers
+# People & Body subtotal:              2986
+# People & Body subtotal:              506     w/o modifiers
 
 # group: Component
 
 1F988                                                  ; fully-qualified     # 🦈 E3.0 shark
 1F419                                                  ; fully-qualified     # 🐙 E0.6 octopus
 1F41A                                                  ; fully-qualified     # 🐚 E0.6 spiral shell
+1FAB8                                                  ; fully-qualified     # 🪸 E14.0 coral
 
 # subgroup: animal-bug
 1F40C                                                  ; fully-qualified     # 🐌 E0.6 snail
 1F490                                                  ; fully-qualified     # 💐 E0.6 bouquet
 1F338                                                  ; fully-qualified     # 🌸 E0.6 cherry blossom
 1F4AE                                                  ; fully-qualified     # 💮 E0.6 white flower
+1FAB7                                                  ; fully-qualified     # 🪷 E14.0 lotus
 1F3F5 FE0F                                             ; fully-qualified     # 🏵️ E0.7 rosette
 1F3F5                                                  ; unqualified         # 🏵 E0.7 rosette
 1F339                                                  ; fully-qualified     # 🌹 E0.6 rose
 1F341                                                  ; fully-qualified     # 🍁 E0.6 maple leaf
 1F342                                                  ; fully-qualified     # 🍂 E0.6 fallen leaf
 1F343                                                  ; fully-qualified     # 🍃 E0.6 leaf fluttering in wind
+1FAB9                                                  ; fully-qualified     # 🪹 E14.0 empty nest
+1FABA                                                  ; fully-qualified     # 🪺 E14.0 nest with eggs
 
-# Animals & Nature subtotal:           147
-# Animals & Nature subtotal:           147     w/o modifiers
+# Animals & Nature subtotal:           151
+# Animals & Nature subtotal:           151     w/o modifiers
 
 # group: Food & Drink
 
 1F9C5                                                  ; fully-qualified     # 🧅 E12.0 onion
 1F344                                                  ; fully-qualified     # 🍄 E0.6 mushroom
 1F95C                                                  ; fully-qualified     # 🥜 E3.0 peanuts
+1FAD8                                                  ; fully-qualified     # 🫘 E14.0 beans
 1F330                                                  ; fully-qualified     # 🌰 E0.6 chestnut
 
 # subgroup: food-prepared
 1F37B                                                  ; fully-qualified     # 🍻 E0.6 clinking beer mugs
 1F942                                                  ; fully-qualified     # 🥂 E3.0 clinking glasses
 1F943                                                  ; fully-qualified     # 🥃 E3.0 tumbler glass
+1FAD7                                                  ; fully-qualified     # 🫗 E14.0 pouring liquid
 1F964                                                  ; fully-qualified     # 🥤 E5.0 cup with straw
 1F9CB                                                  ; fully-qualified     # 🧋 E13.0 bubble tea
 1F9C3                                                  ; fully-qualified     # 🧃 E12.0 beverage box
 1F374                                                  ; fully-qualified     # 🍴 E0.6 fork and knife
 1F944                                                  ; fully-qualified     # 🥄 E3.0 spoon
 1F52A                                                  ; fully-qualified     # 🔪 E0.6 kitchen knife
+1FAD9                                                  ; fully-qualified     # 🫙 E14.0 jar
 1F3FA                                                  ; fully-qualified     # 🏺 E1.0 amphora
 
-# Food & Drink subtotal:               131
-# Food & Drink subtotal:               131     w/o modifiers
+# Food & Drink subtotal:               134
+# Food & Drink subtotal:               134     w/o modifiers
 
 # group: Travel & Places
 
 2668 FE0F                                              ; fully-qualified     # ♨️ E0.6 hot springs
 2668                                                   ; unqualified         # ♨ E0.6 hot springs
 1F3A0                                                  ; fully-qualified     # 🎠 E0.6 carousel horse
+1F6DD                                                  ; fully-qualified     # 🛝 E14.0 playground slide
 1F3A1                                                  ; fully-qualified     # 🎡 E0.6 ferris wheel
 1F3A2                                                  ; fully-qualified     # 🎢 E0.6 roller coaster
 1F488                                                  ; fully-qualified     # 💈 E0.6 barber pole
 1F6E2 FE0F                                             ; fully-qualified     # 🛢️ E0.7 oil drum
 1F6E2                                                  ; unqualified         # 🛢 E0.7 oil drum
 26FD                                                   ; fully-qualified     # ⛽ E0.6 fuel pump
+1F6DE                                                  ; fully-qualified     # 🛞 E14.0 wheel
 1F6A8                                                  ; fully-qualified     # 🚨 E0.6 police car light
 1F6A5                                                  ; fully-qualified     # 🚥 E0.6 horizontal traffic light
 1F6A6                                                  ; fully-qualified     # 🚦 E1.0 vertical traffic light
 
 # subgroup: transport-water
 2693                                                   ; fully-qualified     # ⚓ E0.6 anchor
+1F6DF                                                  ; fully-qualified     # 🛟 E14.0 ring buoy
 26F5                                                   ; fully-qualified     # ⛵ E0.6 sailboat
 1F6F6                                                  ; fully-qualified     # 🛶 E3.0 canoe
 1F6A4                                                  ; fully-qualified     # 🚤 E0.6 speedboat
 1F4A7                                                  ; fully-qualified     # 💧 E0.6 droplet
 1F30A                                                  ; fully-qualified     # 🌊 E0.6 water wave
 
-# Travel & Places subtotal:            264
-# Travel & Places subtotal:            264     w/o modifiers
+# Travel & Places subtotal:            267
+# Travel & Places subtotal:            267     w/o modifiers
 
 # group: Activities
 
 1F52E                                                  ; fully-qualified     # 🔮 E0.6 crystal ball
 1FA84                                                  ; fully-qualified     # 🪄 E13.0 magic wand
 1F9FF                                                  ; fully-qualified     # 🧿 E11.0 nazar amulet
+1FAAC                                                  ; fully-qualified     # 🪬 E14.0 hamsa
 1F3AE                                                  ; fully-qualified     # 🎮 E0.6 video game
 1F579 FE0F                                             ; fully-qualified     # 🕹️ E0.7 joystick
 1F579                                                  ; unqualified         # 🕹 E0.7 joystick
 1F9E9                                                  ; fully-qualified     # 🧩 E11.0 puzzle piece
 1F9F8                                                  ; fully-qualified     # 🧸 E11.0 teddy bear
 1FA85                                                  ; fully-qualified     # 🪅 E13.0 piñata
+1FAA9                                                  ; fully-qualified     # 🪩 E14.0 mirror ball
 1FA86                                                  ; fully-qualified     # 🪆 E13.0 nesting dolls
 2660 FE0F                                              ; fully-qualified     # ♠️ E0.6 spade suit
 2660                                                   ; unqualified         # ♠ E0.6 spade suit
 1F9F6                                                  ; fully-qualified     # 🧶 E11.0 yarn
 1FAA2                                                  ; fully-qualified     # 🪢 E13.0 knot
 
-# Activities subtotal:         95
-# Activities subtotal:         95      w/o modifiers
+# Activities subtotal:         97
+# Activities subtotal:         97      w/o modifiers
 
 # group: Objects
 
 
 # subgroup: computer
 1F50B                                                  ; fully-qualified     # 🔋 E0.6 battery
+1FAAB                                                  ; fully-qualified     # 🪫 E14.0 low battery
 1F50C                                                  ; fully-qualified     # 🔌 E0.6 electric plug
 1F4BB                                                  ; fully-qualified     # 💻 E0.6 laptop
 1F5A5 FE0F                                             ; fully-qualified     # 🖥️ E0.7 desktop computer
 1FA78                                                  ; fully-qualified     # 🩸 E12.0 drop of blood
 1F48A                                                  ; fully-qualified     # 💊 E0.6 pill
 1FA79                                                  ; fully-qualified     # 🩹 E12.0 adhesive bandage
+1FA7C                                                  ; fully-qualified     # 🩼 E14.0 crutch
 1FA7A                                                  ; fully-qualified     # 🩺 E12.0 stethoscope
+1FA7B                                                  ; fully-qualified     # 🩻 E14.0 x-ray
 
 # subgroup: household
 1F6AA                                                  ; fully-qualified     # 🚪 E0.6 door
 1F9FB                                                  ; fully-qualified     # 🧻 E11.0 roll of paper
 1FAA3                                                  ; fully-qualified     # 🪣 E13.0 bucket
 1F9FC                                                  ; fully-qualified     # 🧼 E11.0 soap
+1FAE7                                                  ; fully-qualified     # 🫧 E14.0 bubbles
 1FAA5                                                  ; fully-qualified     # 🪥 E13.0 toothbrush
 1F9FD                                                  ; fully-qualified     # 🧽 E11.0 sponge
 1F9EF                                                  ; fully-qualified     # 🧯 E11.0 fire extinguisher
 26B1                                                   ; unqualified         # ⚱ E1.0 funeral urn
 1F5FF                                                  ; fully-qualified     # 🗿 E0.6 moai
 1FAA7                                                  ; fully-qualified     # 🪧 E13.0 placard
+1FAAA                                                  ; fully-qualified     # 🪪 E14.0 identification card
 
-# Objects subtotal:            299
-# Objects subtotal:            299     w/o modifiers
+# Objects subtotal:            304
+# Objects subtotal:            304     w/o modifiers
 
 # group: Symbols
 
 2795                                                   ; fully-qualified     # ➕ E0.6 plus
 2796                                                   ; fully-qualified     # ➖ E0.6 minus
 2797                                                   ; fully-qualified     # ➗ E0.6 divide
+1F7F0                                                  ; fully-qualified     # 🟰 E14.0 heavy equals sign
 267E FE0F                                              ; fully-qualified     # ♾️ E11.0 infinity
 267E                                                   ; unqualified         # ♾ E11.0 infinity
 
 1F533                                                  ; fully-qualified     # 🔳 E0.6 white square button
 1F532                                                  ; fully-qualified     # 🔲 E0.6 black square button
 
-# Symbols subtotal:            301
-# Symbols subtotal:            301     w/o modifiers
+# Symbols subtotal:            302
+# Symbols subtotal:            302     w/o modifiers
 
 # group: Flags
 
 # Flags subtotal:              275     w/o modifiers
 
 # Status Counts
-# fully-qualified : 3512
+# fully-qualified : 3624
 # minimally-qualified : 817
 # unqualified : 252
 # component : 9
index cdbfeab02b4b2d974248c23c9f5d3da9f2b39429..53e2e9c897d564dd788306a72fa640ff75cbdce8 100644 (file)
@@ -61,7 +61,6 @@ defmodule Pleroma.Hashtag do
                {:ok, Repo.all(from(ht in Hashtag, where: ht.name in ^names))}
              end)
              |> Repo.transaction() do
-        Pleroma.Elasticsearch.maybe_bulk_post(hashtags, :hashtags)
         {:ok, hashtags}
       else
         {:error, _name, value, _changes_so_far} -> {:error, value}
index 9e0ce0329e2638652eded2a150d4fc1087c9d013..2ab09495d0609b0ae091d38174a1eeca3eefdcb2 100644 (file)
@@ -341,6 +341,14 @@ defmodule Pleroma.Notification do
     |> Repo.delete_all()
   end
 
+  def destroy_multiple_from_types(%{id: user_id}, types) do
+    from(n in Notification,
+      where: n.user_id == ^user_id,
+      where: n.type in ^types
+    )
+    |> Repo.delete_all()
+  end
+
   def dismiss(%Pleroma.Activity{} = activity) do
     Notification
     |> where([n], n.activity_id == ^activity.id)
index 99bce632c266e6571da688873e43d8ddf1412aaa..3b266e59bbb90e8251302ab7d74febde6c541546 100644 (file)
@@ -1,12 +1,17 @@
 defmodule Pleroma.Search do
-  @type search_map :: %{
-          statuses: [map],
-          accounts: [map],
-          hashtags: [map]
-        }
-
-  @doc """
-  Searches for stuff
-  """
-  @callback search(map, map, keyword) :: search_map
+  alias Pleroma.Workers.SearchIndexingWorker
+
+  def add_to_index(%Pleroma.Activity{id: activity_id}) do
+    SearchIndexingWorker.enqueue("add_to_index", %{"activity" => activity_id})
+  end
+
+  def remove_from_index(%Pleroma.Object{id: object_id}) do
+    SearchIndexingWorker.enqueue("remove_from_index", %{"object" => object_id})
+  end
+
+  def search(query, options) do
+    search_module = Pleroma.Config.get([Pleroma.Search, :module], Pleroma.Activity)
+
+    search_module.search(options[:for_user], query, options)
+  end
 end
diff --git a/lib/pleroma/search/builtin.ex b/lib/pleroma/search/builtin.ex
deleted file mode 100644 (file)
index 3cbe220..0000000
+++ /dev/null
@@ -1,138 +0,0 @@
-defmodule Pleroma.Search.Builtin do
-  @behaviour Pleroma.Search
-
-  alias Pleroma.Repo
-  alias Pleroma.User
-  alias Pleroma.Activity
-  alias Pleroma.Web.MastodonAPI.AccountView
-  alias Pleroma.Web.MastodonAPI.StatusView
-  alias Pleroma.Web.Endpoint
-
-  require Logger
-
-  @impl Pleroma.Search
-  def search(_conn, %{q: query} = params, options) do
-    version = Keyword.get(options, :version)
-    timeout = Keyword.get(Repo.config(), :timeout, 15_000)
-    query = String.trim(query)
-    default_values = %{"statuses" => [], "accounts" => [], "hashtags" => []}
-
-    default_values
-    |> Enum.map(fn {resource, default_value} ->
-      if params[:type] in [nil, resource] do
-        {resource, fn -> resource_search(version, resource, query, options) end}
-      else
-        {resource, fn -> default_value end}
-      end
-    end)
-    |> Task.async_stream(fn {resource, f} -> {resource, with_fallback(f)} end,
-      timeout: timeout,
-      on_timeout: :kill_task
-    )
-    |> Enum.reduce(default_values, fn
-      {:ok, {resource, result}}, acc ->
-        Map.put(acc, resource, result)
-
-      _error, acc ->
-        acc
-    end)
-  end
-
-  defp resource_search(_, "accounts", query, options) do
-    accounts = with_fallback(fn -> User.search(query, options) end)
-
-    AccountView.render("index.json",
-      users: accounts,
-      for: options[:for_user],
-      embed_relationships: options[:embed_relationships]
-    )
-  end
-
-  defp resource_search(_, "statuses", query, options) do
-    statuses = with_fallback(fn -> Activity.search(options[:for_user], query, options) end)
-
-    StatusView.render("index.json",
-      activities: statuses,
-      for: options[:for_user],
-      as: :activity
-    )
-  end
-
-  defp resource_search(:v2, "hashtags", query, options) do
-    tags_path = Endpoint.url() <> "/tag/"
-
-    query
-    |> prepare_tags(options)
-    |> Enum.map(fn tag ->
-      %{name: tag, url: tags_path <> tag}
-    end)
-  end
-
-  defp resource_search(:v1, "hashtags", query, options) do
-    prepare_tags(query, options)
-  end
-
-  defp prepare_tags(query, options) do
-    tags =
-      query
-      |> preprocess_uri_query()
-      |> String.split(~r/[^#\w]+/u, trim: true)
-      |> Enum.uniq_by(&String.downcase/1)
-
-    explicit_tags = Enum.filter(tags, fn tag -> String.starts_with?(tag, "#") end)
-
-    tags =
-      if Enum.any?(explicit_tags) do
-        explicit_tags
-      else
-        tags
-      end
-
-    tags = Enum.map(tags, fn tag -> String.trim_leading(tag, "#") end)
-
-    tags =
-      if Enum.empty?(explicit_tags) && !options[:skip_joined_tag] do
-        add_joined_tag(tags)
-      else
-        tags
-      end
-
-    Pleroma.Pagination.paginate(tags, options)
-  end
-
-  # If `query` is a URI, returns last component of its path, otherwise returns `query`
-  defp preprocess_uri_query(query) do
-    if query =~ ~r/https?:\/\// do
-      query
-      |> String.trim_trailing("/")
-      |> URI.parse()
-      |> Map.get(:path)
-      |> String.split("/")
-      |> Enum.at(-1)
-    else
-      query
-    end
-  end
-
-  defp add_joined_tag(tags) do
-    tags
-    |> Kernel.++([joined_tag(tags)])
-    |> Enum.uniq_by(&String.downcase/1)
-  end
-
-  defp joined_tag(tags) do
-    tags
-    |> Enum.map(fn tag -> String.capitalize(tag) end)
-    |> Enum.join()
-  end
-
-  defp with_fallback(f, fallback \\ []) do
-    try do
-      f.()
-    rescue
-      error ->
-        Logger.error("#{__MODULE__} search error: #{inspect(error)}")
-        fallback
-    end
-  end
-end
similarity index 92%
rename from lib/pleroma/activity/search.ex
rename to lib/pleroma/search/database_search.ex
index 09671f62103678237efcf4f38cc168fcf9609765..3735a5fab427f87652d6d203da55ee866facc399 100644 (file)
@@ -2,7 +2,7 @@
 # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
 # SPDX-License-Identifier: AGPL-3.0-only
 
-defmodule Pleroma.Activity.Search do
+defmodule Pleroma.Search.DatabaseSearch do
   alias Pleroma.Activity
   alias Pleroma.Object.Fetcher
   alias Pleroma.Pagination
@@ -13,6 +13,8 @@ defmodule Pleroma.Activity.Search do
 
   import Ecto.Query
 
+  @behaviour Pleroma.Search.SearchBackend
+
   def search(user, search_query, options \\ []) do
     index_type = if Pleroma.Config.get([:database, :rum_enabled]), do: :rum, else: :gin
     limit = Enum.min([Keyword.get(options, :limit), 40])
@@ -45,6 +47,12 @@ defmodule Pleroma.Activity.Search do
     end
   end
 
+  @impl true
+  def add_to_index(_activity), do: nil
+
+  @impl true
+  def remove_from_index(_object), do: nil
+
   def maybe_restrict_author(query, %User{} = author) do
     Activity.Queries.by_author(query, author)
   end
@@ -57,7 +65,7 @@ defmodule Pleroma.Activity.Search do
 
   def maybe_restrict_blocked(query, _), do: query
 
-  defp restrict_public(q) do
+  def restrict_public(q) do
     from([a, o] in q,
       where: fragment("?->>'type' = 'Create'", a.data),
       where: ^Pleroma.Constants.as_public() in a.recipients
@@ -124,7 +132,7 @@ defmodule Pleroma.Activity.Search do
     )
   end
 
-  defp maybe_restrict_local(q, user) do
+  def maybe_restrict_local(q, user) do
     limit = Pleroma.Config.get([:instance, :limit_to_local_content], :unauthenticated)
 
     case {limit, user} do
@@ -137,7 +145,7 @@ defmodule Pleroma.Activity.Search do
 
   defp restrict_local(q), do: where(q, local: true)
 
-  defp maybe_fetch(activities, user, search_query) do
+  def maybe_fetch(activities, user, search_query) do
     with true <- Regex.match?(~r/https?:/, search_query),
          {:ok, object} <- Fetcher.fetch_object_from_id(search_query),
          %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
index 76d2c32771764a31b77809bb57c205aace355410..7c7ca82c8825e19a39b5807ec51afe9cb3d5b5e5 100644 (file)
@@ -3,24 +3,22 @@
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Pleroma.Search.Elasticsearch do
-  @behaviour Pleroma.Search
+  @behaviour Pleroma.Search.SearchBackend
 
   alias Pleroma.Activity
   alias Pleroma.Object.Fetcher
-  alias Pleroma.Web.MastodonAPI.StatusView
-  alias Pleroma.Web.MastodonAPI.AccountView
   alias Pleroma.Web.ActivityPub.Visibility
   alias Pleroma.Search.Elasticsearch.Parsers
-  alias Pleroma.Web.Endpoint
 
-  def es_query(:activity, query) do
+  def es_query(:activity, query, offset, limit) do
     must = Parsers.Activity.parse(query)
 
     if must == [] do
       :skip
     else
       %{
-        size: 50,
+        size: limit,
+        from: offset,
         terminate_after: 50,
         timeout: "5s",
         sort: [
@@ -36,50 +34,6 @@ defmodule Pleroma.Search.Elasticsearch do
     end
   end
 
-  def es_query(:user, query) do
-    must = Parsers.User.parse(query)
-
-    if must == [] do
-      :skip
-    else
-      %{
-        size: 50,
-        terminate_after: 50,
-        timeout: "5s",
-        sort: [
-          "_score"
-        ],
-        query: %{
-          bool: %{
-            must: must
-          }
-        }
-      }
-    end
-  end
-
-  def es_query(:hashtag, query) do
-    must = Parsers.Hashtag.parse(query)
-
-    if must == [] do
-      :skip
-    else
-      %{
-        size: 50,
-        terminate_after: 50,
-        timeout: "5s",
-        sort: [
-          "_score"
-        ],
-        query: %{
-          bool: %{
-            must: Parsers.Hashtag.parse(query)
-          }
-        }
-      }
-    end
-  end
-
   defp maybe_fetch(:activity, search_query) do
     with true <- Regex.match?(~r/https?:/, search_query),
          {:ok, object} <- Fetcher.fetch_object_from_id(search_query),
@@ -90,8 +44,10 @@ defmodule Pleroma.Search.Elasticsearch do
     end
   end
 
-  @impl Pleroma.Search
-  def search(%{assigns: %{user: user}} = _conn, %{q: query} = _params, _options) do
+  def search(user, query, options) do
+    limit = Enum.min([Keyword.get(options, :limit), 40])
+    offset = Keyword.get(options, :offset, 0)
+
     parsed_query =
       query
       |> String.trim()
@@ -104,30 +60,13 @@ defmodule Pleroma.Search.Elasticsearch do
 
     activity_task =
       Task.async(fn ->
-        q = es_query(:activity, parsed_query)
+        q = es_query(:activity, parsed_query, offset, limit)
 
-        Pleroma.Elasticsearch.search(:activities, q)
+        Pleroma.Search.Elasticsearch.Store.search(:activities, q)
         |> Enum.filter(fn x -> Visibility.visible_for_user?(x, user) end)
       end)
 
-    user_task =
-      Task.async(fn ->
-        q = es_query(:user, parsed_query)
-
-        Pleroma.Elasticsearch.search(:users, q)
-        |> Enum.filter(fn x -> Pleroma.User.visible_for(x, user) == :visible end)
-      end)
-
-    hashtag_task =
-      Task.async(fn ->
-        q = es_query(:hashtag, parsed_query)
-
-        Pleroma.Elasticsearch.search(:hashtags, q)
-      end)
-
     activity_results = Task.await(activity_task)
-    user_results = Task.await(user_task)
-    hashtag_results = Task.await(hashtag_task)
     direct_activity = Task.await(activity_fetch_task)
 
     activity_results =
@@ -137,25 +76,16 @@ defmodule Pleroma.Search.Elasticsearch do
         [direct_activity | activity_results]
       end
 
-    %{
-      "accounts" =>
-        AccountView.render("index.json",
-          users: user_results,
-          for: user
-        ),
-      "hashtags" =>
-        Enum.map(hashtag_results, fn x ->
-          %{
-            url: Endpoint.url() <> "/tag/" <> x,
-            name: x
-          }
-        end),
-      "statuses" =>
-        StatusView.render("index.json",
-          activities: activity_results,
-          for: user,
-          as: :activity
-        )
-    }
+    activity_results
+  end
+
+  @impl true
+  def add_to_index(activity) do
+    Elasticsearch.put_document(Pleroma.Search.Elasticsearch.Cluster, activity, "activities")
+  end
+
+  @impl true
+  def remove_from_index(object) do
+    Elasticsearch.delete_document(Pleroma.Search.Elasticsearch.Cluster, object, "activities")
   end
 end
diff --git a/lib/pleroma/search/elasticsearch/cluster.ex b/lib/pleroma/search/elasticsearch/cluster.ex
new file mode 100644 (file)
index 0000000..4f76c4e
--- /dev/null
@@ -0,0 +1,4 @@
+defmodule Pleroma.Search.Elasticsearch.Cluster do
+  @moduledoc false
+  use Elasticsearch.Cluster, otp_app: :pleroma
+end
diff --git a/lib/pleroma/search/elasticsearch/document_mappings/activity.ex b/lib/pleroma/search/elasticsearch/document_mappings/activity.ex
new file mode 100644 (file)
index 0000000..3a84e99
--- /dev/null
@@ -0,0 +1,61 @@
+# Akkoma: A lightweight social networking server
+# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defimpl Elasticsearch.Document, for: Pleroma.Activity do
+  alias Pleroma.Object
+  require Pleroma.Constants
+
+  def id(obj), do: obj.id
+  def routing(_), do: false
+
+  def object_to_search_data(object) do
+    # Only index public or unlisted Notes
+    if not is_nil(object) and object.data["type"] == "Note" and
+         not is_nil(object.data["content"]) and
+         (Pleroma.Constants.as_public() in object.data["to"] or
+            Pleroma.Constants.as_public() in object.data["cc"]) and
+         String.length(object.data["content"]) > 1 do
+      data = object.data
+
+      content_str =
+        case data["content"] do
+          [nil | rest] -> to_string(rest)
+          str -> str
+        end
+
+      content =
+        with {:ok, scrubbed} <- FastSanitize.strip_tags(content_str),
+             trimmed <- String.trim(scrubbed) do
+          trimmed
+        end
+
+      if String.length(content) > 1 do
+        {:ok, published, _} = DateTime.from_iso8601(data["published"])
+
+        %{
+          _timestamp: published,
+          content: content,
+          instance: URI.parse(object.data["actor"]).host,
+          hashtags: Object.hashtags(object),
+          user: Pleroma.User.get_cached_by_ap_id(object.data["actor"]).nickname
+        }
+      else
+        %{}
+      end
+    else
+      %{}
+    end
+  end
+
+  def encode(activity) do
+    object = Pleroma.Object.normalize(activity)
+    object_to_search_data(object)
+  end
+end
+
+defimpl Elasticsearch.Document, for: Pleroma.Object do
+  def id(obj), do: obj.id
+  def routing(_), do: false
+  def encode(_), do: nil
+end
diff --git a/lib/pleroma/search/elasticsearch/hashtag_parser.ex b/lib/pleroma/search/elasticsearch/hashtag_parser.ex
deleted file mode 100644 (file)
index 911dc65..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-# Akkoma: A lightweight social networking server
-# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Search.Elasticsearch.Parsers.Hashtag do
-  defp to_es(term) when is_binary(term) do
-    %{
-      term: %{
-        hashtag: %{
-          value: String.downcase(term)
-        }
-      }
-    }
-  end
-
-  defp to_es({:quoted, term}), do: to_es(term)
-
-  defp to_es({:filter, ["hashtag", query]}) do
-    %{
-      term: %{
-        hashtag: %{
-          value: String.downcase(query)
-        }
-      }
-    }
-  end
-
-  defp to_es({:filter, _}), do: nil
-
-  def parse(q) do
-    Enum.map(q, &to_es/1)
-    |> Enum.filter(fn x -> x != nil end)
-  end
-end
diff --git a/lib/pleroma/search/elasticsearch/store.ex b/lib/pleroma/search/elasticsearch/store.ex
new file mode 100644 (file)
index 0000000..895b76d
--- /dev/null
@@ -0,0 +1,52 @@
+# Akkoma: A lightweight social networking server
+# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Search.Elasticsearch.Store do
+  @behaviour Elasticsearch.Store
+  alias Pleroma.Search.Elasticsearch.Cluster
+  require Logger
+
+  alias Pleroma.Repo
+
+  @impl true
+  def stream(schema) do
+    Repo.stream(schema)
+  end
+
+  @impl true
+  def transaction(fun) do
+    {:ok, result} = Repo.transaction(fun, timeout: :infinity)
+    result
+  end
+
+  def search(_, _, _, :skip), do: []
+
+  def search(:raw, index, q) do
+    with {:ok, raw_results} <- Elasticsearch.post(Cluster, "/#{index}/_search", q) do
+      results =
+        raw_results
+        |> Map.get("hits", %{})
+        |> Map.get("hits", [])
+
+      {:ok, results}
+    else
+      {:error, e} ->
+        Logger.error(e)
+        {:error, e}
+    end
+  end
+
+  def search(:activities, q) do
+    with {:ok, results} <- search(:raw, "activities", q) do
+      results
+      |> Enum.map(fn result -> result["_id"] end)
+      |> Pleroma.Activity.all_by_ids_with_object()
+      |> Enum.sort(&(&1.inserted_at >= &2.inserted_at))
+    else
+      e ->
+        Logger.error(e)
+        []
+    end
+  end
+end
diff --git a/lib/pleroma/search/elasticsearch/user_paser.ex b/lib/pleroma/search/elasticsearch/user_paser.ex
deleted file mode 100644 (file)
index 4176c61..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-# Akkoma: A lightweight social networking server
-# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Search.Elasticsearch.Parsers.User do
-  defp to_es(term) when is_binary(term) do
-    %{
-      bool: %{
-        minimum_should_match: 1,
-        should: [
-          %{
-            match: %{
-              bio: %{
-                query: term,
-                operator: "AND"
-              }
-            }
-          },
-          %{
-            term: %{
-              nickname: %{
-                value: term
-              }
-            }
-          },
-          %{
-            match: %{
-              display_name: %{
-                query: term,
-                operator: "AND"
-              }
-            }
-          }
-        ]
-      }
-    }
-  end
-
-  defp to_es({:quoted, term}), do: to_es(term)
-
-  defp to_es({:filter, ["user", query]}) do
-    %{
-      term: %{
-        nickname: %{
-          value: query
-        }
-      }
-    }
-  end
-
-  defp to_es({:filter, _}), do: nil
-
-  def parse(q) do
-    Enum.map(q, &to_es/1)
-    |> Enum.filter(fn x -> x != nil end)
-  end
-end
diff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex
new file mode 100644 (file)
index 0000000..3db65f2
--- /dev/null
@@ -0,0 +1,169 @@
+defmodule Pleroma.Search.Meilisearch do
+  require Logger
+  require Pleroma.Constants
+
+  alias Pleroma.Activity
+
+  import Pleroma.Search.DatabaseSearch
+  import Ecto.Query
+
+  @behaviour Pleroma.Search.SearchBackend
+
+  defp meili_headers do
+    private_key = Pleroma.Config.get([Pleroma.Search.Meilisearch, :private_key])
+
+    [{"Content-Type", "application/json"}] ++
+      if is_nil(private_key), do: [], else: [{"Authorization", "Bearer #{private_key}"}]
+  end
+
+  def meili_get(path) do
+    endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url])
+
+    result =
+      Pleroma.HTTP.get(
+        Path.join(endpoint, path),
+        meili_headers()
+      )
+
+    with {:ok, res} <- result do
+      {:ok, Jason.decode!(res.body)}
+    end
+  end
+
+  def meili_post(path, params) do
+    endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url])
+
+    result =
+      Pleroma.HTTP.post(
+        Path.join(endpoint, path),
+        Jason.encode!(params),
+        meili_headers()
+      )
+
+    with {:ok, res} <- result do
+      {:ok, Jason.decode!(res.body)}
+    end
+  end
+
+  def meili_put(path, params) do
+    endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url])
+
+    result =
+      Pleroma.HTTP.request(
+        :put,
+        Path.join(endpoint, path),
+        Jason.encode!(params),
+        meili_headers(),
+        []
+      )
+
+    with {:ok, res} <- result do
+      {:ok, Jason.decode!(res.body)}
+    end
+  end
+
+  def meili_delete!(path) do
+    endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url])
+
+    {:ok, _} =
+      Pleroma.HTTP.request(
+        :delete,
+        Path.join(endpoint, path),
+        "",
+        meili_headers(),
+        []
+      )
+  end
+
+  def search(user, query, options \\ []) do
+    limit = Enum.min([Keyword.get(options, :limit), 40])
+    offset = Keyword.get(options, :offset, 0)
+    author = Keyword.get(options, :author)
+
+    res =
+      meili_post(
+        "/indexes/objects/search",
+        %{q: query, offset: offset, limit: limit}
+      )
+
+    with {:ok, result} <- res do
+      hits = result["hits"] |> Enum.map(& &1["ap"])
+
+      try do
+        hits
+        |> Activity.create_by_object_ap_id()
+        |> Activity.with_preloaded_object()
+        |> Activity.with_preloaded_object()
+        |> Activity.restrict_deactivated_users()
+        |> maybe_restrict_local(user)
+        |> maybe_restrict_author(author)
+        |> maybe_restrict_blocked(user)
+        |> maybe_fetch(user, query)
+        |> order_by([object: obj], desc: obj.data["published"])
+        |> Pleroma.Repo.all()
+      rescue
+        _ -> maybe_fetch([], user, query)
+      end
+    end
+  end
+
+  def object_to_search_data(object) do
+    # Only index public or unlisted Notes
+    if not is_nil(object) and object.data["type"] == "Note" and
+         not is_nil(object.data["content"]) and
+         (Pleroma.Constants.as_public() in object.data["to"] or
+            Pleroma.Constants.as_public() in object.data["cc"]) and
+         String.length(object.data["content"]) > 1 do
+      data = object.data
+
+      content_str =
+        case data["content"] do
+          [nil | rest] -> to_string(rest)
+          str -> str
+        end
+
+      content =
+        with {:ok, scrubbed} <- FastSanitize.strip_tags(content_str),
+             trimmed <- String.trim(scrubbed) do
+          trimmed
+        end
+
+      if String.length(content) > 1 do
+        {:ok, published, _} = DateTime.from_iso8601(data["published"])
+
+        %{
+          id: object.id,
+          content: content,
+          ap: data["id"],
+          published: published |> DateTime.to_unix()
+        }
+      end
+    end
+  end
+
+  @impl true
+  def add_to_index(activity) do
+    maybe_search_data = object_to_search_data(activity.object)
+
+    if activity.data["type"] == "Create" and maybe_search_data do
+      result =
+        meili_put(
+          "/indexes/objects/documents",
+          [maybe_search_data]
+        )
+
+      with {:ok, res} <- result,
+           true <- Map.has_key?(res, "uid") do
+        # Do nothing
+      else
+        _ ->
+          Logger.error("Failed to add activity #{activity.id} to index: #{inspect(result)}")
+      end
+    end
+  end
+
+  @impl true
+  def remove_from_index(object) do
+    meili_delete!("/indexes/objects/documents/#{object.id}")
+  end
+end
diff --git a/lib/pleroma/search/search_backend.ex b/lib/pleroma/search/search_backend.ex
new file mode 100644 (file)
index 0000000..ed6bfd3
--- /dev/null
@@ -0,0 +1,17 @@
+defmodule Pleroma.Search.SearchBackend do
+  @doc """
+  Add the object associated with the activity to the search index.
+
+  The whole activity is passed, to allow filtering on things such as scope.
+  """
+  @callback add_to_index(activity :: Pleroma.Activity.t()) :: nil
+
+  @doc """
+  Remove the object from the index.
+
+  Just the object, as opposed to the whole activity, is passed, since the object
+  is what contains the actual content and there is no need for fitlering when removing
+  from index.
+  """
+  @callback remove_from_index(object :: Pleroma.Object.t()) :: nil
+end
index 35e245237ec5abf5004b890df5e273eddd1ac247..50f7fcf2a7af3a7c0c6cefe6831bbdacb70e6cfd 100644 (file)
@@ -12,8 +12,7 @@ defmodule Pleroma.Telemetry.Logger do
     [:pleroma, :connection_pool, :reclaim, :stop],
     [:pleroma, :connection_pool, :provision_failure],
     [:pleroma, :connection_pool, :client, :dead],
-    [:pleroma, :connection_pool, :client, :add],
-    [:pleroma, :repo, :query]
+    [:pleroma, :connection_pool, :client, :add]
   ]
   def attach do
     :telemetry.attach_many(
@@ -93,64 +92,4 @@ defmodule Pleroma.Telemetry.Logger do
   end
 
   def handle_event([:pleroma, :connection_pool, :client, :add], _, _, _), do: :ok
-
-  def handle_event(
-        [:pleroma, :repo, :query] = _name,
-        %{query_time: query_time} = measurements,
-        %{source: source} = metadata,
-        config
-      ) do
-    logging_config = Pleroma.Config.get([:telemetry, :slow_queries_logging], [])
-
-    if logging_config[:enabled] &&
-         logging_config[:min_duration] &&
-         query_time > logging_config[:min_duration] and
-         (is_nil(logging_config[:exclude_sources]) or
-            source not in logging_config[:exclude_sources]) do
-      log_slow_query(measurements, metadata, config)
-    else
-      :ok
-    end
-  end
-
-  defp log_slow_query(
-         %{query_time: query_time} = _measurements,
-         %{source: _source, query: query, params: query_params, repo: repo} = _metadata,
-         _config
-       ) do
-    sql_explain =
-      with {:ok, %{rows: explain_result_rows}} <-
-             repo.query("EXPLAIN " <> query, query_params, log: false) do
-        Enum.map_join(explain_result_rows, "\n", & &1)
-      end
-
-    {:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace)
-
-    pleroma_stacktrace =
-      Enum.filter(stacktrace, fn
-        {__MODULE__, _, _, _} ->
-          false
-
-        {mod, _, _, _} ->
-          mod
-          |> to_string()
-          |> String.starts_with?("Elixir.Pleroma.")
-      end)
-
-    Logger.warn(fn ->
-      """
-      Slow query!
-
-      Total time: #{round(query_time / 1_000)} ms
-
-      #{query}
-
-      #{inspect(query_params, limit: :infinity)}
-
-      #{sql_explain}
-
-      #{Exception.format_stacktrace(pleroma_stacktrace)}
-      """
-    end)
-  end
 end
index efe9ec5d6fe7f69d3856f7041ee6fc8ebb12027f..dc6c661eaf03486f79d99a1045f80b24e33b75d0 100644 (file)
@@ -151,6 +151,7 @@ defmodule Pleroma.User do
     field(:pinned_objects, :map, default: %{})
     field(:is_suggested, :boolean, default: false)
     field(:last_status_at, :naive_datetime)
+    field(:language, :string)
 
     embeds_one(
       :notification_settings,
@@ -734,7 +735,8 @@ defmodule Pleroma.User do
       :password_confirmation,
       :emoji,
       :accepts_chat_messages,
-      :registration_reason
+      :registration_reason,
+      :language
     ])
     |> validate_required([:name, :nickname, :password, :password_confirmation])
     |> validate_confirmation(:password)
@@ -1089,11 +1091,24 @@ defmodule Pleroma.User do
     |> update_and_set_cache()
   end
 
-  def update_and_set_cache(changeset) do
+  def update_and_set_cache(%{data: %Pleroma.User{} = user} = changeset) do
+    was_superuser_before_update = User.superuser?(user)
+
     with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
-      Pleroma.Elasticsearch.maybe_put_into_elasticsearch(user)
       set_cache(user)
     end
+    |> maybe_remove_report_notifications(was_superuser_before_update)
+  end
+
+  defp maybe_remove_report_notifications({:ok, %Pleroma.User{} = user} = result, true) do
+    if not User.superuser?(user),
+      do: user |> Notification.destroy_multiple_from_types(["pleroma:report"])
+
+    result
+  end
+
+  defp maybe_remove_report_notifications(result, _) do
+    result
   end
 
   def get_user_friends_ap_ids(user) do
index 7560969525f041506c629c488a700aa92cee0d92..e6548a8188a535d35cdfc29763bcce96cd63e94b 100644 (file)
@@ -140,6 +140,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
         Task.start(fn -> Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) end)
       end)
 
+      # Add local posts to search index
+      if local, do: Pleroma.Search.add_to_index(activity)
+
       {:ok, activity}
     else
       %Activity{} = activity ->
index 851e95d226e7f5b216c500e46a194e5333f621e1..627f52168d469429185ec1067fd662a8c2cc02e3 100644 (file)
@@ -24,7 +24,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do
   defp score_displayname("fedibot"), do: 1.0
   defp score_displayname(_), do: 0.0
 
-  defp determine_if_followbot(%User{nickname: nickname, name: displayname}) do
+  defp determine_if_followbot(%User{nickname: nickname, name: displayname, actor_type: actor_type}) do
     # nickname will be a binary string except when following a relay
     nick_score =
       if is_binary(nickname) do
@@ -45,19 +45,32 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do
         0.0
       end
 
-    nick_score + name_score
+    # actor_type "Service" is a Bot account
+    actor_type_score =
+      if actor_type == "Service" do
+        1.0
+      else
+        0.0
+      end
+
+    nick_score + name_score + actor_type_score
   end
 
   defp determine_if_followbot(_), do: 0.0
 
+  defp bot_allowed?(%{"object" => target}, bot_actor) do
+    %User{} = user = normalize_by_ap_id(target)
+
+    User.following?(user, bot_actor)
+  end
+
   @impl true
   def filter(%{"type" => "Follow", "actor" => actor_id} = message) do
     %User{} = actor = normalize_by_ap_id(actor_id)
 
     score = determine_if_followbot(actor)
 
-    # TODO: scan biography data for keywords and score it somehow.
-    if score < 0.8 do
+    if score < 0.8 || bot_allowed?(message, actor) do
       {:ok, message}
     else
       {:reject, "[AntiFollowbotPolicy] Scored #{actor_id} as #{score}"}
index 0dd415732fa7d7d6623d00be156bb22c83f2271a..61e95b49a13f9dda8784e82f2b0ab4f03717c0a3 100644 (file)
@@ -12,6 +12,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
 
   defp accept_host?(host), do: host in Config.get([:mrf_steal_emoji, :hosts], [])
 
+  defp shortcode_matches?(shortcode, pattern) when is_binary(pattern) do
+    shortcode == pattern
+  end
+
+  defp shortcode_matches?(shortcode, pattern) do
+    String.match?(shortcode, pattern)
+  end
+
   defp steal_emoji({shortcode, url}, emoji_dir_path) do
     url = Pleroma.Web.MediaProxy.url(url)
 
@@ -72,7 +80,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
           reject_emoji? =
             [:mrf_steal_emoji, :rejected_shortcodes]
             |> Config.get([])
-            |> Enum.find(false, fn regex -> String.match?(shortcode, regex) end)
+            |> Enum.find(false, fn pattern -> shortcode_matches?(shortcode, pattern) end)
 
           !reject_emoji?
         end)
@@ -122,8 +130,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
         %{
           key: :rejected_shortcodes,
           type: {:list, :string},
-          description: "Regex-list of shortcodes to reject",
-          suggestions: [""]
+          description: """
+            A list of patterns or matches to reject shortcodes with.
+
+            Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
+          """,
+          suggestions: ["foo", ~r/foo/]
         },
         %{
           key: :size_limit,
index 815995895e067879bf97f381bdef557d0ac20651..a9f395c5eaf397614863bc71c4b5c91b4d792fd0 100644 (file)
@@ -108,6 +108,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
     |> fix_replies()
     |> fix_source()
     |> fix_misskey_content()
+    |> Transmogrifier.fix_attachments()
     |> Transmogrifier.fix_emoji()
     |> Transmogrifier.fix_content_map()
   end
index 214647dbffe8326be6d8f286bde86e0750ed3534..d4e5072874a24a8063b22f10c8d5a69a73512564 100644 (file)
@@ -28,7 +28,6 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do
     case Repo.transaction(fn -> do_common_pipeline(object, meta) end, Utils.query_timeout()) do
       {:ok, {:ok, activity, meta}} ->
         side_effects().handle_after_transaction(meta)
-        side_effects().handle_after_transaction(activity)
         {:ok, activity, meta}
 
       {:ok, value} ->
index 9c2f89e72e290752360de257cf61def2040f869c..e2371b6939d4240e5f305d196afb25f9d657a929 100644 (file)
@@ -1,5 +1,5 @@
 # Pleroma: A lightweight social networking server
-# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Pleroma.Web.ActivityPub.SideEffects do
@@ -193,6 +193,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
   # - Increase replies count
   # - Set up ActivityExpiration
   # - Set up notifications
+  # - Index incoming posts for search (if needed)
   @impl true
   def handle(%{data: %{"type" => "Create"}} = activity, meta) do
     with {:ok, object, meta} <- handle_object_creation(meta[:object_data], activity, meta),
@@ -222,6 +223,8 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
         Task.start(fn -> Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) end)
       end)
 
+      Pleroma.Search.add_to_index(Map.put(activity, :object, object))
+
       meta =
         meta
         |> add_notifications(notifications)
@@ -269,6 +272,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
   def handle(%{data: %{"type" => "EmojiReact"}} = object, meta) do
     reacted_object = Object.get_by_ap_id(object.data["object"])
     Utils.add_emoji_reaction_to_object(object, reacted_object)
+
     Notification.create_notifications(object)
 
     {:ok, object, meta}
@@ -281,6 +285,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
   # - Reduce the user note count
   # - Reduce the reply count
   # - Stream out the activity
+  # - Removes posts from search index (if needed)
   @impl true
   def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, meta) do
     deleted_object =
@@ -320,6 +325,12 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
 
     if result == :ok do
       Notification.create_notifications(object)
+
+      # Only remove from index when deleting actual objects, not users or anything else
+      with %Pleroma.Object{} <- deleted_object do
+        Pleroma.Search.remove_from_index(deleted_object)
+      end
+
       {:ok, object, meta}
     else
       {:error, result}
@@ -537,24 +548,6 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
   end
 
   @impl true
-  def handle_after_transaction(%Pleroma.Activity{data: %{"type" => "Create"}} = activity) do
-    Pleroma.Elasticsearch.put_by_id(:activity, activity.id)
-  end
-
-  def handle_after_transaction(%Pleroma.Activity{
-        data: %{"type" => "Delete", "deleted_activity_id" => id}
-      }) do
-    Pleroma.Elasticsearch.delete_by_id(:activity, id)
-  end
-
-  def handle_after_transaction(%Pleroma.Activity{}) do
-    :ok
-  end
-
-  def handle_after_transaction(%Pleroma.Object{}) do
-    :ok
-  end
-
   def handle_after_transaction(meta) do
     meta
     |> send_notifications()
index a823051552386513702ce5f7023a82b2e08d90a5..eb012f57623a619753638081956e16d3507251c9 100644 (file)
@@ -1,5 +1,5 @@
 # Pleroma: A lightweight social networking server
-# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Pleroma.Web.ActivityPub.SideEffects.Handling do
index f5304d7d6fd4d79cdcadbc51b0df680618daad46..bdbae9b747cedc5a2e94a6a07fd4fe4292da4b1c 100644 (file)
@@ -507,6 +507,11 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
           type: :string,
           nullable: true,
           description: "Invite token required when the registrations aren't public"
+        },
+        language: %Schema{
+          type: :string,
+          nullable: true,
+          description: "User's preferred language for emails"
         }
       },
       example: %{
index 92afd5cb61e6533ebfc0359742b8eee21bc1ed14..856fa95b9c6dd23369b2554b2ad4736cd4124a3d 100644 (file)
@@ -396,13 +396,7 @@ defmodule Pleroma.Web.CommonAPI do
 
   def post(user, %{status: _} = data) do
     with {:ok, draft} <- ActivityDraft.create(user, data) do
-      activity = ActivityPub.create(draft.changes, draft.preview?)
-
-      unless draft.preview? do
-        Pleroma.Elasticsearch.maybe_put_into_elasticsearch(activity)
-      end
-
-      activity
+      ActivityPub.create(draft.changes, draft.preview?)
     end
   end
 
index c0fb35e01ac89cb326c9427e4d05f3b1a242c5bc..52771205ec5e0cb150ca62eb041f29dd4581cce0 100644 (file)
@@ -9,6 +9,7 @@ defmodule Pleroma.Web.Feed.FeedView do
   alias Pleroma.Formatter
   alias Pleroma.Object
   alias Pleroma.User
+  alias Pleroma.Web.Gettext
   alias Pleroma.Web.MediaProxy
 
   require Pleroma.Constants
index c0ca4d0e9e82c92091fe2124f6cc9d20c67edb92..7afcd38f03892cf390bd8b2ca6bb99ff3cc911c9 100644 (file)
@@ -25,4 +25,196 @@ defmodule Pleroma.Web.Gettext do
   See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
   """
   use Gettext, otp_app: :pleroma
+
+  def language_tag do
+    # Naive implementation: HTML lang attribute uses BCP 47, which
+    # uses - as a separator.
+    # https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang
+
+    Gettext.get_locale()
+    |> String.replace("_", "-", global: true)
+  end
+
+  def normalize_locale(locale) do
+    if is_binary(locale) do
+      String.replace(locale, "-", "_", global: true)
+    else
+      nil
+    end
+  end
+
+  def supports_locale?(locale) do
+    Pleroma.Web.Gettext
+    |> Gettext.known_locales()
+    |> Enum.member?(locale)
+  end
+
+  def variant?(locale), do: String.contains?(locale, "_")
+
+  def language_for_variant(locale) do
+    Enum.at(String.split(locale, "_"), 0)
+  end
+
+  def ensure_fallbacks(locales) do
+    locales
+    |> Enum.flat_map(fn locale ->
+      others =
+        other_supported_variants_of_locale(locale)
+        |> Enum.filter(fn l -> not Enum.member?(locales, l) end)
+
+      [locale] ++ others
+    end)
+  end
+
+  def other_supported_variants_of_locale(locale) do
+    cond do
+      supports_locale?(locale) ->
+        []
+
+      variant?(locale) ->
+        lang = language_for_variant(locale)
+        if supports_locale?(lang), do: [lang], else: []
+
+      true ->
+        Gettext.known_locales(Pleroma.Web.Gettext)
+        |> Enum.filter(fn l -> String.starts_with?(l, locale <> "_") end)
+    end
+  end
+
+  def get_locales do
+    Process.get({Pleroma.Web.Gettext, :locales}, [])
+  end
+
+  def is_locale_list(locales) do
+    Enum.all?(locales, &is_binary/1)
+  end
+
+  def put_locales(locales) do
+    if is_locale_list(locales) do
+      Process.put({Pleroma.Web.Gettext, :locales}, Enum.uniq(locales))
+      Gettext.put_locale(Enum.at(locales, 0, Gettext.get_locale()))
+      :ok
+    else
+      {:error, :not_locale_list}
+    end
+  end
+
+  def locale_or_default(locale) do
+    if supports_locale?(locale) do
+      locale
+    else
+      Gettext.get_locale()
+    end
+  end
+
+  def with_locales_func(locales, fun) do
+    prev_locales = Process.get({Pleroma.Web.Gettext, :locales})
+    put_locales(locales)
+
+    try do
+      fun.()
+    after
+      if prev_locales do
+        put_locales(prev_locales)
+      else
+        Process.delete({Pleroma.Web.Gettext, :locales})
+        Process.delete(Gettext)
+      end
+    end
+  end
+
+  defmacro with_locales(locales, do: fun) do
+    quote do
+      Pleroma.Web.Gettext.with_locales_func(unquote(locales), fn ->
+        unquote(fun)
+      end)
+    end
+  end
+
+  def to_locale_list(locale) when is_binary(locale) do
+    locale
+    |> String.split(",")
+    |> Enum.filter(&supports_locale?/1)
+  end
+
+  def to_locale_list(_), do: []
+
+  defmacro with_locale_or_default(locale, do: fun) do
+    quote do
+      Pleroma.Web.Gettext.with_locales_func(
+        Pleroma.Web.Gettext.to_locale_list(unquote(locale))
+        |> Enum.concat(Pleroma.Web.Gettext.get_locales()),
+        fn ->
+          unquote(fun)
+        end
+      )
+    end
+  end
+
+  defp next_locale(locale, list) do
+    index = Enum.find_index(list, fn item -> item == locale end)
+
+    if not is_nil(index) do
+      Enum.at(list, index + 1)
+    else
+      nil
+    end
+  end
+
+  # We do not yet have a proper English translation. The "English"
+  # version is currently but the fallback msgid. However, this
+  # will not work if the user puts English as the first language,
+  # and at the same time specifies other languages, as gettext will
+  # think the English translation is missing, and call
+  # handle_missing_translation functions. This may result in
+  # text in other languages being shown even if English is preferred
+  # by the user.
+  #
+  # To prevent this, we do not allow fallbacking when the current
+  # locale missing a translation is English.
+  defp should_fallback?(locale) do
+    locale != "en"
+  end
+
+  def handle_missing_translation(locale, domain, msgctxt, msgid, bindings) do
+    next = next_locale(locale, get_locales())
+
+    if is_nil(next) or not should_fallback?(locale) do
+      super(locale, domain, msgctxt, msgid, bindings)
+    else
+      {:ok,
+       Gettext.with_locale(next, fn ->
+         Gettext.dpgettext(Pleroma.Web.Gettext, domain, msgctxt, msgid, bindings)
+       end)}
+    end
+  end
+
+  def handle_missing_plural_translation(
+        locale,
+        domain,
+        msgctxt,
+        msgid,
+        msgid_plural,
+        n,
+        bindings
+      ) do
+    next = next_locale(locale, get_locales())
+
+    if is_nil(next) or not should_fallback?(locale) do
+      super(locale, domain, msgctxt, msgid, msgid_plural, n, bindings)
+    else
+      {:ok,
+       Gettext.with_locale(next, fn ->
+         Gettext.dpngettext(
+           Pleroma.Web.Gettext,
+           domain,
+           msgctxt,
+           msgid,
+           msgid_plural,
+           n,
+           bindings
+         )
+       end)}
+    end
+  end
 end
index a307807a93148c1f8199ad314de9d8e3230a3c67..83cebbb963eda2f44e5f5da0cbccbc65059cce9b 100644 (file)
@@ -217,6 +217,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
       |> Maps.put_if_present(:is_locked, params[:locked])
       # Note: param name is indeed :discoverable (not an error)
       |> Maps.put_if_present(:is_discoverable, params[:discoverable])
+      |> Maps.put_if_present(:language, Pleroma.Web.Gettext.normalize_locale(params[:language]))
 
     # What happens here:
     #
index 86ad388fd9d7aeba6da71b8fb91ff610d89aa0ed..e4acba2264b1b2086e6720dcdc04ecdba1ac3456 100644 (file)
@@ -1,13 +1,16 @@
 # Pleroma: A lightweight social networking server
-# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Pleroma.Web.MastodonAPI.SearchController do
   use Pleroma.Web, :controller
 
+  alias Pleroma.Repo
   alias Pleroma.User
   alias Pleroma.Web.ControllerHelper
+  alias Pleroma.Web.Endpoint
   alias Pleroma.Web.MastodonAPI.AccountView
+  alias Pleroma.Web.MastodonAPI.StatusView
   alias Pleroma.Web.Plugs.OAuthScopesPlug
   alias Pleroma.Web.Plugs.RateLimiter
 
@@ -41,13 +44,34 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
   def search2(conn, params), do: do_search(:v2, conn, params)
   def search(conn, params), do: do_search(:v1, conn, params)
 
-  defp do_search(version, %{assigns: %{user: user}} = conn, params) do
-    options =
-      search_options(params, user)
-      |> Keyword.put(:version, version)
-
-    search_provider = Pleroma.Config.get([:search, :provider])
-    json(conn, search_provider.search(conn, params, options))
+  defp do_search(version, %{assigns: %{user: user}} = conn, %{q: query} = params) do
+    query = String.trim(query)
+    options = search_options(params, user)
+    timeout = Keyword.get(Repo.config(), :timeout, 15_000)
+    default_values = %{"statuses" => [], "accounts" => [], "hashtags" => []}
+
+    result =
+      default_values
+      |> Enum.map(fn {resource, default_value} ->
+        if params[:type] in [nil, resource] do
+          {resource, fn -> resource_search(version, resource, query, options) end}
+        else
+          {resource, fn -> default_value end}
+        end
+      end)
+      |> Task.async_stream(fn {resource, f} -> {resource, with_fallback(f)} end,
+        timeout: timeout,
+        on_timeout: :kill_task
+      )
+      |> Enum.reduce(default_values, fn
+        {:ok, {resource, result}}, acc ->
+          Map.put(acc, resource, result)
+
+        _error, acc ->
+          acc
+      end)
+
+    json(conn, result)
   end
 
   defp search_options(params, user) do
@@ -64,6 +88,104 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
     |> Enum.filter(&elem(&1, 1))
   end
 
+  defp resource_search(_, "accounts", query, options) do
+    accounts = with_fallback(fn -> User.search(query, options) end)
+
+    AccountView.render("index.json",
+      users: accounts,
+      for: options[:for_user],
+      embed_relationships: options[:embed_relationships]
+    )
+  end
+
+  defp resource_search(_, "statuses", query, options) do
+    statuses = with_fallback(fn -> Pleroma.Search.search(query, options) end)
+
+    StatusView.render("index.json",
+      activities: statuses,
+      for: options[:for_user],
+      as: :activity
+    )
+  end
+
+  defp resource_search(:v2, "hashtags", query, options) do
+    tags_path = Endpoint.url() <> "/tag/"
+
+    query
+    |> prepare_tags(options)
+    |> Enum.map(fn tag ->
+      %{name: tag, url: tags_path <> tag}
+    end)
+  end
+
+  defp resource_search(:v1, "hashtags", query, options) do
+    prepare_tags(query, options)
+  end
+
+  defp prepare_tags(query, options) do
+    tags =
+      query
+      |> preprocess_uri_query()
+      |> String.split(~r/[^#\w]+/u, trim: true)
+      |> Enum.uniq_by(&String.downcase/1)
+
+    explicit_tags = Enum.filter(tags, fn tag -> String.starts_with?(tag, "#") end)
+
+    tags =
+      if Enum.any?(explicit_tags) do
+        explicit_tags
+      else
+        tags
+      end
+
+    tags = Enum.map(tags, fn tag -> String.trim_leading(tag, "#") end)
+
+    tags =
+      if Enum.empty?(explicit_tags) && !options[:skip_joined_tag] do
+        add_joined_tag(tags)
+      else
+        tags
+      end
+
+    Pleroma.Pagination.paginate(tags, options)
+  end
+
+  defp add_joined_tag(tags) do
+    tags
+    |> Kernel.++([joined_tag(tags)])
+    |> Enum.uniq_by(&String.downcase/1)
+  end
+
+  # If `query` is a URI, returns last component of its path, otherwise returns `query`
+  defp preprocess_uri_query(query) do
+    if query =~ ~r/https?:\/\// do
+      query
+      |> String.trim_trailing("/")
+      |> URI.parse()
+      |> Map.get(:path)
+      |> String.split("/")
+      |> Enum.at(-1)
+    else
+      query
+    end
+  end
+
+  defp joined_tag(tags) do
+    tags
+    |> Enum.map(fn tag -> String.capitalize(tag) end)
+    |> Enum.join()
+  end
+
+  defp with_fallback(f, fallback \\ []) do
+    try do
+      f.()
+    rescue
+      error ->
+        Logger.error("#{__MODULE__} search error: #{inspect(error)}")
+        fallback
+    end
+  end
+
   defp get_author(%{account_id: account_id}) when is_binary(account_id),
     do: User.get_cached_by_id(account_id)
 
index 2eff4d9d08c04c9cd9ac8da5aaf2944ed8f75eb9..60f4c44d7c705ef7079fbf17439ab61a79bc6367 100644 (file)
@@ -384,11 +384,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
   def context(%{assigns: %{user: user}} = conn, %{id: id}) do
     with %Activity{} = activity <- Activity.get_by_id(id) do
       activities =
-        ActivityPub.fetch_activities_for_context(activity.data["context"], %{
+        activity.data["context"]
+        |> ActivityPub.fetch_activities_for_context(%{
           blocking_user: user,
           user: user,
           exclude_id: activity.id
         })
+        |> Enum.filter(fn activity -> Visibility.visible_for_user?(activity, user) end)
 
       render(conn, "context.json", activity: activity, activities: activities, user: user)
     end
index 3d473f29c489bffd259beeddc93304321c520496..952c90efefab598a778683c5fdf75c8123e0ed04 100644 (file)
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.OAuth.MFAView do
   use Pleroma.Web, :view
   import Phoenix.HTML.Form
   alias Pleroma.MFA
+  alias Pleroma.Web.Gettext
 
   def render("mfa_response.json", %{token: token, user: user}) do
     %{
index 1419c96a2f905bbecffeb70702b5bc0bf711ed08..57a315705f7fb9a1485485c16eba88dea2513600 100644 (file)
@@ -5,6 +5,8 @@
 defmodule Pleroma.Web.OAuth.OAuthView do
   use Pleroma.Web, :view
   import Phoenix.HTML.Form
+  import Phoenix.HTML
+  alias Pleroma.Web.Gettext
 
   alias Pleroma.Web.OAuth.Token.Utils
 
index d77191cffb05b0247e661e9d994e00cd8a83d04e..e78917199bf9e3695c579a4dd28ccae75ff83f28 100644 (file)
@@ -6,18 +6,56 @@
 defmodule Pleroma.Web.Plugs.SetLocalePlug do
   import Plug.Conn, only: [get_req_header: 2, assign: 3]
 
+  def frontend_language_cookie_name, do: "userLanguage"
+
   def init(_), do: nil
 
   def call(conn, _) do
-    locale = get_locale_from_header(conn) || Gettext.get_locale()
-    Gettext.put_locale(locale)
-    assign(conn, :locale, locale)
+    locales = get_locales_from_header(conn)
+    first_locale = Enum.at(locales, 0, Gettext.get_locale())
+
+    Pleroma.Web.Gettext.put_locales(locales)
+
+    conn
+    |> assign(:locale, first_locale)
+    |> assign(:locales, locales)
   end
 
-  defp get_locale_from_header(conn) do
+  defp get_locales_from_header(conn) do
     conn
-    |> extract_accept_language()
-    |> Enum.find(&supported_locale?/1)
+    |> extract_preferred_language()
+    |> normalize_language_codes()
+    |> all_supported()
+    |> Enum.uniq()
+  end
+
+  defp all_supported(locales) do
+    locales
+    |> Pleroma.Web.Gettext.ensure_fallbacks()
+    |> Enum.filter(&supported_locale?/1)
+  end
+
+  defp normalize_language_codes(codes) do
+    codes
+    |> Enum.map(fn code -> Pleroma.Web.Gettext.normalize_locale(code) end)
+  end
+
+  defp extract_preferred_language(conn) do
+    extract_frontend_language(conn) ++ extract_accept_language(conn)
+  end
+
+  defp extract_frontend_language(conn) do
+    %{req_cookies: cookies} =
+      conn
+      |> Plug.Conn.fetch_cookies()
+
+    case cookies[frontend_language_cookie_name()] do
+      nil ->
+        []
+
+      fe_lang ->
+        String.split(fe_lang, ",")
+    end
   end
 
   defp extract_accept_language(conn) do
@@ -29,7 +67,6 @@ defmodule Pleroma.Web.Plugs.SetLocalePlug do
         |> Enum.sort(&(&1.quality > &2.quality))
         |> Enum.map(& &1.tag)
         |> Enum.reject(&is_nil/1)
-        |> ensure_language_fallbacks()
 
       _ ->
         []
@@ -37,9 +74,7 @@ defmodule Pleroma.Web.Plugs.SetLocalePlug do
   end
 
   defp supported_locale?(locale) do
-    Pleroma.Web.Gettext
-    |> Gettext.known_locales()
-    |> Enum.member?(locale)
+    Pleroma.Web.Gettext.supports_locale?(locale)
   end
 
   defp parse_language_option(string) do
@@ -53,11 +88,4 @@ defmodule Pleroma.Web.Plugs.SetLocalePlug do
 
     %{tag: captures["tag"], quality: quality}
   end
-
-  defp ensure_language_fallbacks(tags) do
-    Enum.flat_map(tags, fn tag ->
-      [language | _] = String.split(tag, "-")
-      if Enum.member?(tags, language), do: [tag], else: [tag, language]
-    end)
-  end
 end
index 60eceff221d665ea37cdb3c78f0db8fa304331b3..1efc76e1ad880076b17807163a223dedab802624 100644 (file)
                                                                                                <div
                                                                                                        style="font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height: 14px; color: <%= @styling.header_color %>;">
                                                                                                        <p style="line-height: 36px; text-align: center; margin: 0;"><span
-                                                                                                                       style="font-size: 30px; color: <%= @styling.header_color %>;">Hey <%= @user.nickname %>, here is what you've missed!</span></p>
+                                                                                                                       style="font-size: 30px; color: <%= @styling.header_color %>;"><%= Gettext.dpgettext("static_pages", "digest email header line", "Hey %{nickname}, here is what you've missed!", nickname: @user.nickname) %></span></p>
                                                                                                </div>
                                                                                        </div>
                                                                                        <!--[if mso]></td></tr></table><![endif]-->
                                                                                                <div
                                                                                                        style="font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size: 12px; line-height: 14px; color: <%= @styling.text_color %>;">
                                                                                                        <p style="font-size: 12px; line-height: 24px; text-align: center; margin: 0;"><span
-                                                                                                                       style="font-size: 20px;"><%= length(@followers) %> New Followers</span><span
+                                                                                                                       style="font-size: 20px;"><%= Gettext.dpngettext("static_pages", "new followers count header", "%{count} New Follower", "%{count} New Followers", length(@followers), count: length(@followers)) %></span><span
                                                                                                                        style="font-size: 20px; line-height: 24px;"></span></p>
                                                                                                </div>
                                                                                        </div>
                                                                                                style="color:<%= @styling.text_color %>;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
                                                                                                <p
                                                                                                        style="font-size: 12px; line-height: 16px; text-align: center; color: <%= @styling.text_color %>; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin: 0;">
-                                                                                                       <span style="font-size: 14px;">You have received this email because you have signed up to receive digest emails from <b><%= @instance %></b> Pleroma instance.</span></p>
+                                                                                                       <span style="font-size: 14px;"><%= raw Gettext.dpgettext("static_pages", "digest email sending reason", "You have received this email because you have signed up to receive digest emails from <b>%{instance}</b> Pleroma instance.", instance: safe_to_string(html_escape(@instance))) %></span></p>
                                                                                                <p
                                                                                                        style="font-size: 12px; line-height: 14px; text-align: center; color: <%= @styling.text_color %>; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin: 0;">
                                                                                                         </p>
                                                                                                <p
                                                                                                        style="font-size: 12px; line-height: 16px; text-align: center; color: <%= @styling.text_color %>; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin: 0;">
-                                                                                                       <span style="font-size: 14px;">The email address you are subscribed as is <a href="mailto:<%= @user.email %>" style="color: <%= @styling.link_color %>;text-decoration: none;"><%= @user.email %></a>. </span></p>
+                                                                                                       <span style="font-size: 14px;"><%= raw Gettext.dpgettext("static_pages", "digest email receiver address", "The email address you are subscribed as is <a href='mailto:%{@user.email}' style='color: %{color};text-decoration: none;'>%{email}</a>. ", color: safe_to_string(html_escape(@styling.link_color)), email: safe_to_string(html_escape(@user.email))) %></span></p>
                                                                                                <p
                                                                                                        style="font-size: 12px; line-height: 16px; text-align: center; color: <%= @styling.text_color %>; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin: 0;">
-                                                                                                       <span style="font-size: 14px;">To unsubscribe, please go <%= link "here", style: "color: #{@styling.link_color};text-decoration: none;", to: @unsubscribe_link %>.</span></p>
+                                                                                                       <span style="font-size: 14px;"><%= raw Gettext.dpgettext("static_pages", "digest email unsubscribe action", "To unsubscribe, please go %{here}.", here: safe_to_string link(Gettext.dpgettext("static_pages", "digest email unsubscribe action link text", "here"), style: "color: #{@styling.link_color};text-decoration: none;", to: @unsubscribe_link)) %></span></p>
                                                                                        </div>
                                                                                        <!--[if mso]></td></tr></table><![endif]-->
                                                                                        <!--[if (!mso)&(!IE)]><!-->
index de07310856bb9b2b05b21c5f98a97e415baf5da7..6d497e84c2b4bf52a7d82836151520f0b98c49cc 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 
-<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom"
+<feed xml:lang="<%= Gettext.language_tag() %>" xmlns="http://www.w3.org/2005/Atom"
       xmlns:thr="http://purl.org/syndication/thread/1.0"
       xmlns:georss="http://www.georss.org/georss"
       xmlns:activity="http://activitystrea.ms/spec/1.0/"
@@ -12,7 +12,7 @@
     <id><%= '#{Routes.tag_feed_url(@conn, :feed, @tag)}.rss' %></id>
     <title>#<%= @tag %></title>
 
-    <subtitle>These are public toots tagged with #<%= @tag %>. You can interact with them if you have an account anywhere in the fediverse.</subtitle>
+    <subtitle><%= Gettext.dpgettext("static_pages", "tag feed description", "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse.", tag: @tag) %></subtitle>
     <logo><%= feed_logo() %></logo>
     <updated><%= most_recent_update(@activities) %></updated>
     <link rel="self" href="<%= '#{Routes.tag_feed_url(@conn, :feed, @tag)}.atom'  %>" type="application/atom+xml"/>
index 9c3613febcc9e9ea9e9c7355cf70fe67a54739c2..edcc3e436d069f1b60b6a60b44d258beb368dc7e 100644 (file)
@@ -4,7 +4,7 @@
 
 
     <title>#<%= @tag %></title>
-    <description>These are public toots tagged with #<%= @tag %>. You can interact with them if you have an account anywhere in the fediverse.</description>
+    <description><%= Gettext.dpgettext("static_pages", "tag feed description", "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse.", tag: @tag) %></description>
     <link><%= '#{Routes.tag_feed_url(@conn, :feed, @tag)}.rss' %></link>
     <webfeeds:logo><%= feed_logo() %></webfeeds:logo>
     <webfeeds:accentColor>2b90d9</webfeeds:accentColor>
index 1ede59fd850cef070e7ee4f41015426ae9fc477a..e33bada858e979173d92ef45e65c55bf3f077340 100644 (file)
@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-<html lang="en">
+<html lang="<%= Pleroma.Web.Gettext.language_tag() %>">
   <head>
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui">
index f6dcd7f0fcd6d97a75161e834e32010f3cccb7bd..087aa4fc01886eb6d7c0c14bc3d278325ebc7ede 100644 (file)
@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-<html lang="en">
+<html lang="<%= Pleroma.Web.Gettext.language_tag() %>">
   <head>
     <meta charset="utf-8">
     <title><%= @email.subject %></title>
@@ -7,4 +7,4 @@
   <body>
     <%= render @view_module, @view_template, assigns %>
   </body>
-</html>
\ No newline at end of file
+</html>
index 7b476f02ddfd30b6bbdf9305646d1ad664afe412..df090ffcddec75aad917e43362ca95a41292b4a6 100644 (file)
@@ -1 +1 @@
-<h1>UNSUBSCRIBE FAILURE</h1>
+<h1><%= Gettext.dpgettext("static_pages", "mailer unsubscribe failed message", "UNSUBSCRIBE FAILURE") %></h1>
index 6dfa2c1859352de9345a89495586f8d25392e6e8..cbce495d4460ae67f683cb83640fea741f21cdcc 100644 (file)
@@ -1 +1 @@
-<h1>UNSUBSCRIBE SUCCESSFUL</h1>
+<h1><%= Gettext.dpgettext("static_pages", "mailer unsubscribe successful message", "UNSUBSCRIBE SUCCESSFUL") %></h1>
index b9daa8d8b52078b1d2a310382a04a33356550099..e45d13bdfae20f1c9e920c4c0512f17c6b9ab4ba 100644 (file)
@@ -5,11 +5,11 @@
 <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
 <% end %>
 
-<h2>Two-factor recovery</h2>
+<h2><%= Gettext.dpgettext("static_pages", "mfa recover page title", "Two-factor recovery") %></h2>
 
 <%= form_for @conn, Routes.mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %>
 <div class="input">
-  <%= label f, :code, "Recovery code" %>
+  <%= label f, :code, Gettext.dpgettext("static_pages", "mfa recover recovery code prompt", "Recovery code") %>
   <%= text_input f, :code, [autocomplete: false, autocorrect: "off", autocapitalize: "off", autofocus: true, spellcheck: false] %>
   <%= hidden_input f, :mfa_token, value: @mfa_token %>
   <%= hidden_input f, :state, value: @state %>
@@ -17,8 +17,8 @@
   <%= hidden_input f, :challenge_type, value: "recovery" %>
 </div>
 
-<%= submit "Verify" %>
+<%= submit Gettext.dpgettext("static_pages", "mfa recover verify recovery code button", "Verify") %>
 <% end %>
 <a href="<%= Routes.mfa_path(@conn, :show, %{challenge_type: "totp", mfa_token: @mfa_token, state: @state, redirect_uri: @redirect_uri}) %>">
-  Enter a two-factor code
+  <%= Gettext.dpgettext("static_pages", "mfa recover use 2fa code link", "Enter a two-factor code") %>
 </a>
index 29ea7c5fb6f2a3a0e3db74addb50109feb89da3e..50e6c04b64dd7d96776e66372a80e40cef6be42b 100644 (file)
@@ -5,20 +5,20 @@
 <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
 <% end %>
 
-<h2>Two-factor authentication</h2>
+<h2><%= Gettext.dpgettext("static_pages", "mfa auth page title", "Two-factor authentication") %></h2>
 
 <%= form_for @conn, Routes.mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %>
 <div class="input">
-  <%= label f, :code, "Authentication code" %>
-  <%= text_input f, :code, [autocomplete: false, autocorrect: "off", autocapitalize: "off", autofocus: true, pattern: "[0-9]*", spellcheck: false] %>
+  <%= label f, :code, Gettext.dpgettext("static_pages", "mfa auth code prompt", "Authentication code") %>
+  <%= text_input f, :code, [autocomplete: "one-time-code", autocorrect: "off", autocapitalize: "off", autofocus: true, pattern: "[0-9]*", spellcheck: false] %>
   <%= hidden_input f, :mfa_token, value: @mfa_token %>
   <%= hidden_input f, :state, value: @state %>
   <%= hidden_input f, :redirect_uri, value: @redirect_uri %>
   <%= hidden_input f, :challenge_type, value: "totp" %>
 </div>
 
-<%= submit "Verify" %>
+<%= submit Gettext.dpgettext("static_pages", "mfa auth verify code button", "Verify") %>
 <% end %>
 <a href="<%= Routes.mfa_path(@conn, :show, %{challenge_type: "recovery", mfa_token: @mfa_token, state: @state, redirect_uri: @redirect_uri}) %>">
-  Enter a two-factor recovery code
+  <%= Gettext.dpgettext("static_pages", "mfa auth page use recovery code link", "Enter a two-factor recovery code") %>
 </a>
index c9ec1ecbfae18749f0c808e31d9b9e155a38c62e..73115e92a2f191a1646f2d2e2e3c3b990d985260 100644 (file)
@@ -1,5 +1,5 @@
 <div class="scopes-input">
-  <%= label @form, :scope, "The following permissions will be granted" %>
+  <%= label @form, :scope, Gettext.dpgettext("static_pages", "oauth scopes message", "The following permissions will be granted") %>
   <div class="scopes">
     <%= for scope <- @available_scopes do %>
       <%# Note: using hidden input with `unchecked_value` in order to distinguish user's empty selection from `scope` param being omitted %>
index dc4521a62b41b7ee358e7c1908eafe09216d24fd..8b894cd58a3053413355b6972145a40b574fef5f 100644 (file)
@@ -1,4 +1,4 @@
-<h2>Sign in with external provider</h2>
+<h2><%= Gettext.dpgettext("static_pages", "oauth external provider page title", "Sign in with external provider") %></h2>
 
 <%= form_for @conn, Routes.o_auth_path(@conn, :prepare_request), [as: "authorization", method: "get"], fn f -> %>
   <div style="display: none">
@@ -10,6 +10,6 @@
   <%= hidden_input f, :state, value: @state %>
 
     <%= for strategy <- Pleroma.Config.oauth_consumer_strategies() do %>
-      <%= submit "Sign in with #{String.capitalize(strategy)}", name: "provider", value: strategy %>
+      <%= submit Gettext.dpgettext("static_pages", "oauth external provider sign in button", "Sign in with %{strategy}", strategy: String.capitalize(strategy)), name: "provider", value: strategy %>
     <% end %>
 <% end %>
index ffabe29a624ad85c67cd5e5ab5553bc9dde80cce..76ed3fda5e7c0f36decbb3219a8bef08285098d4 100644 (file)
@@ -1,2 +1,2 @@
-<h1>Successfully authorized</h1>
-<h2>Token code is <br><%= @auth.token %></h2>
+<h1><%= Gettext.dpgettext("static_pages", "oauth authorized page title", "Successfully authorized") %></h1>
+<h2><%= raw Gettext.dpgettext("static_pages", "oauth token code message", "Token code is <br>%{token}", token: safe_to_string(html_escape(@auth.token))) %></h2>
index 82785c4b99bede3011b23c3e8a44d4b64751bd2a..754bf2eb0959085cf4d6add5663a82fe2432a310 100644 (file)
@@ -1,2 +1,2 @@
-<h1>Authorization exists</h1>
-<h2>Access token is <br><%= @token.token %></h2>
+<h1><%= Gettext.dpgettext("static_pages", "oauth authorization exists page title", "Authorization exists") %></h1>
+<h2><%= raw Gettext.dpgettext("static_pages", "oauth token code message", "Token code is <br>%{token}", token: safe_to_string(html_escape(@token.token))) %></h2>
index 99f900fb7ccf59a9a64f1c9c22f78c11bd822605..1f661efb212a4dff5512ec4db979dcf77dffef1c 100644 (file)
@@ -5,34 +5,34 @@
   <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
 <% end %>
 
-<h2>Registration Details</h2>
+<h2><%= Gettext.dpgettext("static_pages", "oauth register page title", "Registration Details") %></h2>
 
-<p>If you'd like to register a new account, please provide the details below.</p>
+<p><%= Gettext.dpgettext("static_pages", "oauth register page fill form prompt", "If you'd like to register a new account, please provide the details below.") %></p>
 <%= form_for @conn, Routes.o_auth_path(@conn, :register), [as: "authorization"], fn f -> %>
 
 <div class="input">
-  <%= label f, :nickname, "Nickname" %>
-  <%= text_input f, :nickname, value: @nickname %>
+  <%= label f, :nickname, Gettext.dpgettext("static_pages", "oauth register page nickname prompt", "Nickname") %>
+  <%= text_input f, :nickname, value: @nickname, autocomplete: "username" %>
 </div>
 <div class="input">
-  <%= label f, :email, "Email" %>
-  <%= text_input f, :email, value: @email %>
+  <%= label f, :email, Gettext.dpgettext("static_pages", "oauth register page email prompt", "Email") %>
+  <%= text_input f, :email, value: @email, autocomplete: "email" %>
 </div>
 
-<%= submit "Proceed as new user", name: "op", value: "register" %>
+<%= submit Gettext.dpgettext("static_pages", "oauth register page register button", "Proceed as new user"), name: "op", value: "register" %>
 
-<p>Alternatively, sign in to connect to existing account.</p>
+<p><%= Gettext.dpgettext("static_pages", "oauth register page login prompt", "Alternatively, sign in to connect to existing account.") %></p>
 
 <div class="input">
-  <%= label f, :name, "Name or email" %>
-  <%= text_input f, :name %>
+  <%= label f, :name, Gettext.dpgettext("static_pages", "oauth register page login username prompt", "Name or email") %>
+  <%= text_input f, :name, autocomplete: "username" %>
 </div>
 <div class="input">
-  <%= label f, :password, "Password" %>
-  <%= password_input f, :password %>
+  <%= label f, :password, Gettext.dpgettext("static_pages", "oauth register page login password prompt", "Password") %>
+  <%= password_input f, :password, autocomplete: "password" %>
 </div>
 
-<%= submit "Proceed as existing user", name: "op", value: "connect" %>
+<%= submit Gettext.dpgettext("static_pages", "oauth register page login button", "Proceed as existing user"), name: "op", value: "connect" %>
 
 <%= hidden_input f, :client_id, value: @client_id %>
 <%= hidden_input f, :redirect_uri, value: @redirect_uri %>
index 181a9519ab843ea6b168efd7dc6f3e7a79603711..a2f41618e66f0ec0f3f637a10162966ae542ff1d 100644 (file)
 
 <div class="container__content">
   <%= if @app do %>
-    <p>Application <strong><%= @app.client_name %></strong> is requesting access to your account.</p>
+    <p><%= raw Gettext.dpgettext("static_pages", "oauth authorize message", "Application <strong>%{client_name}</strong> is requesting access to your account.", client_name: safe_to_string(html_escape(@app.client_name))) %></p>
     <%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f}) %>
   <% end %>
 
   <%= if @user do %>
     <div class="actions">
-      <a class="button button--cancel" href="/">Cancel</a>
-      <%= submit "Approve", class: "button--approve" %>
+      <a class="button button--cancel" href="/">
+        <%= Gettext.dpgettext("static_pages", "oauth authorize cancel button", "Cancel") %>
+      </a>
+      <%= submit Gettext.dpgettext("static_pages", "oauth authorize approve button", "Approve"), class: "button--approve" %>
     </div>
   <% else %>
     <%= if @params["registration"] in ["true", true] do %>
-      <h3>This is the first time you visit! Please enter your Pleroma handle.</h3>
-      <p>Choose carefully! You won't be able to change this later. You will be able to change your display name, though.</p>
+      <h3><%= Gettext.dpgettext("static_pages", "oauth register page title", "This is the first time you visit! Please enter your Pleroma handle.") %></h3>
+      <p><%= Gettext.dpgettext("static_pages", "oauth register nickname unchangeable warning", "Choose carefully! You won't be able to change this later. You will be able to change your display name, though.") %></p>
       <div class="input">
-        <%= label f, :nickname, "Pleroma Handle" %>
-        <%= text_input f, :nickname, placeholder: "lain" %>
+        <%= label f, :nickname, Gettext.dpgettext("static_pages", "oauth register nickname prompt", "Pleroma Handle") %>
+        <%= text_input f, :nickname, placeholder: "lain", autocomplete: "username" %>
       </div>
       <%= hidden_input f, :name, value: @params["name"] %>
       <%= hidden_input f, :password, value: @params["password"] %>
       <br>
     <% else %>
       <div class="input">
-        <%= label f, :name, "Username" %>
+        <%= label f, :name, Gettext.dpgettext("static_pages", "oauth login username prompt", "Username") %>
         <%= text_input f, :name %>
       </div>
       <div class="input">
-        <%= label f, :password, "Password" %>
+        <%= label f, :password, Gettext.dpgettext("static_pages", "oauth login password prompt", "Password") %>
         <%= password_input f, :password %>
       </div>
-      <%= submit "Log In" %>
+      <%= submit Gettext.dpgettext("static_pages", "oauth login button", "Log In") %>
     <% end %>
   <% end %>
 </div>
index 3191bf45045cb8359f58fb2425292343eebf1a05..a14ca305efe3feff17467d3306f8cbba74a8a658 100644 (file)
@@ -5,7 +5,7 @@
     <form class="pull-right collapse" method="POST" action="<%= Helpers.util_path(@conn, :remote_subscribe) %>">
       <input type="hidden" name="nickname" value="<%= @user.nickname %>">
       <input type="hidden" name="profile" value="">
-      <button type="submit" class="collapse">Remote follow</button>
+      <button type="submit" class="collapse"><%= Gettext.dpgettext("static_pages", "static fe profile page remote follow button", "Remote follow") %></button>
     </form>
     <%= raw Formatter.emojify(@user.name, @user.emoji) %> |
     <%= link "@#{@user.nickname}@#{Endpoint.host()}", to: (@user.uri || @user.ap_id) %>
index ee84750c7da9e5b04e6a6c1daf926a58c11bdfa4..5ac0aa4e0eb045d90595abeabaf7ed1583da3051 100644 (file)
@@ -1 +1 @@
-<h2>Invalid Token</h2>
+<h2><%= Gettext.dpgettext("static_pages", "password reset invalid token message", "Invalid Token") %></h2>
index fbcacdc14819867f7fc8599b0920f49e9c6eddc8..6a544af51435888c46c8c89c4cc6cc6896cc0117 100644 (file)
@@ -1,13 +1,13 @@
 <h2>Password Reset for <%= @user.nickname %></h2>
 <%= form_for @conn, Routes.reset_password_path(@conn, :do_reset), [as: "data"], fn f -> %>
   <div class="form-row">
-    <%= label f, :password, "Password" %>
+    <%= label f, :password, Gettext.dpgettext("static_pages", "password reset form password prompt", "Password") %>
     <%= password_input f, :password %>
   </div>
   <div class="form-row">
-    <%= label f, :password_confirmation, "Confirmation" %>
+    <%= label f, :password_confirmation, Gettext.dpgettext("static_pages", "password reset form confirm password prompt", "Confirmation") %>
     <%= password_input f, :password_confirmation %>
   </div>
   <%= hidden_input f, :token, value: @token.token %>
-  <%= submit "Reset" %>
+  <%= submit Gettext.dpgettext("static_pages", "password reset button", "Reset") %>
 <% end %>
index 4ed4ac8bce314effb5bc9c09f79ea790faa3ac07..774e3462aa4e82301145ed76204c428c92382f07 100644 (file)
@@ -1,2 +1,6 @@
-<h2>Password reset failed</h2>
-<h3><a href="<%= Pleroma.Web.Endpoint.url() %>">Homepage</a></h3>
+<h2><%= Gettext.dpgettext("static_pages", "password reset failed message", "Password reset failed") %></h2>
+<h3>
+  <a href="<%= Pleroma.Web.Endpoint.url() %>">
+    <%= Gettext.dpgettext("static_pages", "password reset failed homepage link", "Homepage") %>
+  </a>
+</h3>
index 086d4e08b6ad0394f40ee9a59c942b8634377c15..40f6bb3fcb3544117e2a29a2396a0e0696043e90 100644 (file)
@@ -1,2 +1,2 @@
-<h2>Password changed!</h2>
-<h3><a href="<%= Pleroma.Web.Endpoint.url() %>">Homepage</a></h3>
+<h2><%= Gettext.dpgettext("static_pages", "password reset successful message", "Password changed!") %></h2>
+<h3><a href="<%= Pleroma.Web.Endpoint.url() %>"><%= Gettext.dpgettext("static_pages", "password reset successful homepage link", "Homepage") %></a></h3>
index a7be530914e3d4f87a76d9de1757e3b2e8d41a65..e2d251faca99c206f6d3ce9ee5a4cbfb5e9650e6 100644 (file)
@@ -1,11 +1,11 @@
 <%= if @error == :error do %>
-    <h2>Error fetching user</h2>
+    <h2><%= Gettext.dpgettext("static_pages", "remote follow error", "Error fetching user") %></h2>
 <% else %>
-    <h2>Remote follow</h2>
+    <h2><%= Gettext.dpgettext("static_pages", "remote follow header", "Remote follow") %></h2>
     <img height="128" width="128" src="<%= avatar_url(@followee) %>">
     <p><%= @followee.nickname %></p>
     <%= form_for @conn, Routes.remote_follow_path(@conn, :do_follow), [as: "user"], fn f -> %>
     <%= hidden_input f, :id, value: @followee.id %>
-    <%= submit "Authorize" %>
+    <%= submit Gettext.dpgettext("static_pages", "remote follow authorization button", "Authorize") %>
     <% end %>
 <% end %>
index a8026fa9d5f056e2a0c94f31c68a3397d81beaca..26340a906f3bffad2299d284f139141113142bce 100644 (file)
@@ -1,14 +1,14 @@
 <%= if @error do %>
 <h2><%= @error %></h2>
 <% end %>
-<h2>Log in to follow</h2>
+<h2><%= Gettext.dpgettext("static_pages", "remote follow header, need login", "Log in to follow") %></h2>
 <p><%= @followee.nickname %></p>
 <img height="128" width="128" src="<%= avatar_url(@followee) %>">
 <%= form_for @conn, Routes.remote_follow_path(@conn, :do_follow), [as: "authorization"], fn f -> %>
-<%= text_input f, :name, placeholder: "Username", required: true %>
+<%= text_input f, :name, placeholder: Gettext.dpgettext("static_pages", "placeholder text for username entry", "Username"), required: true, autocomplete: "username" %>
 <br>
-<%= password_input f, :password, placeholder: "Password", required: true %>
+<%= password_input f, :password, placeholder: Gettext.dpgettext("static_pages", "placeholder text for password entry", "Password"), required: true, autocomplete: "password" %>
 <br>
 <%= hidden_input f, :id, value: @followee.id %>
-<%= submit "Authorize" %>
+<%= submit Gettext.dpgettext("static_pages", "remote follow authorization button for login", "Authorize") %>
 <% end %>
index a54ed83b593ae90df0030615f342a2006cb1fcf1..638212c1e37850194142457945360bc86818b790 100644 (file)
@@ -1,13 +1,13 @@
 <%= if @error do %>
 <h2><%= @error %></h2>
 <% end %>
-<h2>Two-factor authentication</h2>
+<h2><%= Gettext.dpgettext("static_pages", "remote follow mfa header", "Two-factor authentication") %></h2>
 <p><%= @followee.nickname %></p>
 <img height="128" width="128" src="<%= avatar_url(@followee) %>">
 <%= form_for @conn, Routes.remote_follow_path(@conn, :do_follow), [as: "mfa"], fn f -> %>
-<%= text_input f, :code, placeholder: "Authentication code", required: true %>
+<%= text_input f, :code, placeholder: Gettext.dpgettext("static_pages", "placeholder text for auth code entry", "Authentication code"), required: true %>
 <br>
 <%= hidden_input f, :id, value: @followee.id %>
 <%= hidden_input f, :token, value: @mfa_token %>
-<%= submit "Authorize" %>
+<%= submit Gettext.dpgettext("static_pages", "remote follow authorization button for mfa", "Authorize") %>
 <% end %>
index da473d502c036091c09610ec9354dc625c8596ec..2fb4cc5d36f8ec99b55c7ce09f32ffef3928165d 100644 (file)
@@ -1,6 +1,5 @@
 <%= if @error do %>
-<p>Error following account</p>
+<p><%= Gettext.dpgettext("static_pages", "remote follow error", "Error following account") %></p>
 <% else %>
-<h2>Account followed!</h2>
+<h2><%= Gettext.dpgettext("static_pages", "remote follow success", "Account followed!") %></h2>
 <% end %>
-
index a6b313d8aed26f4c1c24f9028372d0c5c2292b6e..848660f264987a1e98c8b9e379ed68e53cd0f43e 100644 (file)
@@ -1,10 +1,10 @@
 <%= if @error do %>
-  <h2>Error: <%= @error %></h2>
+  <h2><%= Gettext.dpgettext("static_pages", "remote follow error", "Error: %{error}", error: @error) %></h2>
 <% else %>
-  <h2>Remotely follow <%= @nickname %></h2>
+  <h2><%= Gettext.dpgettext("static_pages", "remote follow header", "Remotely follow %{nickname}", nickname: @nickname) %></h2>
   <%= form_for @conn, Routes.util_path(@conn, :remote_subscribe), [as: "user"], fn f -> %>
   <%= hidden_input f, :nickname, value: @nickname %>
-  <%= text_input f, :profile, placeholder: "Your account ID, e.g. lain@quitter.se" %>
-  <%= submit "Follow" %>
+  <%= text_input f, :profile, placeholder: Gettext.dpgettext("static_pages", "placeholder text for account id", "Your account ID, e.g. lain@quitter.se") %>
+  <%= submit Gettext.dpgettext("static_pages", "remote follow authorization button for following with a remote account", "Follow") %>
   <% end %>
 <% end %>
index 76ca82d20b6568e60ecd1a020c01e3a09b0c637e..7921653a86f68e7c6f9a1fa57fe86e671e83d047 100644 (file)
@@ -12,6 +12,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
   alias Pleroma.UserInviteToken
 
   def register_user(params, opts \\ []) do
+    fallback_language = Gettext.get_locale()
+
     params =
       params
       |> Map.take([:email, :token, :password])
@@ -20,6 +22,10 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
       |> Map.put(:name, Map.get(params, :fullname, params[:username]))
       |> Map.put(:password_confirmation, params[:password])
       |> Map.put(:registration_reason, params[:reason])
+      |> Map.put(
+        :language,
+        Pleroma.Web.Gettext.normalize_locale(params[:language]) || fallback_language
+      )
 
     if Pleroma.Config.get([:instance, :registrations_open]) do
       create_user(params, opts)
index a9bb95a2cc7ef0833bd79ac4dd03450efe2e5603..40e7fca49e35cce29a439bbc7f55e8ff467be50f 100644 (file)
@@ -5,4 +5,5 @@
 defmodule Pleroma.Web.TwitterAPI.PasswordView do
   use Pleroma.Web, :view
   import Phoenix.HTML.Form
+  alias Pleroma.Web.Gettext
 end
index ac3f15eec356156aeb2bd398b7a40cfb9c534285..618ba2ba58fcf0e2cf34560fa367d13d082670fe 100644 (file)
@@ -5,6 +5,7 @@
 defmodule Pleroma.Web.TwitterAPI.RemoteFollowView do
   use Pleroma.Web, :view
   import Phoenix.HTML.Form
+  alias Pleroma.Web.Gettext
 
   defdelegate avatar_url(user), to: Pleroma.User
 end
index 87cb79dd793742a4c2a60d4912caa96d37273f3c..a03020290b0129b9f1aa885e1a450284c2c68d2e 100644 (file)
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilView do
   import Phoenix.HTML.Form
   alias Pleroma.Config
   alias Pleroma.Web.Endpoint
+  alias Pleroma.Web.Gettext
 
   def status_net_config(instance) do
     """
index f7659b994f593bb9f63f0408f747933fa7e4899e..2ef049d27b387344e82169604318f83e6046e0f2 100644 (file)
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.EmailView do
   use Pleroma.Web, :view
   import Phoenix.HTML
   import Phoenix.HTML.Link
+  alias Pleroma.Web.Gettext
 
   def avatar_url(user) do
     Pleroma.User.avatar_url(user)
index 1dc80987b9cacae717582f7f9cf738baa176c2ee..01e96c61ccb2dfaf5724743caf85cbc3d37d9d63 100644 (file)
@@ -4,4 +4,5 @@
 
 defmodule Pleroma.Web.Mailer.SubscriptionView do
   use Pleroma.Web, :view
+  alias Pleroma.Web.Gettext
 end
diff --git a/lib/pleroma/workers/search_indexing_worker.ex b/lib/pleroma/workers/search_indexing_worker.ex
new file mode 100644 (file)
index 0000000..70a8d42
--- /dev/null
@@ -0,0 +1,25 @@
+defmodule Pleroma.Workers.SearchIndexingWorker do
+  use Pleroma.Workers.WorkerHelper, queue: "search_indexing"
+
+  @impl Oban.Worker
+
+  def perform(%Job{args: %{"op" => "add_to_index", "activity" => activity_id}}) do
+    activity = Pleroma.Activity.get_by_id_with_object(activity_id)
+
+    search_module = Pleroma.Config.get([Pleroma.Search, :module])
+
+    search_module.add_to_index(activity)
+
+    :ok
+  end
+
+  def perform(%Job{args: %{"op" => "remove_from_index", "object" => object_id}}) do
+    object = Pleroma.Object.get_by_id(object_id)
+
+    search_module = Pleroma.Config.get([Pleroma.Search, :module])
+
+    search_module.remove_from_index(object)
+
+    :ok
+  end
+end
diff --git a/mix.exs b/mix.exs
index 8df00154ecaa4475f9680333ea6288c1431350b6..558e71262fe67fa1a1ccd868399998ae83055055 100644 (file)
--- a/mix.exs
+++ b/mix.exs
@@ -123,7 +123,10 @@ defmodule Pleroma.Mixfile do
       {:ecto_sql, "~> 3.6.2"},
       {:postgrex, ">= 0.15.5"},
       {:oban, "~> 2.3.4"},
-      {:gettext, "~> 0.18"},
+      {:gettext,
+       git: "https://github.com/tusooa/gettext.git",
+       ref: "72fb2496b6c5280ed911bdc3756890e7f38a4808",
+       override: true},
       {:bcrypt_elixir, "~> 2.2"},
       {:trailing_format_plug, "~> 0.0.7"},
       {:fast_sanitize, "~> 0.2.0"},
@@ -200,6 +203,7 @@ defmodule Pleroma.Mixfile do
       {:nimble_parsec, "~> 1.0", override: true},
       {:phoenix_live_dashboard, "~> 0.6.2"},
       {:ecto_psql_extras, "~> 0.6"},
+      {:elasticsearch, "~> 1.0.0"},
 
       # indirect dependency version override
       {:plug, "~> 1.10.4", override: true},
@@ -248,9 +252,10 @@ defmodule Pleroma.Mixfile do
     identifier_filter = ~r/[^0-9a-z\-]+/i
 
     git_available? = match?({_output, 0}, System.cmd("sh", ["-c", "command -v git"]))
+    dotgit_present? = File.exists?(".git")
 
     git_pre_release =
-      if git_available? do
+      if git_available? and dotgit_present? do
         {tag, tag_err} =
           System.cmd("git", ["describe", "--tags", "--abbrev=0"], stderr_to_stdout: true)
 
@@ -277,6 +282,7 @@ defmodule Pleroma.Mixfile do
     # Branch name as pre-release version component, denoted with a dot
     branch_name =
       with true <- git_available?,
+           true <- dotgit_present?,
            {branch_name, 0} <- System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"]),
            branch_name <- String.trim(branch_name),
            branch_name <- System.get_env("PLEROMA_BUILD_BRANCH") || branch_name,
index 8c39d2199463b0b73aecc52e78d9cce0ad92af83..422bbea5ec6889463fa142168c6d673ce9c3c6ce 100644 (file)
--- a/mix.lock
+++ b/mix.lock
@@ -57,7 +57,7 @@
   "gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"},
   "gen_stage": {:hex, :gen_stage, "0.14.3", "d0c66f1c87faa301c1a85a809a3ee9097a4264b2edf7644bf5c123237ef732bf", [:mix], [], "hexpm"},
   "gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm"},
-  "gettext": {:hex, :gettext, "0.18.2", "7df3ea191bb56c0309c00a783334b288d08a879f53a7014341284635850a6e55", [:mix], [], "hexpm", "f9f537b13d4fdd30f3039d33cb80144c3aa1f8d9698e47d7bcbcc8df93b1f5c5"},
+  "gettext": {:git, "https://github.com/tusooa/gettext.git", "72fb2496b6c5280ed911bdc3756890e7f38a4808", [ref: "72fb2496b6c5280ed911bdc3756890e7f38a4808"]},
   "gun": {:hex, :gun, "2.0.0-rc.2", "7c489a32dedccb77b6e82d1f3c5a7dadfbfa004ec14e322cdb5e579c438632d2", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "6b9d1eae146410d727140dbf8b404b9631302ecc2066d1d12f22097ad7d254fc"},
   "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"},
   "hpax": {:hex, :hpax, "0.1.1", "2396c313683ada39e98c20a75a82911592b47e5c24391363343bde74f82396ca", [:mix], [], "hexpm", "0ae7d5a0b04a8a60caf7a39fcf3ec476f35cc2cc16c05abea730d3ce6ac6c826"},
index e476fd59f5ea10443c4f3ed80a06bff16ef67981..052633496a08f96ca086f09deed869c504bf2b94 100644 (file)
@@ -1,20 +1,22 @@
 {
-  "properties": {
-    "_timestamp": {
-      "type": "date",
-      "index": true
-    },
-    "instance": {
-      "type": "keyword"
-    },
-    "content": {
-      "type": "text"
-    },
-    "hashtags": {
-      "type": "keyword"
-    },
-    "user": {
-      "type": "text"
+  "mappings": {
+    "properties": {
+      "_timestamp": {
+        "type": "date",
+        "index": true
+      },
+      "instance": {
+        "type": "keyword"
+      },
+      "content": {
+        "type": "text"
+      },
+      "hashtags": {
+        "type": "keyword"
+      },
+      "user": {
+        "type": "text"
+      }
     }
   }
 }
diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot
new file mode 100644 (file)
index 0000000..fed111c
--- /dev/null
@@ -0,0 +1,185 @@
+## 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 ""
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:122
+msgid "%{name} - %{count} is not a multiple of %{multiple}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:131
+msgid "%{name} - %{value} is larger than exclusive maximum %{max}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:140
+msgid "%{name} - %{value} is larger than inclusive maximum %{max}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:149
+msgid "%{name} - %{value} is smaller than exclusive minimum %{min}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:158
+msgid "%{name} - %{value} is smaller than inclusive minimum %{min}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:102
+msgid "%{name} - Array items must be unique."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:114
+msgid "%{name} - Array length %{length} is larger than maxItems: %{}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:106
+msgid "%{name} - Array length %{length} is smaller than minItems: %{min}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:166
+msgid "%{name} - Invalid %{type}. Got: %{value}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:174
+msgid "%{name} - Invalid format. Expected %{format}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:51
+msgid "%{name} - Invalid schema.type. Got: %{type}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:178
+msgid "%{name} - Invalid value for enum."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:95
+msgid "%{name} - String length is larger than maxLength: %{length}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:88
+msgid "%{name} - String length is smaller than minLength: %{length}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:63
+msgid "%{name} - null value where %{type} expected."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:60
+msgid "%{name} - null value."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:182
+msgid "Failed to cast to any schema in %{polymorphic_type}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:71
+msgid "Failed to cast value as %{invalid_schema}. Value must be castable using `allOf` schemas listed."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:84
+msgid "Failed to cast value to one of: %{failed_schemas}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:78
+msgid "Failed to cast value using any of: %{failed_schemas}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:212
+msgid "Invalid value for header: %{name}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:204
+msgid "Missing field: %{name}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:208
+msgid "Missing header: %{name}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:196
+msgid "No value provided for required discriminator `%{field}`."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:216
+msgid "Object property count %{property_count} is greater than maxProperties: %{max_properties}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:224
+msgid "Object property count %{property_count} is less than minProperties: %{min_properties}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/static_fe/static_fe/error.html.eex:2
+msgid "Oops"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:188
+msgid "Unexpected field: %{name}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:200
+msgid "Unknown schema: %{name}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:192
+msgid "Value used as discriminator for `%{field}` matches no schemas."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/embed/show.html.eex:43
+#: lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex:37
+msgid "announces"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/embed/show.html.eex:44
+#: lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex:38
+msgid "likes"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/embed/show.html.eex:42
+#: lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex:36
+msgid "replies"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/embed/show.html.eex:27
+#: lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex:22
+msgid "sensitive media"
+msgstr ""
diff --git a/priv/gettext/en_test/LC_MESSAGES/default.po b/priv/gettext/en_test/LC_MESSAGES/default.po
new file mode 100644 (file)
index 0000000..63db746
--- /dev/null
@@ -0,0 +1,186 @@
+## "msgid"s in this file come from POT (.pot) files.
+##
+## Do not add, change, or remove "msgid"s manually here as
+## they're tied to the ones in the corresponding POT file
+## (with the same domain).
+##
+## Use "mix gettext.extract --merge" or "mix gettext.merge"
+## to merge POT files into PO files.
+msgid ""
+msgstr ""
+"Language: en_test\n"
+"Plural-Forms: nplurals=2\n"
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:122
+msgid "%{name} - %{count} is not a multiple of %{multiple}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:131
+msgid "%{name} - %{value} is larger than exclusive maximum %{max}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:140
+msgid "%{name} - %{value} is larger than inclusive maximum %{max}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:149
+msgid "%{name} - %{value} is smaller than exclusive minimum %{min}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:158
+msgid "%{name} - %{value} is smaller than inclusive minimum %{min}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:102
+msgid "%{name} - Array items must be unique."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:114
+msgid "%{name} - Array length %{length} is larger than maxItems: %{}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:106
+msgid "%{name} - Array length %{length} is smaller than minItems: %{min}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:166
+msgid "%{name} - Invalid %{type}. Got: %{value}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:174
+msgid "%{name} - Invalid format. Expected %{format}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:51
+msgid "%{name} - Invalid schema.type. Got: %{type}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:178
+msgid "%{name} - Invalid value for enum."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:95
+msgid "%{name} - String length is larger than maxLength: %{length}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:88
+msgid "%{name} - String length is smaller than minLength: %{length}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:63
+msgid "%{name} - null value where %{type} expected."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:60
+msgid "%{name} - null value."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:182
+msgid "Failed to cast to any schema in %{polymorphic_type}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:71
+msgid "Failed to cast value as %{invalid_schema}. Value must be castable using `allOf` schemas listed."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:84
+msgid "Failed to cast value to one of: %{failed_schemas}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:78
+msgid "Failed to cast value using any of: %{failed_schemas}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:212
+msgid "Invalid value for header: %{name}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:204
+msgid "Missing field: %{name}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:208
+msgid "Missing header: %{name}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:196
+msgid "No value provided for required discriminator `%{field}`."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:216
+msgid "Object property count %{property_count} is greater than maxProperties: %{max_properties}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:224
+msgid "Object property count %{property_count} is less than minProperties: %{min_properties}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/static_fe/static_fe/error.html.eex:2
+msgid "Oops"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:188
+msgid "Unexpected field: %{name}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:200
+msgid "Unknown schema: %{name}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/api_spec/render_error.ex:192
+msgid "Value used as discriminator for `%{field}` matches no schemas."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/embed/show.html.eex:43
+#: lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex:37
+msgid "announces"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/embed/show.html.eex:44
+#: lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex:38
+msgid "likes"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/embed/show.html.eex:42
+#: lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex:36
+msgid "replies"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/embed/show.html.eex:27
+#: lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex:22
+msgid "sensitive media"
+msgstr ""
diff --git a/priv/gettext/en_test/LC_MESSAGES/errors.po b/priv/gettext/en_test/LC_MESSAGES/errors.po
new file mode 100644 (file)
index 0000000..a40de7f
--- /dev/null
@@ -0,0 +1,557 @@
+## "msgid"s in this file come from POT (.pot) files.
+##
+## Do not add, change, or remove "msgid"s manually here as
+## they're tied to the ones in the corresponding POT file
+## (with the same domain).
+##
+## Use "mix gettext.extract --merge" or "mix gettext.merge"
+## to merge POT files into PO files.
+msgid ""
+msgstr ""
+"Language: en_test\n"
+"Plural-Forms: nplurals=2\n"
+
+msgid "can't be blank"
+msgstr ""
+
+msgid "has already been taken"
+msgstr ""
+
+msgid "is invalid"
+msgstr ""
+
+msgid "has invalid format"
+msgstr ""
+
+msgid "has an invalid entry"
+msgstr ""
+
+msgid "is reserved"
+msgstr ""
+
+msgid "does not match confirmation"
+msgstr ""
+
+msgid "is still associated with this entry"
+msgstr ""
+
+msgid "are still associated with this entry"
+msgstr ""
+
+msgid "should be %{count} character(s)"
+msgid_plural "should be %{count} character(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should have %{count} item(s)"
+msgid_plural "should have %{count} item(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should be at least %{count} character(s)"
+msgid_plural "should be at least %{count} character(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should have at least %{count} item(s)"
+msgid_plural "should have at least %{count} item(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should be at most %{count} character(s)"
+msgid_plural "should be at most %{count} character(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "should have at most %{count} item(s)"
+msgid_plural "should have at most %{count} item(s)"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "must be less than %{number}"
+msgstr ""
+
+msgid "must be greater than %{number}"
+msgstr ""
+
+msgid "must be less than or equal to %{number}"
+msgstr ""
+
+msgid "must be greater than or equal to %{number}"
+msgstr ""
+
+msgid "must be equal to %{number}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/common_api.ex:523
+msgid "Account not found"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/common_api.ex:316
+msgid "Already voted"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:402
+msgid "Bad request"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/controller_helper.ex:97
+#: lib/pleroma/web/controller_helper.ex:103
+msgid "Can't display this activity"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:324
+msgid "Can't find user"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:80
+msgid "Can't get favorites"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/common_api/utils.ex:482
+msgid "Cannot post an empty status without attachments"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/common_api/utils.ex:441
+msgid "Comment must be up to %{max_size} characters"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/config_db.ex:200
+msgid "Config with params %{params} not found"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/common_api.ex:167 lib/pleroma/web/common_api.ex:171
+msgid "Could not delete"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/common_api.ex:217
+msgid "Could not favorite"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/common_api.ex:254
+msgid "Could not unfavorite"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/common_api.ex:202
+msgid "Could not unrepeat"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/common_api.ex:530 lib/pleroma/web/common_api.ex:539
+msgid "Could not update state"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:205
+msgid "Error."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/twitter_api/twitter_api.ex:99
+msgid "Invalid CAPTCHA"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:144
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:631
+msgid "Invalid credentials"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/plugs/ensure_authenticated_plug.ex:42
+msgid "Invalid credentials."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/common_api.ex:337
+msgid "Invalid indices"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:29
+msgid "Invalid parameters"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/common_api/utils.ex:349
+msgid "Invalid password."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:254
+msgid "Invalid request"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/twitter_api/twitter_api.ex:102
+msgid "Kocaptcha service unavailable"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:140
+msgid "Missing parameters"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/common_api/utils.ex:477
+msgid "No such conversation"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:171
+#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:197 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:239
+msgid "No such permission_group"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:504
+#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:11 lib/pleroma/web/feed/tag_controller.ex:16
+#: lib/pleroma/web/feed/user_controller.ex:69 lib/pleroma/web/o_status/o_status_controller.ex:132
+#: lib/pleroma/web/plugs/uploaded_media.ex:84
+msgid "Not found"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/common_api.ex:308
+msgid "Poll's author can't vote"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:20
+#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:39 lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:51
+#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:52 lib/pleroma/web/mastodon_api/controllers/status_controller.ex:326
+#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:71
+msgid "Record not found"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:35
+#: lib/pleroma/web/feed/user_controller.ex:78 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:42
+#: lib/pleroma/web/o_status/o_status_controller.ex:138
+msgid "Something went wrong"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/common_api/activity_draft.ex:143
+msgid "The message visibility must be direct"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/common_api/utils.ex:492
+msgid "The status is over the character limit"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/plugs/ensure_public_or_authenticated_plug.ex:36
+msgid "This resource requires authentication."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/plugs/rate_limiter.ex:208
+msgid "Throttled"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/common_api.ex:338
+msgid "Too many choices"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:268
+msgid "You can't revoke your own admin status."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:243
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:333
+msgid "Your account is currently disabled"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:205
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:356
+msgid "Your login is missing a confirmed e-mail address"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:392
+msgid "can't read inbox of %{nickname} as %{as_nickname}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:491
+msgid "can't update outbox of %{nickname} as %{as_nickname}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/common_api.ex:475
+msgid "conversation is already muted"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:510
+msgid "error"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:34
+msgid "mascots can only be images"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:63
+msgid "not found"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:437
+msgid "Bad OAuth request."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/twitter_api/twitter_api.ex:108
+msgid "CAPTCHA already used"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/twitter_api/twitter_api.ex:105
+msgid "CAPTCHA expired"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/plugs/uploaded_media.ex:57
+msgid "Failed"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:453
+msgid "Failed to authenticate: %{message}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:484
+msgid "Failed to set up user account."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/plugs/o_auth_scopes_plug.ex:37
+msgid "Insufficient permissions: %{permissions}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/plugs/uploaded_media.ex:111
+msgid "Internal Error"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/o_auth/fallback_controller.ex:22
+#: lib/pleroma/web/o_auth/fallback_controller.ex:29
+msgid "Invalid Username/Password"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/twitter_api/twitter_api.ex:111
+msgid "Invalid answer data"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:33
+msgid "Nodeinfo schema version not handled"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:194
+msgid "This action is outside the authorized scopes"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/o_auth/fallback_controller.ex:14
+msgid "Unknown error, please check the details and try again."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:136
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:180
+msgid "Unlisted redirect_uri."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:433
+msgid "Unsupported OAuth provider: %{provider}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/uploaders/uploader.ex:74
+msgid "Uploader callback timeout"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/uploader_controller.ex:23
+msgid "bad request"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/twitter_api/twitter_api.ex:96
+msgid "CAPTCHA Error"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/common_api.ex:266
+msgid "Could not add reaction emoji"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/common_api.ex:277
+msgid "Could not remove reaction emoji"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/twitter_api/twitter_api.ex:122
+msgid "Invalid CAPTCHA (Missing parameter: %{name})"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:96
+msgid "List not found"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:151
+msgid "Missing parameter: %{name}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:232
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:346
+msgid "Password reset is required"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/tests/auth_test_controller.ex:9
+#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:6 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:6
+#: lib/pleroma/web/admin_api/controllers/chat_controller.ex:6 lib/pleroma/web/admin_api/controllers/config_controller.ex:6
+#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:6 lib/pleroma/web/admin_api/controllers/frontend_controller.ex:6
+#: lib/pleroma/web/admin_api/controllers/instance_controller.ex:6 lib/pleroma/web/admin_api/controllers/instance_document_controller.ex:6
+#: lib/pleroma/web/admin_api/controllers/invite_controller.ex:6 lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex:6
+#: lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex:6 lib/pleroma/web/admin_api/controllers/relay_controller.ex:6
+#: lib/pleroma/web/admin_api/controllers/report_controller.ex:6 lib/pleroma/web/admin_api/controllers/status_controller.ex:6
+#: lib/pleroma/web/admin_api/controllers/user_controller.ex:6 lib/pleroma/web/controller_helper.ex:6 lib/pleroma/web/embed_controller.ex:6
+#: lib/pleroma/web/fallback/redirect_controller.ex:6 lib/pleroma/web/feed/tag_controller.ex:6
+#: lib/pleroma/web/feed/user_controller.ex:6 lib/pleroma/web/mailer/subscription_controller.ex:6
+#: lib/pleroma/web/manifest_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/account_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/app_controller.ex:11 lib/pleroma/web/mastodon_api/controllers/auth_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/directory_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/filter_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/instance_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/marker_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex:14 lib/pleroma/web/mastodon_api/controllers/media_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/notification_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/report_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/search_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/status_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:7 lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:6 lib/pleroma/web/media_proxy/media_proxy_controller.ex:6
+#: lib/pleroma/web/mongoose_im/mongoose_im_controller.ex:6 lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:6
+#: lib/pleroma/web/o_auth/fallback_controller.ex:6 lib/pleroma/web/o_auth/mfa_controller.ex:10
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:6 lib/pleroma/web/o_status/o_status_controller.ex:6
+#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/app_controller.ex:6
+#: lib/pleroma/web/pleroma_api/controllers/backup_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/chat_controller.ex:5
+#: lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex:6
+#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex:6
+#: lib/pleroma/web/pleroma_api/controllers/instances_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:6
+#: lib/pleroma/web/pleroma_api/controllers/notification_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/report_controller.ex:6
+#: lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex:6
+#: lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex:7 lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex:6
+#: lib/pleroma/web/static_fe/static_fe_controller.ex:6 lib/pleroma/web/twitter_api/controller.ex:6
+#: lib/pleroma/web/twitter_api/controllers/password_controller.ex:10 lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex:6
+#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:6 lib/pleroma/web/uploader_controller.ex:6
+#: lib/pleroma/web/web_finger/web_finger_controller.ex:6
+msgid "Security violation: OAuth scopes check was neither handled nor explicitly skipped."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/plugs/ensure_authenticated_plug.ex:32
+msgid "Two-factor authentication enabled, you must use a access token."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61
+msgid "Web push subscription is disabled on this Pleroma instance"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:234
+msgid "You can't revoke your own admin/moderator status."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:129
+msgid "authorization required for timeline view"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:24
+msgid "Access denied"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:321
+msgid "This API requires an authenticated user"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/plugs/ensure_staff_privileged_plug.ex:26
+#: lib/pleroma/web/plugs/user_is_admin_plug.ex:21
+msgid "User is not an admin."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/user/backup.ex:75
+msgid "Last export was less than a day ago"
+msgid_plural "Last export was less than %{days} days ago"
+msgstr[0] ""
+msgstr[1] ""
+
+#, elixir-format
+#: lib/pleroma/user/backup.ex:93
+msgid "Backups require enabled email"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:423
+msgid "Character limit (%{limit} characters) exceeded, contains %{length} characters"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/user/backup.ex:98
+msgid "Email is required"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/common_api/utils.ex:507
+msgid "Too many attachments"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/plugs/ensure_staff_privileged_plug.ex:33
+#: lib/pleroma/web/plugs/user_is_staff_plug.ex:20
+msgid "User is not a staff member."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:366
+msgid "Your account is awaiting approval."
+msgstr ""
diff --git a/priv/gettext/en_test/LC_MESSAGES/posix_errors.po b/priv/gettext/en_test/LC_MESSAGES/posix_errors.po
new file mode 100644 (file)
index 0000000..663fc59
--- /dev/null
@@ -0,0 +1,153 @@
+## "msgid"s in this file come from POT (.pot) files.
+##
+## Do not add, change, or remove "msgid"s manually here as
+## they're tied to the ones in the corresponding POT file
+## (with the same domain).
+##
+## Use "mix gettext.extract --merge" or "mix gettext.merge"
+## to merge POT files into PO files.
+msgid ""
+msgstr ""
+"Language: en_test\n"
+"Plural-Forms: nplurals=2\n"
+
+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/en_test/LC_MESSAGES/static_pages.po b/priv/gettext/en_test/LC_MESSAGES/static_pages.po
new file mode 100644 (file)
index 0000000..1a3b7b3
--- /dev/null
@@ -0,0 +1,529 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR Free Software Foundation, Inc.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"PO-Revision-Date: 2022-03-06 11:27-0500\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#~ ## "msgid"s in this file come from POT (.pot) files.
+#~ ##
+#~ ## Do not add, change, or remove "msgid"s manually here as
+#~ ## they're tied to the ones in the corresponding POT file
+#~ ## (with the same domain).
+#~ ##
+#~ ## Use "mix gettext.extract --merge" or "mix gettext.merge"
+#~ ## to merge POT files into PO files.
+#~ msgid ""
+#~ msgstr ""
+#~ "Language: en_test\n"
+#~ "Plural-Forms: nplurals=2\n"
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:9
+msgctxt "remote follow authorization button"
+msgid "Authorize"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:2
+msgctxt "remote follow error"
+msgid "Error fetching user"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:4
+msgctxt "remote follow header"
+msgid "Remote follow"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:8
+msgctxt "placeholder text for auth code entry"
+msgid "Authentication code"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:10
+msgctxt "placeholder text for password entry"
+msgid "Password"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:8
+msgctxt "placeholder text for username entry"
+msgid "Username"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:13
+msgctxt "remote follow authorization button for login"
+msgid "Authorize"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:12
+msgctxt "remote follow authorization button for mfa"
+msgid "Authorize"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:2
+msgctxt "remote follow error"
+msgid "Error following account"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:4
+msgctxt "remote follow header, need login"
+msgid "Log in to follow"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:4
+msgctxt "remote follow mfa header"
+msgid "Two-factor authentication"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:4
+msgctxt "remote follow success"
+msgid "Account followed!"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:7
+msgctxt "placeholder text for account id"
+msgid "Your account ID, e.g. lain@quitter.se"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:8
+msgctxt "remote follow authorization button for following with a remote account"
+msgid "Follow"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:2
+msgctxt "remote follow error"
+msgid "Error: %{error}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:4
+msgctxt "remote follow header"
+msgid "Remotely follow %{nickname}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:12
+msgctxt "password reset button"
+msgid "Reset"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:4
+msgctxt "password reset failed homepage link"
+msgid "Homepage"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:1
+msgctxt "password reset failed message"
+msgid "Password reset failed"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:8
+msgctxt "password reset form confirm password prompt"
+msgid "Confirmation"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:4
+msgctxt "password reset form password prompt"
+msgid "Password"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex:1
+msgctxt "password reset invalid token message"
+msgid "Invalid Token"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:2
+msgctxt "password reset successful homepage link"
+msgid "Homepage"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:1
+msgctxt "password reset successful message"
+msgid "Password changed!"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/feed/feed/tag.atom.eex:15
+#: lib/pleroma/web/templates/feed/feed/tag.rss.eex:7
+msgctxt "tag feed description"
+msgid "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:1
+msgctxt "oauth authorization exists page title"
+msgid "Authorization exists"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:32
+msgctxt "oauth authorize approve button"
+msgid "Approve"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:30
+msgctxt "oauth authorize cancel button"
+msgid "Cancel"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:23
+msgctxt "oauth authorize message"
+msgid "Application <strong>%{client_name}</strong> is requesting access to your account."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:1
+msgctxt "oauth authorized page title"
+msgid "Successfully authorized"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:1
+msgctxt "oauth external provider page title"
+msgid "Sign in with external provider"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:13
+msgctxt "oauth external provider sign in button"
+msgid "Sign in with %{strategy}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:54
+msgctxt "oauth login button"
+msgid "Log In"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:51
+msgctxt "oauth login password prompt"
+msgid "Password"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:47
+msgctxt "oauth login username prompt"
+msgid "Username"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:39
+msgctxt "oauth register nickname prompt"
+msgid "Pleroma Handle"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:37
+msgctxt "oauth register nickname unchangeable warning"
+msgid "Choose carefully! You won't be able to change this later. You will be able to change your display name, though."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:18
+msgctxt "oauth register page email prompt"
+msgid "Email"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:10
+msgctxt "oauth register page fill form prompt"
+msgid "If you'd like to register a new account, please provide the details below."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:35
+msgctxt "oauth register page login button"
+msgid "Proceed as existing user"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:31
+msgctxt "oauth register page login password prompt"
+msgid "Password"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:24
+msgctxt "oauth register page login prompt"
+msgid "Alternatively, sign in to connect to existing account."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:27
+msgctxt "oauth register page login username prompt"
+msgid "Name or email"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:14
+msgctxt "oauth register page nickname prompt"
+msgid "Nickname"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:22
+msgctxt "oauth register page register button"
+msgid "Proceed as new user"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:8
+msgctxt "oauth register page title"
+msgid "Registration Details"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:36
+msgctxt "oauth register page title"
+msgid "This is the first time you visit! Please enter your Pleroma handle."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex:2
+msgctxt "oauth scopes message"
+msgid "The following permissions will be granted"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:2
+#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:2
+msgctxt "oauth token code message"
+msgid "Token code is <br>%{token}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:12
+msgctxt "mfa auth code prompt"
+msgid "Authentication code"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:8
+msgctxt "mfa auth page title"
+msgid "Two-factor authentication"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:23
+msgctxt "mfa auth page use recovery code link"
+msgid "Enter a two-factor recovery code"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:20
+msgctxt "mfa auth verify code button"
+msgid "Verify"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:8
+msgctxt "mfa recover page title"
+msgid "Two-factor recovery"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:12
+msgctxt "mfa recover recovery code prompt"
+msgid "Recovery code"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:23
+msgctxt "mfa recover use 2fa code link"
+msgid "Enter a two-factor code"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:20
+msgctxt "mfa recover verify recovery code button"
+msgid "Verify"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex:8
+msgctxt "static fe profile page remote follow button"
+msgid "Remote follow"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/email/digest.html.eex:163
+msgctxt "digest email header line"
+msgid "Hey %{nickname}, here is what you've missed!"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/email/digest.html.eex:544
+msgctxt "digest email receiver address"
+msgid "The email address you are subscribed as is <a href='mailto:%{@user.email}' style='color: %{color};text-decoration: none;'>%{email}</a>. "
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/email/digest.html.eex:538
+msgctxt "digest email sending reason"
+msgid "You have received this email because you have signed up to receive digest emails from <b>%{instance}</b> Pleroma instance."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/email/digest.html.eex:547
+msgctxt "digest email unsubscribe action"
+msgid "To unsubscribe, please go %{here}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/email/digest.html.eex:547
+msgctxt "digest email unsubscribe action link text"
+msgid "here"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_failure.html.eex:1
+msgctxt "mailer unsubscribe failed message"
+msgid "UNSUBSCRIBE FAILURE"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_success.html.eex:1
+msgctxt "mailer unsubscribe successful message"
+msgid "UNSUBSCRIBE SUCCESSFUL"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/email/digest.html.eex:385
+msgctxt "new followers count header"
+msgid "%{count} New Follower"
+msgid_plural "%{count} New Followers"
+msgstr[0] "xx%{count} New Followerxx"
+msgstr[1] "xx%{count} New Followersxx"
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:356
+msgctxt "account archive email body - self-requested"
+msgid "<p>You requested a full backup of your Pleroma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:384
+msgctxt "account archive email subject"
+msgid "Your account archive is ready"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:188
+msgctxt "approval pending email body"
+msgid "<h3>Awaiting Approval</h3>\n<p>Your account at %{instance_name} is being reviewed by staff. You will receive another email once your account is approved.</p>\n"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:202
+msgctxt "approval pending email subject"
+msgid "Your account is awaiting approval"
+msgstr "xxYour account is awaiting approvalxx"
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:158
+msgctxt "confirmation email body"
+msgid "<h3>Thank you for registering on %{instance_name}</h3>\n<p>Email confirmation is required to activate the account.</p>\n<p>Please click the following link to <a href=\"%{confirmation_url}\">activate your account</a>.</p>\n"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:174
+msgctxt "confirmation email subject"
+msgid "%{instance_name} account confirmation"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:310
+msgctxt "digest email subject"
+msgid "Your digest from %{instance_name}"
+msgstr "xxYour digest from %{instance_name}xx"
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:81
+msgctxt "password reset email body"
+msgid "<h3>Reset your password at %{instance_name}</h3>\n<p>Someone has requested password change for your account at %{instance_name}.</p>\n<p>If it was you, visit the following link to proceed: <a href=\"%{password_reset_url}\">reset password</a>.</p>\n<p>If it was someone else, nothing to worry about: your data is secure and your password has not been changed.</p>\n"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:98
+msgctxt "password reset email subject"
+msgid "Password reset"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:215
+msgctxt "successful registration email body"
+msgid "<h3>Hello @%{nickname},</h3>\n<p>Your account at %{instance_name} has been registered successfully.</p>\n<p>No further action is required to activate your account.</p>\n"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:231
+msgctxt "successful registration email subject"
+msgid "Account registered on %{instance_name}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:119
+msgctxt "user invitation email body"
+msgid "<h3>You are invited to %{instance_name}</h3>\n<p>%{inviter_name} invites you to join %{instance_name}, an instance of Pleroma federated social networking platform.</p>\n<p>Click the following link to register: <a href=\"%{registration_url}\">accept invitation</a>.</p>\n"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:136
+msgctxt "user invitation email subject"
+msgid "Invitation to %{instance_name}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:53
+msgctxt "welcome email html body"
+msgid "Welcome to %{instance_name}!"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:41
+msgctxt "welcome email subject"
+msgid "Welcome to %{instance_name}!"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:65
+msgctxt "welcome email text body"
+msgid "Welcome to %{instance_name}!"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:368
+msgctxt "account archive email body - admin requested"
+msgid "<p>Admin @%{admin_nickname} requested a full backup of your Pleroma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
+msgstr ""
index e337226a736c63c0e7d5d319c9b3ebc7f42b5ee9..7644fc2305a4725e58e8b4fbc3a2370820db6419 100644 (file)
@@ -90,121 +90,99 @@ msgid "must be equal to %{number}"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:505
+#: lib/pleroma/web/common_api.ex:523
 msgid "Account not found"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:339
+#: lib/pleroma/web/common_api.ex:316
 msgid "Already voted"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/oauth/oauth_controller.ex:359
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:402
 msgid "Bad request"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:426
-msgid "Can't delete object"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/controller_helper.ex:105
-#: lib/pleroma/web/controller_helper.ex:111
+#: lib/pleroma/web/controller_helper.ex:97
+#: lib/pleroma/web/controller_helper.ex:103
 msgid "Can't display this activity"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:285
+#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:324
 msgid "Can't find user"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:61
+#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:80
 msgid "Can't get favorites"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:438
-msgid "Can't like object"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/common_api/utils.ex:563
+#: lib/pleroma/web/common_api/utils.ex:482
 msgid "Cannot post an empty status without attachments"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/common_api/utils.ex:511
+#: lib/pleroma/web/common_api/utils.ex:441
 msgid "Comment must be up to %{max_size} characters"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/config/config_db.ex:191
+#: lib/pleroma/config_db.ex:200
 msgid "Config with params %{params} not found"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:181
-#: lib/pleroma/web/common_api/common_api.ex:185
+#: lib/pleroma/web/common_api.ex:167 lib/pleroma/web/common_api.ex:171
 msgid "Could not delete"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:231
+#: lib/pleroma/web/common_api.ex:217
 msgid "Could not favorite"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:453
-msgid "Could not pin"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:278
+#: lib/pleroma/web/common_api.ex:254
 msgid "Could not unfavorite"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:463
-msgid "Could not unpin"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:216
+#: lib/pleroma/web/common_api.ex:202
 msgid "Could not unrepeat"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:512
-#: lib/pleroma/web/common_api/common_api.ex:521
+#: lib/pleroma/web/common_api.ex:530 lib/pleroma/web/common_api.ex:539
 msgid "Could not update state"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:207
+#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:205
 msgid "Error."
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/twitter_api/twitter_api.ex:106
+#: lib/pleroma/web/twitter_api/twitter_api.ex:99
 msgid "Invalid CAPTCHA"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:116
-#: lib/pleroma/web/oauth/oauth_controller.ex:568
+#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:144
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:631
 msgid "Invalid credentials"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/plugs/ensure_authenticated_plug.ex:38
+#: lib/pleroma/web/plugs/ensure_authenticated_plug.ex:42
 msgid "Invalid credentials."
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:355
+#: lib/pleroma/web/common_api.ex:337
 msgid "Invalid indices"
 msgstr ""
 
@@ -214,189 +192,184 @@ msgid "Invalid parameters"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/common_api/utils.ex:414
+#: lib/pleroma/web/common_api/utils.ex:349
 msgid "Invalid password."
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:220
+#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:254
 msgid "Invalid request"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/twitter_api/twitter_api.ex:109
+#: lib/pleroma/web/twitter_api/twitter_api.ex:102
 msgid "Kocaptcha service unavailable"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:112
+#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:140
 msgid "Missing parameters"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/common_api/utils.ex:547
+#: lib/pleroma/web/common_api/utils.ex:477
 msgid "No such conversation"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:388
-#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:414 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:456
+#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:171
+#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:197 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:239
 msgid "No such permission_group"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/plugs/uploaded_media.ex:84
-#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:486 lib/pleroma/web/admin_api/controllers/fallback_controller.ex:11
-#: lib/pleroma/web/feed/user_controller.ex:71 lib/pleroma/web/ostatus/ostatus_controller.ex:143
+#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:504
+#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:11 lib/pleroma/web/feed/tag_controller.ex:16
+#: lib/pleroma/web/feed/user_controller.ex:69 lib/pleroma/web/o_status/o_status_controller.ex:132
+#: lib/pleroma/web/plugs/uploaded_media.ex:84
 msgid "Not found"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:331
+#: lib/pleroma/web/common_api.ex:308
 msgid "Poll's author can't vote"
 msgstr ""
 
 #, elixir-format
 #: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:20
-#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:37 lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:49
-#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:50 lib/pleroma/web/mastodon_api/controllers/status_controller.ex:306
+#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:39 lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:51
+#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:52 lib/pleroma/web/mastodon_api/controllers/status_controller.ex:326
 #: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:71
 msgid "Record not found"
 msgstr ""
 
 #, elixir-format
 #: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:35
-#: lib/pleroma/web/feed/user_controller.ex:77 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:36
-#: lib/pleroma/web/ostatus/ostatus_controller.ex:149
+#: lib/pleroma/web/feed/user_controller.ex:78 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:42
+#: lib/pleroma/web/o_status/o_status_controller.ex:138
 msgid "Something went wrong"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/common_api/activity_draft.ex:107
+#: lib/pleroma/web/common_api/activity_draft.ex:143
 msgid "The message visibility must be direct"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/common_api/utils.ex:573
+#: lib/pleroma/web/common_api/utils.ex:492
 msgid "The status is over the character limit"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex:31
+#: lib/pleroma/web/plugs/ensure_public_or_authenticated_plug.ex:36
 msgid "This resource requires authentication."
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/plugs/rate_limiter/rate_limiter.ex:206
+#: lib/pleroma/web/plugs/rate_limiter.ex:208
 msgid "Throttled"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:356
+#: lib/pleroma/web/common_api.ex:338
 msgid "Too many choices"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:443
-msgid "Unhandled activity type"
-msgstr ""
-
-#, elixir-format
-#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:485
+#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:268
 msgid "You can't revoke your own admin status."
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/oauth/oauth_controller.ex:221
-#: lib/pleroma/web/oauth/oauth_controller.ex:308
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:243
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:333
 msgid "Your account is currently disabled"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/oauth/oauth_controller.ex:183
-#: lib/pleroma/web/oauth/oauth_controller.ex:331
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:205
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:356
 msgid "Your login is missing a confirmed e-mail address"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:390
+#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:392
 msgid "can't read inbox of %{nickname} as %{as_nickname}"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:473
+#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:491
 msgid "can't update outbox of %{nickname} as %{as_nickname}"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:471
+#: lib/pleroma/web/common_api.ex:475
 msgid "conversation is already muted"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:314
-#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:492
+#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:510
 msgid "error"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:32
+#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:34
 msgid "mascots can only be images"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:62
+#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:63
 msgid "not found"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/oauth/oauth_controller.ex:394
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:437
 msgid "Bad OAuth request."
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/twitter_api/twitter_api.ex:115
+#: lib/pleroma/web/twitter_api/twitter_api.ex:108
 msgid "CAPTCHA already used"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/twitter_api/twitter_api.ex:112
+#: lib/pleroma/web/twitter_api/twitter_api.ex:105
 msgid "CAPTCHA expired"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/plugs/uploaded_media.ex:57
+#: lib/pleroma/web/plugs/uploaded_media.ex:57
 msgid "Failed"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/oauth/oauth_controller.ex:410
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:453
 msgid "Failed to authenticate: %{message}."
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/oauth/oauth_controller.ex:441
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:484
 msgid "Failed to set up user account."
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/plugs/oauth_scopes_plug.ex:38
+#: lib/pleroma/web/plugs/o_auth_scopes_plug.ex:37
 msgid "Insufficient permissions: %{permissions}."
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/plugs/uploaded_media.ex:104
+#: lib/pleroma/web/plugs/uploaded_media.ex:111
 msgid "Internal Error"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/oauth/fallback_controller.ex:22
-#: lib/pleroma/web/oauth/fallback_controller.ex:29
+#: lib/pleroma/web/o_auth/fallback_controller.ex:22
+#: lib/pleroma/web/o_auth/fallback_controller.ex:29
 msgid "Invalid Username/Password"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/twitter_api/twitter_api.ex:118
+#: lib/pleroma/web/twitter_api/twitter_api.ex:111
 msgid "Invalid answer data"
 msgstr ""
 
@@ -406,28 +379,28 @@ msgid "Nodeinfo schema version not handled"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/oauth/oauth_controller.ex:172
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:194
 msgid "This action is outside the authorized scopes"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/oauth/fallback_controller.ex:14
+#: lib/pleroma/web/o_auth/fallback_controller.ex:14
 msgid "Unknown error, please check the details and try again."
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/oauth/oauth_controller.ex:119
-#: lib/pleroma/web/oauth/oauth_controller.ex:158
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:136
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:180
 msgid "Unlisted redirect_uri."
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/oauth/oauth_controller.ex:390
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:433
 msgid "Unsupported OAuth provider: %{provider}."
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/uploaders/uploader.ex:72
+#: lib/pleroma/uploaders/uploader.ex:74
 msgid "Uploader callback timeout"
 msgstr ""
 
@@ -437,134 +410,154 @@ msgid "bad request"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/twitter_api/twitter_api.ex:103
+#: lib/pleroma/web/twitter_api/twitter_api.ex:96
 msgid "CAPTCHA Error"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:290
+#: lib/pleroma/web/common_api.ex:266
 msgid "Could not add reaction emoji"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:301
+#: lib/pleroma/web/common_api.ex:277
 msgid "Could not remove reaction emoji"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/twitter_api/twitter_api.ex:129
+#: lib/pleroma/web/twitter_api/twitter_api.ex:122
 msgid "Invalid CAPTCHA (Missing parameter: %{name})"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:92
+#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:96
 msgid "List not found"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:123
+#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:151
 msgid "Missing parameter: %{name}"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/oauth/oauth_controller.ex:210
-#: lib/pleroma/web/oauth/oauth_controller.ex:321
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:232
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:346
 msgid "Password reset is required"
 msgstr ""
 
 #, elixir-format
 #: lib/pleroma/tests/auth_test_controller.ex:9
 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:6 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:6
-#: lib/pleroma/web/admin_api/controllers/config_controller.ex:6 lib/pleroma/web/admin_api/controllers/fallback_controller.ex:6
+#: lib/pleroma/web/admin_api/controllers/chat_controller.ex:6 lib/pleroma/web/admin_api/controllers/config_controller.ex:6
+#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:6 lib/pleroma/web/admin_api/controllers/frontend_controller.ex:6
+#: lib/pleroma/web/admin_api/controllers/instance_controller.ex:6 lib/pleroma/web/admin_api/controllers/instance_document_controller.ex:6
 #: lib/pleroma/web/admin_api/controllers/invite_controller.ex:6 lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex:6
-#: lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex:6 lib/pleroma/web/admin_api/controllers/relay_controller.ex:6
+#: lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex:6 lib/pleroma/web/admin_api/controllers/relay_controller.ex:6
 #: lib/pleroma/web/admin_api/controllers/report_controller.ex:6 lib/pleroma/web/admin_api/controllers/status_controller.ex:6
-#: lib/pleroma/web/controller_helper.ex:6 lib/pleroma/web/embed_controller.ex:6
-#: lib/pleroma/web/fallback_redirect_controller.ex:6 lib/pleroma/web/feed/tag_controller.ex:6
-#: lib/pleroma/web/feed/user_controller.ex:6 lib/pleroma/web/mailer/subscription_controller.ex:2
-#: lib/pleroma/web/masto_fe_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/account_controller.ex:6
-#: lib/pleroma/web/mastodon_api/controllers/app_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/auth_controller.ex:6
+#: lib/pleroma/web/admin_api/controllers/user_controller.ex:6 lib/pleroma/web/controller_helper.ex:6 lib/pleroma/web/embed_controller.ex:6
+#: lib/pleroma/web/fallback/redirect_controller.ex:6 lib/pleroma/web/feed/tag_controller.ex:6
+#: lib/pleroma/web/feed/user_controller.ex:6 lib/pleroma/web/mailer/subscription_controller.ex:6
+#: lib/pleroma/web/manifest_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/account_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/app_controller.ex:11 lib/pleroma/web/mastodon_api/controllers/auth_controller.ex:6
 #: lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex:6
-#: lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:6
-#: lib/pleroma/web/mastodon_api/controllers/filter_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex:6
-#: lib/pleroma/web/mastodon_api/controllers/instance_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/list_controller.ex:6
-#: lib/pleroma/web/mastodon_api/controllers/marker_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex:14
-#: lib/pleroma/web/mastodon_api/controllers/media_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/notification_controller.ex:6
-#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/report_controller.ex:8
-#: lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/search_controller.ex:6
-#: lib/pleroma/web/mastodon_api/controllers/status_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:7
-#: lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:6
-#: lib/pleroma/web/media_proxy/media_proxy_controller.ex:6 lib/pleroma/web/mongooseim/mongoose_im_controller.ex:6
-#: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:6 lib/pleroma/web/oauth/fallback_controller.ex:6
-#: lib/pleroma/web/oauth/mfa_controller.ex:10 lib/pleroma/web/oauth/oauth_controller.ex:6
-#: lib/pleroma/web/ostatus/ostatus_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/account_controller.ex:6
-#: lib/pleroma/web/pleroma_api/controllers/chat_controller.ex:5 lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex:6
-#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:2 lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex:6
-#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/notification_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/directory_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/filter_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/instance_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/marker_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex:14 lib/pleroma/web/mastodon_api/controllers/media_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/notification_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/report_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/search_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/status_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:7 lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:6 lib/pleroma/web/media_proxy/media_proxy_controller.ex:6
+#: lib/pleroma/web/mongoose_im/mongoose_im_controller.ex:6 lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:6
+#: lib/pleroma/web/o_auth/fallback_controller.ex:6 lib/pleroma/web/o_auth/mfa_controller.ex:10
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:6 lib/pleroma/web/o_status/o_status_controller.ex:6
+#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/app_controller.ex:6
+#: lib/pleroma/web/pleroma_api/controllers/backup_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/chat_controller.ex:5
+#: lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex:6
+#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex:6
+#: lib/pleroma/web/pleroma_api/controllers/instances_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:6
+#: lib/pleroma/web/pleroma_api/controllers/notification_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/report_controller.ex:6
 #: lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex:6
-#: lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex:7 lib/pleroma/web/static_fe/static_fe_controller.ex:6
+#: lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex:7 lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex:6
+#: lib/pleroma/web/static_fe/static_fe_controller.ex:6 lib/pleroma/web/twitter_api/controller.ex:6
 #: lib/pleroma/web/twitter_api/controllers/password_controller.ex:10 lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex:6
-#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:6 lib/pleroma/web/twitter_api/twitter_api_controller.ex:6
-#: lib/pleroma/web/uploader_controller.ex:6 lib/pleroma/web/web_finger/web_finger_controller.ex:6
+#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:6 lib/pleroma/web/uploader_controller.ex:6
+#: lib/pleroma/web/web_finger/web_finger_controller.ex:6
 msgid "Security violation: OAuth scopes check was neither handled nor explicitly skipped."
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/plugs/ensure_authenticated_plug.ex:28
+#: lib/pleroma/web/plugs/ensure_authenticated_plug.ex:32
 msgid "Two-factor authentication enabled, you must use a access token."
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:210
-msgid "Unexpected error occurred while adding file to pack."
+#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61
+msgid "Web push subscription is disabled on this Pleroma instance"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:234
+msgid "You can't revoke your own admin/moderator status."
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:138
-msgid "Unexpected error occurred while creating pack."
+#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:129
+msgid "authorization required for timeline view"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:278
-msgid "Unexpected error occurred while removing file from pack."
+#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:24
+msgid "Access denied"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:250
-msgid "Unexpected error occurred while updating file in pack."
+#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:321
+msgid "This API requires an authenticated user"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:179
-msgid "Unexpected error occurred while updating pack metadata."
+#: lib/pleroma/web/plugs/ensure_staff_privileged_plug.ex:26
+#: lib/pleroma/web/plugs/user_is_admin_plug.ex:21
+msgid "User is not an admin."
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61
-msgid "Web push subscription is disabled on this Pleroma instance"
+#: lib/pleroma/user/backup.ex:75
+msgid "Last export was less than a day ago"
+msgid_plural "Last export was less than %{days} days ago"
+msgstr[0] ""
+msgstr[1] ""
+
+#, elixir-format
+#: lib/pleroma/user/backup.ex:93
+msgid "Backups require enabled email"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:451
-msgid "You can't revoke your own admin/moderator status."
+#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:423
+msgid "Character limit (%{limit} characters) exceeded, contains %{length} characters"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:126
-msgid "authorization required for timeline view"
+#: lib/pleroma/user/backup.ex:98
+msgid "Email is required"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:24
-msgid "Access denied"
+#: lib/pleroma/web/common_api/utils.ex:507
+msgid "Too many attachments"
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:282
-msgid "This API requires an authenticated user"
+#: lib/pleroma/web/plugs/ensure_staff_privileged_plug.ex:33
+#: lib/pleroma/web/plugs/user_is_staff_plug.ex:20
+msgid "User is not a staff member."
 msgstr ""
 
 #, elixir-format
-#: lib/pleroma/plugs/user_is_admin_plug.ex:21
-msgid "User is not an admin."
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:366
+msgid "Your account is awaiting approval."
 msgstr ""
index c9f593944bd3d209ba62c0e323686293bc814d0c..3533639e01f3b97f3a807d5cf790d204d9932f8c 100644 (file)
@@ -15,135 +15,135 @@ 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/static_pages.pot b/priv/gettext/static_pages.pot
new file mode 100644 (file)
index 0000000..fbc3e61
--- /dev/null
@@ -0,0 +1,513 @@
+## 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 ""
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:9
+msgctxt "remote follow authorization button"
+msgid "Authorize"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:2
+msgctxt "remote follow error"
+msgid "Error fetching user"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:4
+msgctxt "remote follow header"
+msgid "Remote follow"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:8
+msgctxt "placeholder text for auth code entry"
+msgid "Authentication code"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:10
+msgctxt "placeholder text for password entry"
+msgid "Password"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:8
+msgctxt "placeholder text for username entry"
+msgid "Username"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:13
+msgctxt "remote follow authorization button for login"
+msgid "Authorize"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:12
+msgctxt "remote follow authorization button for mfa"
+msgid "Authorize"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:2
+msgctxt "remote follow error"
+msgid "Error following account"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:4
+msgctxt "remote follow header, need login"
+msgid "Log in to follow"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:4
+msgctxt "remote follow mfa header"
+msgid "Two-factor authentication"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:4
+msgctxt "remote follow success"
+msgid "Account followed!"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:7
+msgctxt "placeholder text for account id"
+msgid "Your account ID, e.g. lain@quitter.se"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:8
+msgctxt "remote follow authorization button for following with a remote account"
+msgid "Follow"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:2
+msgctxt "remote follow error"
+msgid "Error: %{error}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:4
+msgctxt "remote follow header"
+msgid "Remotely follow %{nickname}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:12
+msgctxt "password reset button"
+msgid "Reset"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:4
+msgctxt "password reset failed homepage link"
+msgid "Homepage"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:1
+msgctxt "password reset failed message"
+msgid "Password reset failed"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:8
+msgctxt "password reset form confirm password prompt"
+msgid "Confirmation"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:4
+msgctxt "password reset form password prompt"
+msgid "Password"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex:1
+msgctxt "password reset invalid token message"
+msgid "Invalid Token"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:2
+msgctxt "password reset successful homepage link"
+msgid "Homepage"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:1
+msgctxt "password reset successful message"
+msgid "Password changed!"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/feed/feed/tag.atom.eex:15
+#: lib/pleroma/web/templates/feed/feed/tag.rss.eex:7
+msgctxt "tag feed description"
+msgid "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:1
+msgctxt "oauth authorization exists page title"
+msgid "Authorization exists"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:32
+msgctxt "oauth authorize approve button"
+msgid "Approve"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:30
+msgctxt "oauth authorize cancel button"
+msgid "Cancel"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:23
+msgctxt "oauth authorize message"
+msgid "Application <strong>%{client_name}</strong> is requesting access to your account."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:1
+msgctxt "oauth authorized page title"
+msgid "Successfully authorized"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:1
+msgctxt "oauth external provider page title"
+msgid "Sign in with external provider"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:13
+msgctxt "oauth external provider sign in button"
+msgid "Sign in with %{strategy}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:54
+msgctxt "oauth login button"
+msgid "Log In"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:51
+msgctxt "oauth login password prompt"
+msgid "Password"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:47
+msgctxt "oauth login username prompt"
+msgid "Username"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:39
+msgctxt "oauth register nickname prompt"
+msgid "Pleroma Handle"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:37
+msgctxt "oauth register nickname unchangeable warning"
+msgid "Choose carefully! You won't be able to change this later. You will be able to change your display name, though."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:18
+msgctxt "oauth register page email prompt"
+msgid "Email"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:10
+msgctxt "oauth register page fill form prompt"
+msgid "If you'd like to register a new account, please provide the details below."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:35
+msgctxt "oauth register page login button"
+msgid "Proceed as existing user"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:31
+msgctxt "oauth register page login password prompt"
+msgid "Password"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:24
+msgctxt "oauth register page login prompt"
+msgid "Alternatively, sign in to connect to existing account."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:27
+msgctxt "oauth register page login username prompt"
+msgid "Name or email"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:14
+msgctxt "oauth register page nickname prompt"
+msgid "Nickname"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:22
+msgctxt "oauth register page register button"
+msgid "Proceed as new user"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:8
+msgctxt "oauth register page title"
+msgid "Registration Details"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:36
+msgctxt "oauth register page title"
+msgid "This is the first time you visit! Please enter your Pleroma handle."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex:2
+msgctxt "oauth scopes message"
+msgid "The following permissions will be granted"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:2
+#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:2
+msgctxt "oauth token code message"
+msgid "Token code is <br>%{token}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:12
+msgctxt "mfa auth code prompt"
+msgid "Authentication code"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:8
+msgctxt "mfa auth page title"
+msgid "Two-factor authentication"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:23
+msgctxt "mfa auth page use recovery code link"
+msgid "Enter a two-factor recovery code"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:20
+msgctxt "mfa auth verify code button"
+msgid "Verify"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:8
+msgctxt "mfa recover page title"
+msgid "Two-factor recovery"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:12
+msgctxt "mfa recover recovery code prompt"
+msgid "Recovery code"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:23
+msgctxt "mfa recover use 2fa code link"
+msgid "Enter a two-factor code"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:20
+msgctxt "mfa recover verify recovery code button"
+msgid "Verify"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex:8
+msgctxt "static fe profile page remote follow button"
+msgid "Remote follow"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/email/digest.html.eex:163
+msgctxt "digest email header line"
+msgid "Hey %{nickname}, here is what you've missed!"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/email/digest.html.eex:544
+msgctxt "digest email receiver address"
+msgid "The email address you are subscribed as is <a href='mailto:%{@user.email}' style='color: %{color};text-decoration: none;'>%{email}</a>. "
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/email/digest.html.eex:538
+msgctxt "digest email sending reason"
+msgid "You have received this email because you have signed up to receive digest emails from <b>%{instance}</b> Pleroma instance."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/email/digest.html.eex:547
+msgctxt "digest email unsubscribe action"
+msgid "To unsubscribe, please go %{here}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/email/digest.html.eex:547
+msgctxt "digest email unsubscribe action link text"
+msgid "here"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_failure.html.eex:1
+msgctxt "mailer unsubscribe failed message"
+msgid "UNSUBSCRIBE FAILURE"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_success.html.eex:1
+msgctxt "mailer unsubscribe successful message"
+msgid "UNSUBSCRIBE SUCCESSFUL"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/email/digest.html.eex:385
+msgctxt "new followers count header"
+msgid "%{count} New Follower"
+msgid_plural "%{count} New Followers"
+msgstr[0] ""
+msgstr[1] ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:356
+msgctxt "account archive email body - self-requested"
+msgid "<p>You requested a full backup of your Pleroma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:384
+msgctxt "account archive email subject"
+msgid "Your account archive is ready"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:188
+msgctxt "approval pending email body"
+msgid "<h3>Awaiting Approval</h3>\n<p>Your account at %{instance_name} is being reviewed by staff. You will receive another email once your account is approved.</p>\n"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:202
+msgctxt "approval pending email subject"
+msgid "Your account is awaiting approval"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:158
+msgctxt "confirmation email body"
+msgid "<h3>Thank you for registering on %{instance_name}</h3>\n<p>Email confirmation is required to activate the account.</p>\n<p>Please click the following link to <a href=\"%{confirmation_url}\">activate your account</a>.</p>\n"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:174
+msgctxt "confirmation email subject"
+msgid "%{instance_name} account confirmation"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:310
+msgctxt "digest email subject"
+msgid "Your digest from %{instance_name}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:81
+msgctxt "password reset email body"
+msgid "<h3>Reset your password at %{instance_name}</h3>\n<p>Someone has requested password change for your account at %{instance_name}.</p>\n<p>If it was you, visit the following link to proceed: <a href=\"%{password_reset_url}\">reset password</a>.</p>\n<p>If it was someone else, nothing to worry about: your data is secure and your password has not been changed.</p>\n"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:98
+msgctxt "password reset email subject"
+msgid "Password reset"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:215
+msgctxt "successful registration email body"
+msgid "<h3>Hello @%{nickname},</h3>\n<p>Your account at %{instance_name} has been registered successfully.</p>\n<p>No further action is required to activate your account.</p>\n"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:231
+msgctxt "successful registration email subject"
+msgid "Account registered on %{instance_name}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:119
+msgctxt "user invitation email body"
+msgid "<h3>You are invited to %{instance_name}</h3>\n<p>%{inviter_name} invites you to join %{instance_name}, an instance of Pleroma federated social networking platform.</p>\n<p>Click the following link to register: <a href=\"%{registration_url}\">accept invitation</a>.</p>\n"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:136
+msgctxt "user invitation email subject"
+msgid "Invitation to %{instance_name}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:53
+msgctxt "welcome email html body"
+msgid "Welcome to %{instance_name}!"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:41
+msgctxt "welcome email subject"
+msgid "Welcome to %{instance_name}!"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:65
+msgctxt "welcome email text body"
+msgid "Welcome to %{instance_name}!"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:368
+msgctxt "account archive email body - admin requested"
+msgid "<p>Admin @%{admin_nickname} requested a full backup of your Pleroma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
+msgstr ""
diff --git a/priv/repo/migrations/20220302013920_add_language_to_users.exs b/priv/repo/migrations/20220302013920_add_language_to_users.exs
new file mode 100644 (file)
index 0000000..7a63c36
--- /dev/null
@@ -0,0 +1,9 @@
+defmodule Pleroma.Repo.Migrations.AddLanguageToUsers do
+  use Ecto.Migration
+
+  def change do
+    alter table(:users) do
+      add_if_not_exists(:language, :string)
+    end
+  end
+end
diff --git a/test/fixtures/owncast-note-with-attachment.json b/test/fixtures/owncast-note-with-attachment.json
new file mode 100644 (file)
index 0000000..68cb6bb
--- /dev/null
@@ -0,0 +1,31 @@
+{
+  "attachment": {
+    "content": "Live stream preview",
+    "type": "Image",
+    "url": "https://owncast.localhost.localdomain/preview.gif?us=KjfNX387gm"
+  },
+  "attributedTo": "https://owncast.localhost.localdomain/federation/user/streamer",
+  "audience": "https://www.w3.org/ns/activitystreams#Public",
+  "content": "<p>I've gone live!</p><p></p><p><a class=\"hashtag\" href=\"https://directory.owncast.online/tags/owncast\">#owncast</a> <a class=\"hashtag\" href=\"https://directory.owncast.online/tags/streaming\">#streaming</a></p><a href=\"https://owncast.localhost.localdomain\">https://owncast.localhost.localdomain</a>",
+  "id": "https://owncast.localhost.localdomain/federation/KjBNuq8ng",
+  "published": "2022-04-17T15:42:03Z",
+  "tag": [
+    {
+      "href": "https://directory.owncast.online/tags/owncast",
+      "name": "#owncast",
+      "type": "Hashtag"
+    },
+    {
+      "href": "https://directory.owncast.online/tags/streaming",
+      "name": "#streaming",
+      "type": "Hashtag"
+    },
+    {
+      "href": "https://directory.owncast.online/tags/owncast",
+      "name": "#owncast",
+      "type": "Hashtag"
+    }
+  ],
+  "to": "https://www.w3.org/ns/activitystreams#Public",
+  "type": "Note"
+}
index 4a9e461a9fb21b00abe66315baeaca4ec59aa3a3..b8050c7afe14f54111c26db112ec78081a80a079 100644 (file)
@@ -53,7 +53,13 @@ defmodule Mix.Tasks.Pleroma.DigestTest do
 
       assert_email_sent(
         to: {user2.name, user2.email},
-        html_body: ~r/here is what you've missed!/i
+        html_body:
+          Regex.compile!(
+            "here is what you've missed!"
+            |> Phoenix.HTML.html_escape()
+            |> Phoenix.HTML.safe_to_string(),
+            "i"
+          )
       )
     end
   end
index 21fd06ea67c4a03d736c7f8e4698797efcd1be76..771a9a49011d42cf48902cb430627c779ec34283 100644 (file)
@@ -56,4 +56,16 @@ defmodule Pleroma.Emails.UserEmailTest do
     assert email.subject == "Your account is awaiting approval"
     assert email.html_body =~ "Awaiting Approval"
   end
+
+  test "email i18n" do
+    user = insert(:user, language: "en_test")
+    email = UserEmail.approval_pending_email(user)
+    assert email.subject == "xxYour account is awaiting approvalxx"
+  end
+
+  test "email i18n should fallback to default locale if user language is unsupported" do
+    user = insert(:user, language: "unsupported")
+    email = UserEmail.approval_pending_email(user)
+    assert email.subject == "Your account is awaiting approval"
+  end
 end
index fe7fd111c8fc417ef0a9b6e60d3e68ad499810ec..978473b140ac342338ab6d01a701aade276ca2c9 100644 (file)
@@ -20,6 +20,7 @@ defmodule Pleroma.EmojiTest do
       assert Emoji.is_unicode_emoji?("🤰")
       assert Emoji.is_unicode_emoji?("❤️")
       assert Emoji.is_unicode_emoji?("🏳️‍⚧️")
+      assert Emoji.is_unicode_emoji?("🫵")
 
       # Additionally, we accept regional indicators.
       assert Emoji.is_unicode_emoji?("🇵")
index 716af496d8074a6e1dd25098c7e8c03fdfd18cfb..b47edd0a3604b37cd6bdb2d0e97ac6d774d9ef58 100644 (file)
@@ -520,6 +520,25 @@ defmodule Pleroma.NotificationTest do
     end
   end
 
+  describe "destroy_multiple_from_types/2" do
+    test "clears all notifications of a certain type for a given user" do
+      report_activity = insert(:report_activity)
+      user1 = insert(:user, is_moderator: true, is_admin: true)
+      user2 = insert(:user, is_moderator: true, is_admin: true)
+      {:ok, _} = Notification.create_notifications(report_activity)
+
+      {:ok, _} =
+        CommonAPI.post(user2, %{
+          status: "hey @#{user1.nickname} !"
+        })
+
+      Notification.destroy_multiple_from_types(user1, ["pleroma:report"])
+
+      assert [%Pleroma.Notification{type: "mention"}] = Notification.for_user(user1)
+      assert [%Pleroma.Notification{type: "pleroma:report"}] = Notification.for_user(user2)
+    end
+  end
+
   describe "set_read_up_to()" do
     test "it sets all notifications as read up to a specified notification ID" do
       user = insert(:user)
similarity index 81%
rename from test/pleroma/activity/search_test.exs
rename to test/pleroma/search/database_search_test.exs
index 657fbc627ab29c7a4632d2f4d1a0391fea1e5071..2387ac29b7023804b392f99ba6caa6ab107f8342 100644 (file)
@@ -2,8 +2,8 @@
 # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
 # SPDX-License-Identifier: AGPL-3.0-only
 
-defmodule Pleroma.Activity.SearchTest do
-  alias Pleroma.Activity.Search
+defmodule Pleroma.Search.DatabaseSearchTest do
+  alias Pleroma.Search.DatabaseSearch
   alias Pleroma.Web.CommonAPI
   import Pleroma.Factory
 
@@ -13,7 +13,7 @@ defmodule Pleroma.Activity.SearchTest do
     user = insert(:user)
     {:ok, post} = CommonAPI.post(user, %{status: "it's wednesday my dudes"})
 
-    [result] = Search.search(nil, "wednesday")
+    [result] = DatabaseSearch.search(nil, "wednesday")
 
     assert result.id == post.id
   end
@@ -28,7 +28,7 @@ defmodule Pleroma.Activity.SearchTest do
     {:ok, _post2} = CommonAPI.post(user, %{status: "it's wednesday my bros"})
 
     # plainto doesn't understand complex queries
-    assert [result] = Search.search(nil, "wednesday -dudes")
+    assert [result] = DatabaseSearch.search(nil, "wednesday -dudes")
 
     assert result.id == post.id
   end
@@ -38,7 +38,7 @@ defmodule Pleroma.Activity.SearchTest do
     {:ok, _post} = CommonAPI.post(user, %{status: "it's wednesday my dudes"})
     {:ok, other_post} = CommonAPI.post(user, %{status: "it's wednesday my bros"})
 
-    assert [result] = Search.search(nil, "wednesday -dudes")
+    assert [result] = DatabaseSearch.search(nil, "wednesday -dudes")
 
     assert result.id == other_post.id
   end
diff --git a/test/pleroma/search/elasticsearch_test.exs b/test/pleroma/search/elasticsearch_test.exs
new file mode 100644 (file)
index 0000000..cc5eb67
--- /dev/null
@@ -0,0 +1,120 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Search.ElasticsearchTest do
+  require Pleroma.Constants
+
+  use Pleroma.DataCase
+  use Oban.Testing, repo: Pleroma.Repo
+
+  import Pleroma.Factory
+  import Tesla.Mock
+  import Mock
+
+  alias Pleroma.Web.CommonAPI
+  alias Pleroma.Workers.SearchIndexingWorker
+
+  describe "elasticsearch" do
+    setup do
+      clear_config([Pleroma.Search, :module], Pleroma.Search.Elasticsearch)
+      clear_config([Pleroma.Search.Elasticsearch.Cluster, :api], Pleroma.ElasticsearchMock)
+    end
+
+    setup_with_mocks(
+      [
+        {Pleroma.Search.Elasticsearch, [:passthrough],
+         [
+           add_to_index: fn a -> passthrough([a]) end,
+           remove_from_index: fn a -> passthrough([a]) end
+         ]},
+        {Elasticsearch, [:passthrough],
+         [
+           put_document: fn _, _, _ -> :ok end,
+           delete_document: fn _, _, _ -> :ok end
+         ]}
+      ],
+      context,
+      do: {:ok, context}
+    )
+
+    test "indexes a local post on creation" do
+      user = insert(:user)
+
+      {:ok, activity} =
+        CommonAPI.post(user, %{
+          status: "guys i just don't wanna leave the swamp",
+          visibility: "public"
+        })
+
+      args = %{"op" => "add_to_index", "activity" => activity.id}
+
+      assert_enqueued(
+        worker: SearchIndexingWorker,
+        args: args
+      )
+
+      assert :ok = perform_job(SearchIndexingWorker, args)
+
+      assert_called(Pleroma.Search.Elasticsearch.add_to_index(activity))
+    end
+
+    test "doesn't index posts that are not public" do
+      user = insert(:user)
+
+      Enum.each(["private", "direct"], fn visibility ->
+        {:ok, activity} =
+          CommonAPI.post(user, %{
+            status: "guys i just don't wanna leave the swamp",
+            visibility: visibility
+          })
+
+        args = %{"op" => "add_to_index", "activity" => activity.id}
+
+        assert_enqueued(worker: SearchIndexingWorker, args: args)
+        assert :ok = perform_job(SearchIndexingWorker, args)
+
+        assert_not_called(Elasticsearch.put_document(:_))
+      end)
+
+      history = call_history(Pleroma.Search.Elasticsearch)
+      assert Enum.count(history) == 2
+    end
+
+    test "deletes posts from index when deleted locally" do
+      user = insert(:user)
+
+      mock_global(fn
+        %{method: :put, url: "http://127.0.0.1:7700/indexes/objects/documents", body: body} ->
+          assert match?(
+                   [%{"content" => "guys i just don&#39;t wanna leave the swamp"}],
+                   Jason.decode!(body)
+                 )
+
+          json(%{updateId: 1})
+
+        %{method: :delete, url: "http://127.0.0.1:7700/indexes/objects/documents/" <> id} ->
+          assert String.length(id) > 1
+          json(%{updateId: 2})
+      end)
+
+      {:ok, activity} =
+        CommonAPI.post(user, %{
+          status: "guys i just don't wanna leave the swamp",
+          visibility: "public"
+        })
+
+      args = %{"op" => "add_to_index", "activity" => activity.id}
+      assert_enqueued(worker: SearchIndexingWorker, args: args)
+      assert :ok = perform_job(SearchIndexingWorker, args)
+
+      {:ok, _} = CommonAPI.delete(activity.id, user)
+
+      delete_args = %{"op" => "remove_from_index", "object" => activity.object.id}
+      assert_enqueued(worker: SearchIndexingWorker, args: delete_args)
+      assert :ok = perform_job(SearchIndexingWorker, delete_args)
+
+      assert_called(Pleroma.Search.Elasticsearch.remove_from_index(:_))
+    end
+  end
+end
diff --git a/test/pleroma/search/meilisearch_test.exs b/test/pleroma/search/meilisearch_test.exs
new file mode 100644 (file)
index 0000000..04a2d75
--- /dev/null
@@ -0,0 +1,129 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Search.MeilisearchTest do
+  require Pleroma.Constants
+
+  use Pleroma.DataCase
+  use Oban.Testing, repo: Pleroma.Repo
+
+  import Pleroma.Factory
+  import Tesla.Mock
+  import Mock
+
+  alias Pleroma.Search.Meilisearch
+  alias Pleroma.Web.CommonAPI
+  alias Pleroma.Workers.SearchIndexingWorker
+
+  setup_all do
+    Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
+    :ok
+  end
+
+  describe "meilisearch" do
+    setup do: clear_config([Pleroma.Search, :module], Meilisearch)
+
+    setup_with_mocks(
+      [
+        {Meilisearch, [:passthrough],
+         [
+           add_to_index: fn a -> passthrough([a]) end,
+           remove_from_index: fn a -> passthrough([a]) end,
+           meili_put: fn u, a -> passthrough([u, a]) end
+         ]}
+      ],
+      context,
+      do: {:ok, context}
+    )
+
+    test "indexes a local post on creation" do
+      user = insert(:user)
+
+      mock_global(fn
+        %{method: :put, url: "http://127.0.0.1:7700/indexes/objects/documents", body: body} ->
+          assert match?(
+                   [%{"content" => "guys i just don&#39;t wanna leave the swamp"}],
+                   Jason.decode!(body)
+                 )
+
+          json(%{updateId: 1})
+      end)
+
+      {:ok, activity} =
+        CommonAPI.post(user, %{
+          status: "guys i just don't wanna leave the swamp",
+          visibility: "public"
+        })
+
+      args = %{"op" => "add_to_index", "activity" => activity.id}
+
+      assert_enqueued(
+        worker: SearchIndexingWorker,
+        args: args
+      )
+
+      assert :ok = perform_job(SearchIndexingWorker, args)
+
+      assert_called(Meilisearch.add_to_index(activity))
+    end
+
+    test "doesn't index posts that are not public" do
+      user = insert(:user)
+
+      Enum.each(["private", "direct"], fn visibility ->
+        {:ok, activity} =
+          CommonAPI.post(user, %{
+            status: "guys i just don't wanna leave the swamp",
+            visibility: visibility
+          })
+
+        args = %{"op" => "add_to_index", "activity" => activity.id}
+
+        assert_enqueued(worker: SearchIndexingWorker, args: args)
+        assert :ok = perform_job(SearchIndexingWorker, args)
+
+        assert_not_called(Meilisearch.meili_put(:_))
+      end)
+
+      history = call_history(Meilisearch)
+      assert Enum.count(history) == 2
+    end
+
+    test "deletes posts from index when deleted locally" do
+      user = insert(:user)
+
+      mock_global(fn
+        %{method: :put, url: "http://127.0.0.1:7700/indexes/objects/documents", body: body} ->
+          assert match?(
+                   [%{"content" => "guys i just don&#39;t wanna leave the swamp"}],
+                   Jason.decode!(body)
+                 )
+
+          json(%{updateId: 1})
+
+        %{method: :delete, url: "http://127.0.0.1:7700/indexes/objects/documents/" <> id} ->
+          assert String.length(id) > 1
+          json(%{updateId: 2})
+      end)
+
+      {:ok, activity} =
+        CommonAPI.post(user, %{
+          status: "guys i just don't wanna leave the swamp",
+          visibility: "public"
+        })
+
+      args = %{"op" => "add_to_index", "activity" => activity.id}
+      assert_enqueued(worker: SearchIndexingWorker, args: args)
+      assert :ok = perform_job(SearchIndexingWorker, args)
+
+      {:ok, _} = CommonAPI.delete(activity.id, user)
+
+      delete_args = %{"op" => "remove_from_index", "object" => activity.object.id}
+      assert_enqueued(worker: SearchIndexingWorker, args: delete_args)
+      assert :ok = perform_job(SearchIndexingWorker, delete_args)
+
+      assert_called(Meilisearch.remove_from_index(:_))
+    end
+  end
+end
index 7c30f39add281430906c5ecc93e788fd8c5b302e..756281a461724ac12dd86be39db5bbc8ac7d786d 100644 (file)
@@ -5,6 +5,7 @@
 defmodule Pleroma.UserTest do
   alias Pleroma.Activity
   alias Pleroma.Builders.UserBuilder
+  alias Pleroma.Notification
   alias Pleroma.Object
   alias Pleroma.Repo
   alias Pleroma.Tests.ObanHelpers
@@ -2153,6 +2154,26 @@ defmodule Pleroma.UserTest do
       assert {:ok, user} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}")
       assert %User{bio: "test-bio"} = User.get_cached_by_ap_id(user.ap_id)
     end
+
+    test "removes report notifs when user isn't superuser any more" do
+      report_activity = insert(:report_activity)
+      user = insert(:user, is_moderator: true, is_admin: true)
+      {:ok, _} = Notification.create_notifications(report_activity)
+
+      assert [%Pleroma.Notification{type: "pleroma:report"}] = Notification.for_user(user)
+
+      {:ok, user} = user |> User.admin_api_update(%{is_moderator: false})
+      # is still superuser because still admin
+      assert [%Pleroma.Notification{type: "pleroma:report"}] = Notification.for_user(user)
+
+      {:ok, user} = user |> User.admin_api_update(%{is_moderator: true, is_admin: false})
+      # is still superuser because still moderator
+      assert [%Pleroma.Notification{type: "pleroma:report"}] = Notification.for_user(user)
+
+      {:ok, user} = user |> User.admin_api_update(%{is_moderator: false})
+      # is not a superuser any more
+      assert [] = Notification.for_user(user)
+    end
   end
 
   describe "following/followers synchronization" do
index d5af3a9b652d1c93481d1f7f5fd2e9929b3a48ef..14a6ae52b1eefee345cb88e1bebd7eefccb42e06 100644 (file)
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicyTest do
   use Pleroma.DataCase, async: true
   import Pleroma.Factory
 
+  alias Pleroma.User
   alias Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy
 
   describe "blocking based on attributes" do
@@ -38,21 +39,55 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicyTest do
 
       assert {:reject, "[AntiFollowbotPolicy]" <> _} = AntiFollowbotPolicy.filter(message)
     end
+
+    test "matches followbots by actor_type" do
+      actor = insert(:user, %{actor_type: "Service"})
+      target = insert(:user)
+
+      message = %{
+        "@context" => "https://www.w3.org/ns/activitystreams",
+        "type" => "Follow",
+        "actor" => actor.ap_id,
+        "object" => target.ap_id,
+        "id" => "https://example.com/activities/1234"
+      }
+
+      assert {:reject, "[AntiFollowbotPolicy]" <> _} = AntiFollowbotPolicy.filter(message)
+    end
   end
 
-  test "it allows non-followbots" do
-    actor = insert(:user)
-    target = insert(:user)
+  describe "it allows" do
+    test "non-followbots" do
+      actor = insert(:user)
+      target = insert(:user)
 
-    message = %{
-      "@context" => "https://www.w3.org/ns/activitystreams",
-      "type" => "Follow",
-      "actor" => actor.ap_id,
-      "object" => target.ap_id,
-      "id" => "https://example.com/activities/1234"
-    }
+      message = %{
+        "@context" => "https://www.w3.org/ns/activitystreams",
+        "type" => "Follow",
+        "actor" => actor.ap_id,
+        "object" => target.ap_id,
+        "id" => "https://example.com/activities/1234"
+      }
 
-    {:ok, _} = AntiFollowbotPolicy.filter(message)
+      {:ok, _} = AntiFollowbotPolicy.filter(message)
+    end
+
+    test "bots if the target follows the bots" do
+      actor = insert(:user, %{actor_type: "Service"})
+      target = insert(:user)
+
+      User.follow(target, actor)
+
+      message = %{
+        "@context" => "https://www.w3.org/ns/activitystreams",
+        "type" => "Follow",
+        "actor" => actor.ap_id,
+        "object" => target.ap_id,
+        "id" => "https://example.com/activities/1234"
+      }
+
+      {:ok, _} = AntiFollowbotPolicy.filter(message)
+    end
   end
 
   test "it gracefully handles nil display names" do
index 1b37e4c26dd635e196cda3984cea394cf3692a1b..b0a7e8993ad0a0eafc05fb412e3214b9956894a1 100644 (file)
@@ -60,7 +60,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicyTest do
            |> File.exists?()
   end
 
-  test "reject shortcode", %{message: message} do
+  test "reject regex shortcode", %{message: message} do
     refute "firedfox" in installed()
 
     clear_config(:mrf_steal_emoji,
@@ -74,6 +74,20 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicyTest do
     refute "firedfox" in installed()
   end
 
+  test "reject string shortcode", %{message: message} do
+    refute "firedfox" in installed()
+
+    clear_config(:mrf_steal_emoji,
+      hosts: ["example.org"],
+      size_limit: 284_468,
+      rejected_shortcodes: ["firedfox"]
+    )
+
+    assert {:ok, _message} = StealEmojiPolicy.filter(message)
+
+    refute "firedfox" in installed()
+  end
+
   test "reject if size is above the limit", %{message: message} do
     refute "firedfox" in installed()
 
index 2bd1e46c131376f719a5c3ede92940c4e99779cf..717a704d407649e0efe21d384953ed0d78902283 100644 (file)
@@ -52,5 +52,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest
       %{valid?: true, changes: %{replies: ["https://bookwyrm.com/user/TestUser/status/18"]}} =
         ArticleNotePageValidator.cast_and_validate(note)
     end
+
+    test "a note with an attachment should work", _ do
+      insert(:user, %{ap_id: "https://owncast.localhost.localdomain/federation/user/streamer"})
+
+      note =
+        "test/fixtures/owncast-note-with-attachment.json"
+        |> File.read!()
+        |> Jason.decode!()
+
+      %{valid?: true} = ArticleNotePageValidator.cast_and_validate(note)
+    end
   end
 end
index 30fd5651b43a0039a388d111ce7deec455a8e415..e606fa3d11fbc3353ab797b29203f2e41d50b0aa 100644 (file)
@@ -28,7 +28,6 @@ defmodule Pleroma.Web.ActivityPub.PipelineTest do
       SideEffectsMock
       |> expect(:handle, fn o, m -> {:ok, o, m} end)
       |> expect(:handle_after_transaction, fn m -> m end)
-      |> expect(:handle_after_transaction, fn m -> m end)
 
       :ok
     end
index 642b05f3f7623e3f0fe651b636801a771bd8b7cc..2d526527b2ee8967730b813e80578484b4efd5f8 100644 (file)
@@ -369,7 +369,8 @@ defmodule Pleroma.Web.AdminAPI.ReportControllerTest do
       refute is_nil(note)
 
       assert note["user"]["nickname"] == admin.nickname
-      assert note["content"] == "this is disgusting!"
+      # We use '=~' because the order of the notes isn't guaranteed
+      assert note["content"] =~ "this is disgusting"
       assert note["created_at"]
       assert response["total"] == 1
     end
diff --git a/test/pleroma/web/gettext_test.exs b/test/pleroma/web/gettext_test.exs
new file mode 100644 (file)
index 0000000..e186f1a
--- /dev/null
@@ -0,0 +1,173 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.GettextTest do
+  use ExUnit.Case
+
+  require Pleroma.Web.Gettext
+
+  test "put_locales/1: set the first in the list to Gettext's locale" do
+    Pleroma.Web.Gettext.put_locales(["zh_Hans", "en_test"])
+
+    assert "zh_Hans" == Gettext.get_locale(Pleroma.Web.Gettext)
+  end
+
+  test "with_locales/2: reset locale on exit" do
+    old_first_locale = Gettext.get_locale(Pleroma.Web.Gettext)
+    old_locales = Pleroma.Web.Gettext.get_locales()
+
+    Pleroma.Web.Gettext.with_locales ["zh_Hans", "en_test"] do
+      assert "zh_Hans" == Gettext.get_locale(Pleroma.Web.Gettext)
+      assert ["zh_Hans", "en_test"] == Pleroma.Web.Gettext.get_locales()
+    end
+
+    assert old_first_locale == Gettext.get_locale(Pleroma.Web.Gettext)
+    assert old_locales == Pleroma.Web.Gettext.get_locales()
+  end
+
+  describe "handle_missing_translation/5" do
+    test "fallback to next locale if some translation is not available" do
+      Pleroma.Web.Gettext.with_locales ["x_unsupported", "en_test"] do
+        assert "xxYour account is awaiting approvalxx" ==
+                 Pleroma.Web.Gettext.dpgettext(
+                   "static_pages",
+                   "approval pending email subject",
+                   "Your account is awaiting approval"
+                 )
+      end
+    end
+
+    test "putting en locale at the front should not make gettext fallback unexpectedly" do
+      Pleroma.Web.Gettext.with_locales ["en", "en_test"] do
+        assert "Your account is awaiting approval" ==
+                 Pleroma.Web.Gettext.dpgettext(
+                   "static_pages",
+                   "approval pending email subject",
+                   "Your account is awaiting approval"
+                 )
+      end
+    end
+
+    test "duplicated locale in list should not result in infinite loops" do
+      Pleroma.Web.Gettext.with_locales ["x_unsupported", "x_unsupported", "en_test"] do
+        assert "xxYour account is awaiting approvalxx" ==
+                 Pleroma.Web.Gettext.dpgettext(
+                   "static_pages",
+                   "approval pending email subject",
+                   "Your account is awaiting approval"
+                 )
+      end
+    end
+
+    test "direct interpolation" do
+      Pleroma.Web.Gettext.with_locales ["en_test"] do
+        assert "xxYour digest from some instancexx" ==
+                 Pleroma.Web.Gettext.dpgettext(
+                   "static_pages",
+                   "digest email subject",
+                   "Your digest from %{instance_name}",
+                   instance_name: "some instance"
+                 )
+      end
+    end
+
+    test "fallback with interpolation" do
+      Pleroma.Web.Gettext.with_locales ["x_unsupported", "en_test"] do
+        assert "xxYour digest from some instancexx" ==
+                 Pleroma.Web.Gettext.dpgettext(
+                   "static_pages",
+                   "digest email subject",
+                   "Your digest from %{instance_name}",
+                   instance_name: "some instance"
+                 )
+      end
+    end
+
+    test "fallback to msgid" do
+      Pleroma.Web.Gettext.with_locales ["x_unsupported"] do
+        assert "Your digest from some instance" ==
+                 Pleroma.Web.Gettext.dpgettext(
+                   "static_pages",
+                   "digest email subject",
+                   "Your digest from %{instance_name}",
+                   instance_name: "some instance"
+                 )
+      end
+    end
+  end
+
+  describe "handle_missing_plural_translation/7" do
+    test "direct interpolation" do
+      Pleroma.Web.Gettext.with_locales ["en_test"] do
+        assert "xx1 New Followerxx" ==
+                 Pleroma.Web.Gettext.dpngettext(
+                   "static_pages",
+                   "new followers count header",
+                   "%{count} New Follower",
+                   "%{count} New Followers",
+                   1,
+                   count: 1
+                 )
+
+        assert "xx5 New Followersxx" ==
+                 Pleroma.Web.Gettext.dpngettext(
+                   "static_pages",
+                   "new followers count header",
+                   "%{count} New Follower",
+                   "%{count} New Followers",
+                   5,
+                   count: 5
+                 )
+      end
+    end
+
+    test "fallback with interpolation" do
+      Pleroma.Web.Gettext.with_locales ["x_unsupported", "en_test"] do
+        assert "xx1 New Followerxx" ==
+                 Pleroma.Web.Gettext.dpngettext(
+                   "static_pages",
+                   "new followers count header",
+                   "%{count} New Follower",
+                   "%{count} New Followers",
+                   1,
+                   count: 1
+                 )
+
+        assert "xx5 New Followersxx" ==
+                 Pleroma.Web.Gettext.dpngettext(
+                   "static_pages",
+                   "new followers count header",
+                   "%{count} New Follower",
+                   "%{count} New Followers",
+                   5,
+                   count: 5
+                 )
+      end
+    end
+
+    test "fallback to msgid" do
+      Pleroma.Web.Gettext.with_locales ["x_unsupported"] do
+        assert "1 New Follower" ==
+                 Pleroma.Web.Gettext.dpngettext(
+                   "static_pages",
+                   "new followers count header",
+                   "%{count} New Follower",
+                   "%{count} New Followers",
+                   1,
+                   count: 1
+                 )
+
+        assert "5 New Followers" ==
+                 Pleroma.Web.Gettext.dpngettext(
+                   "static_pages",
+                   "new followers count header",
+                   "%{count} New Follower",
+                   "%{count} New Followers",
+                   5,
+                   count: 5
+                 )
+      end
+    end
+  end
+end
index 374e2048a732114a60c521bcd1a2073d24201a83..de38a9798d18d47c8360f3407576cdb8d9857ca6 100644 (file)
@@ -11,6 +11,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
   alias Pleroma.Web.ActivityPub.InternalFetchActor
   alias Pleroma.Web.CommonAPI
   alias Pleroma.Web.OAuth.Token
+  alias Pleroma.Web.Plugs.SetLocalePlug
 
   import Pleroma.Factory
 
@@ -1586,6 +1587,75 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
     end
   end
 
+  describe "create account with language" do
+    setup %{conn: conn} do
+      app_token = insert(:oauth_token, user: nil)
+
+      conn =
+        conn
+        |> put_req_header("authorization", "Bearer " <> app_token.token)
+        |> put_req_header("content-type", "multipart/form-data")
+        |> put_req_cookie(SetLocalePlug.frontend_language_cookie_name(), "zh-Hans")
+        |> SetLocalePlug.call([])
+
+      [conn: conn]
+    end
+
+    test "creates an account with language parameter", %{conn: conn} do
+      params = %{
+        username: "foo",
+        email: "foo@example.org",
+        password: "dupa.8",
+        agreement: true,
+        language: "ru"
+      }
+
+      res =
+        conn
+        |> post("/api/v1/accounts", params)
+
+      assert json_response_and_validate_schema(res, 200)
+
+      assert %{language: "ru"} = Pleroma.User.get_by_nickname("foo")
+    end
+
+    test "language parameter should be normalized", %{conn: conn} do
+      params = %{
+        username: "foo",
+        email: "foo@example.org",
+        password: "dupa.8",
+        agreement: true,
+        language: "ru-RU"
+      }
+
+      res =
+        conn
+        |> post("/api/v1/accounts", params)
+
+      assert json_response_and_validate_schema(res, 200)
+
+      assert %{language: "ru_RU"} = Pleroma.User.get_by_nickname("foo")
+    end
+
+    test "createing an account without language parameter should fallback to cookie/header language",
+         %{conn: conn} do
+      params = %{
+        username: "foo2",
+        email: "foo2@example.org",
+        password: "dupa.8",
+        agreement: true
+      }
+
+      res =
+        conn
+        |> post("/api/v1/accounts", params)
+
+      assert json_response_and_validate_schema(res, 200)
+
+      assert %{language: "zh_Hans"} = Pleroma.User.get_by_nickname("foo2")
+    end
+  end
+
   describe "GET /api/v1/accounts/:id/lists - account_lists" do
     test "returns lists to which the account belongs" do
       %{user: user, conn: conn} = oauth_access(["read:lists"])
index ed66d370ab3fdc59ee5cf49a1b9798d04e39a20e..3e0660031ed3d5e4b195a116003ab46cd6fc3d07 100644 (file)
@@ -1810,6 +1810,39 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
            } = response
   end
 
+  test "context when restrict_unauthenticated is on" do
+    user = insert(:user)
+    remote_user = insert(:user, local: false)
+
+    {:ok, %{id: id1}} = CommonAPI.post(user, %{status: "1"})
+    {:ok, %{id: id2}} = CommonAPI.post(user, %{status: "2", in_reply_to_status_id: id1})
+
+    {:ok, %{id: id3}} =
+      CommonAPI.post(remote_user, %{status: "3", in_reply_to_status_id: id2, local: false})
+
+    response =
+      build_conn()
+      |> get("/api/v1/statuses/#{id2}/context")
+      |> json_response_and_validate_schema(:ok)
+
+    assert %{
+             "ancestors" => [%{"id" => ^id1}],
+             "descendants" => [%{"id" => ^id3}]
+           } = response
+
+    clear_config([:restrict_unauthenticated, :activities, :local], true)
+
+    response =
+      build_conn()
+      |> get("/api/v1/statuses/#{id2}/context")
+      |> json_response_and_validate_schema(:ok)
+
+    assert %{
+             "ancestors" => [],
+             "descendants" => []
+           } = response
+  end
+
   test "favorites paginate correctly" do
     %{user: user, conn: conn} = oauth_access(["read:favourites"])
     other_user = insert(:user)
index a5223b0a53268d31c1b1213602767c9c0c33afd1..3c5ca07ae4468c7b5da504cc67d64991658750b4 100644 (file)
@@ -51,6 +51,6 @@ defmodule Pleroma.Web.OAuth.AppTest do
       insert(:oauth_app, user_id: user.id)
     ]
 
-    assert App.get_user_apps(user) == apps
+    assert Enum.sort(App.get_user_apps(user)) == Enum.sort(apps)
   end
 end
index 5261e67aec64b23a49f4f6c29af559273bcac7da..f9d34bbe48ac62beb76eabfd495150ceadac59b4 100644 (file)
@@ -16,7 +16,7 @@ defmodule Pleroma.Web.Plugs.SetLocalePlugTest do
       |> SetLocalePlug.call([])
 
     assert "en" == Gettext.get_locale()
-    assert %{locale: "en"} == conn.assigns
+    assert %{locale: "en"} = conn.assigns
   end
 
   test "use supported locale from `accept-language`" do
@@ -30,7 +30,125 @@ defmodule Pleroma.Web.Plugs.SetLocalePlugTest do
       |> SetLocalePlug.call([])
 
     assert "ru" == Gettext.get_locale()
-    assert %{locale: "ru"} == conn.assigns
+    assert %{locale: "ru"} = conn.assigns
+  end
+
+  test "fallback to the general language if a variant is not supported" do
+    conn =
+      :get
+      |> conn("/cofe")
+      |> Conn.put_req_header(
+        "accept-language",
+        "ru-CA;q=0.9, en;q=0.8, *;q=0.5"
+      )
+      |> SetLocalePlug.call([])
+
+    assert "ru" == Gettext.get_locale()
+    assert %{locale: "ru"} = conn.assigns
+  end
+
+  test "use supported locale with specifiers from `accept-language`" do
+    conn =
+      :get
+      |> conn("/cofe")
+      |> Conn.put_req_header(
+        "accept-language",
+        "zh-Hans;q=0.9, en;q=0.8, *;q=0.5"
+      )
+      |> SetLocalePlug.call([])
+
+    assert "zh_Hans" == Gettext.get_locale()
+    assert %{locale: "zh_Hans"} = conn.assigns
+  end
+
+  test "it assigns all supported locales" do
+    conn =
+      :get
+      |> conn("/cofe")
+      |> Conn.put_req_header(
+        "accept-language",
+        "ru, fr-CH, fr;q=0.9, en;q=0.8, x-unsupported;q=0.8, *;q=0.5"
+      )
+      |> SetLocalePlug.call([])
+
+    assert "ru" == Gettext.get_locale()
+    assert %{locale: "ru", locales: ["ru", "fr", "en"]} = conn.assigns
+  end
+
+  test "it assigns all supported locales in cookie" do
+    conn =
+      :get
+      |> conn("/cofe")
+      |> put_req_cookie(SetLocalePlug.frontend_language_cookie_name(), "zh-Hans,uk,zh-Hant")
+      |> Conn.put_req_header(
+        "accept-language",
+        "ru, fr-CH, fr;q=0.9, en;q=0.8, x-unsupported;q=0.8, *;q=0.5"
+      )
+      |> SetLocalePlug.call([])
+
+    assert "zh_Hans" == Gettext.get_locale()
+
+    assert %{locale: "zh_Hans", locales: ["zh_Hans", "uk", "zh_Hant", "ru", "fr", "en"]} =
+             conn.assigns
+  end
+
+  test "fallback to some variant of the language if the unqualified language is not supported" do
+    conn =
+      :get
+      |> conn("/cofe")
+      |> Conn.put_req_header(
+        "accept-language",
+        "zh;q=0.9, en;q=0.8, *;q=0.5"
+      )
+      |> SetLocalePlug.call([])
+
+    assert "zh_" <> _ = Gettext.get_locale()
+    assert %{locale: "zh_" <> _} = conn.assigns
+  end
+
+  test "use supported locale from cookie" do
+    conn =
+      :get
+      |> conn("/cofe")
+      |> put_req_cookie(SetLocalePlug.frontend_language_cookie_name(), "zh-Hans")
+      |> Conn.put_req_header(
+        "accept-language",
+        "ru, fr-CH, fr;q=0.9, en;q=0.8, *;q=0.5"
+      )
+      |> SetLocalePlug.call([])
+
+    assert "zh_Hans" == Gettext.get_locale()
+    assert %{locale: "zh_Hans"} = conn.assigns
+  end
+
+  test "fallback to supported locale from `accept-language` if locale in cookie not supported" do
+    conn =
+      :get
+      |> conn("/cofe")
+      |> put_req_cookie(SetLocalePlug.frontend_language_cookie_name(), "x-nonexist")
+      |> Conn.put_req_header(
+        "accept-language",
+        "ru, fr-CH, fr;q=0.9, en;q=0.8, *;q=0.5"
+      )
+      |> SetLocalePlug.call([])
+
+    assert "ru" == Gettext.get_locale()
+    assert %{locale: "ru"} = conn.assigns
+  end
+
+  test "fallback to default if nothing is supported" do
+    conn =
+      :get
+      |> conn("/cofe")
+      |> put_req_cookie(SetLocalePlug.frontend_language_cookie_name(), "x-nonexist")
+      |> Conn.put_req_header(
+        "accept-language",
+        "x-nonexist"
+      )
+      |> SetLocalePlug.call([])
+
+    assert "en" == Gettext.get_locale()
+    assert %{locale: "en"} = conn.assigns
   end
 
   test "use default locale if locale from `accept-language` is not supported" do
@@ -41,6 +159,6 @@ defmodule Pleroma.Web.Plugs.SetLocalePlugTest do
       |> SetLocalePlug.call([])
 
     assert "en" == Gettext.get_locale()
-    assert %{locale: "en"} == conn.assigns
+    assert %{locale: "en"} = conn.assigns
   end
 end
diff --git a/test/support/elasticsearch_mock.ex b/test/support/elasticsearch_mock.ex
new file mode 100644 (file)
index 0000000..6e203f2
--- /dev/null
@@ -0,0 +1,14 @@
+defmodule Pleroma.ElasticsearchMock do
+  @behaviour Elasticsearch.API
+
+  @impl true
+  def request(_config, :get, "/posts/1", _data, _opts) do
+    {:ok,
+     %HTTPoison.Response{
+       status_code: 404,
+       body: %{
+         "status" => "not_found"
+       }
+     }}
+  end
+end