diff --git a/.woodpecker/.release.yml b/.woodpecker/.release.yml index 335f3c8e8..28043aa65 100644 --- a/.woodpecker/.release.yml +++ b/.woodpecker/.release.yml @@ -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 diff --git a/.woodpecker/.test.yml b/.woodpecker/.test.yml index cef743643..6724d363d 100644 --- a/.woodpecker/.test.yml +++ b/.woodpecker/.test.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 9513a54fa..2119c8e21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/config/config.exs b/config/config.exs index 15bcfb2b9..8de699e3c 100644 --- a/config/config.exs +++ b/config/config.exs @@ -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, @@ -846,17 +848,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, Pleroma.Search, module: Pleroma.Search.DatabaseSearch -config :pleroma, :telemetry, - slow_queries_logging: [ - enabled: false, - min_duration: 500_000, - exclude_sources: [nil, "oban_jobs"] - ] +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. diff --git a/config/description.exs b/config/description.exs index 4ca79ad51..9401bed5c 100644 --- a/config/description.exs +++ b/config/description.exs @@ -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] + } + ] + } + ] + } + ] } ] diff --git a/config/test.exs b/config/test.exs index a5bf3a4d1..7fbababdf 100644 --- a/config/test.exs +++ b/config/test.exs @@ -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 diff --git a/docs/administration/updating.md b/docs/administration/updating.md index ef2c9218c..01d3b9b0e 100644 --- a/docs/administration/updating.md +++ b/docs/administration/updating.md @@ -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. diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 3b8f8cc52..50281f451 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -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 index 000000000..e1f23b505 --- /dev/null +++ b/docs/configuration/search.md @@ -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 + ``` + +=== "From Source" + ```sh + mix pleroma.search.meilisearch show-keys + ``` + +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 +``` diff --git a/docs/development/API/differences_in_mastoapi_responses.md b/docs/development/API/differences_in_mastoapi_responses.md index 518aca114..def718b95 100644 --- a/docs/development/API/differences_in_mastoapi_responses.md +++ b/docs/development/API/differences_in_mastoapi_responses.md @@ -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 diff --git a/lib/mix/pleroma.ex b/lib/mix/pleroma.ex index 2b6c7d6bb..02c40850a 100644 --- a/lib/mix/pleroma.ex +++ b/lib/mix/pleroma.ex @@ -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 diff --git a/lib/mix/tasks/pleroma/search.ex b/lib/mix/tasks/pleroma/search.ex index 1fd880eab..102bc5b63 100644 --- a/lib/mix/tasks/pleroma/search.ex +++ b/lib/mix/tasks/pleroma/search.ex @@ -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 index 000000000..d4a83c3cd --- /dev/null +++ b/lib/mix/tasks/pleroma/search/meilisearch.ex @@ -0,0 +1,144 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# 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 diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index 4106feef6..abfe778d2 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -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 diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index d37454d2c..b709e737b 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -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 index a028c6fad..000000000 --- a/lib/pleroma/elasticsearch/document_mappings/activity.ex +++ /dev/null @@ -1,19 +0,0 @@ -# Akkoma: A lightweight social networking server -# Copyright © 2022-2022 Akkoma Authors -# 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 index 7391983f6..000000000 --- a/lib/pleroma/elasticsearch/document_mappings/hashtag.ex +++ /dev/null @@ -1,21 +0,0 @@ -# Akkoma: A lightweight social networking server -# Copyright © 2022-2022 Akkoma Authors -# 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 index d5cfca656..000000000 --- a/lib/pleroma/elasticsearch/document_mappings/user.ex +++ /dev/null @@ -1,17 +0,0 @@ -# Akkoma: A lightweight social networking server -# Copyright © 2022-2022 Akkoma Authors -# 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 index 98c88a7c7..000000000 --- a/lib/pleroma/elasticsearch/store.ex +++ /dev/null @@ -1,256 +0,0 @@ -# Akkoma: A lightweight social networking server -# Copyright © 2022-2022 Akkoma Authors -# 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 diff --git a/lib/pleroma/emails/user_email.ex b/lib/pleroma/emails/user_email.ex index e38c681ba..24adfabd7 100644 --- a/lib/pleroma/emails/user_email.ex +++ b/lib/pleroma/emails/user_email.ex @@ -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) + Gettext.with_locale_or_default user.language do + password_reset_url = Router.Helpers.reset_password_url(Endpoint, :reset, token) - html_body = """ -

Reset your password at #{instance_name()}

-

Someone has requested password change for your account at #{instance_name()}.

-

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

-

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

- """ + html_body = + Gettext.dpgettext( + "static_pages", + "password reset email body", + """ +

Reset your password at %{instance_name}

+

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

+

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

+

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

+ """, + instance_name: instance_name(), + password_reset_url: password_reset_url + ) - new() - |> to(recipient(user)) - |> from(sender()) - |> subject("Password reset") - |> html_body(html_body) + 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", + """ +

You are invited to %{instance_name}

+

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

+

Click the following link to register: accept invitation.

+ """, + instance_name: instance_name(), + inviter_name: user.name, + registration_url: registration_url + ) + + 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 = """ -

You are invited to #{instance_name()}

-

#{user.name} invites you to join #{instance_name()}, an instance of Pleroma federated social networking platform.

-

Click the following link to register: accept invitation.

- """ - - new() - |> to(recipient(to_email, to_name)) - |> from(sender()) - |> subject("Invitation to #{instance_name()}") - |> html_body(html_body) + |> 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", + """ +

Thank you for registering on %{instance_name}

+

Email confirmation is required to activate the account.

+

Please click the following link to activate your account.

+ """, + instance_name: instance_name(), + confirmation_url: confirmation_url + ) + + new() + |> to(recipient(user)) + |> from(sender()) + |> subject( + Gettext.dpgettext( + "static_pages", + "confirmation email subject", + "%{instance_name} account confirmation", + instance_name: instance_name() + ) ) - - html_body = """ -

Thank you for registering on #{instance_name()}

-

Email confirmation is required to activate the account.

-

Please click the following link to activate your account.

- """ - - new() - |> to(recipient(user)) - |> from(sender()) - |> subject("#{instance_name()} account confirmation") - |> html_body(html_body) + |> html_body(html_body) + end end def approval_pending_email(user) do - html_body = """ -

Awaiting Approval

-

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

- """ + Gettext.with_locale_or_default user.language do + html_body = + Gettext.dpgettext( + "static_pages", + "approval pending email body", + """ +

Awaiting Approval

+

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

+ """, + instance_name: instance_name() + ) - new() - |> to(recipient(user)) - |> from(sender()) - |> subject("Your account is awaiting approval") - |> html_body(html_body) + 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 = """ -

Hello @#{user.nickname},

-

Your account at #{instance_name()} has been registered successfully.

-

No further action is required to activate your account.

- """ + Gettext.with_locale_or_default user.language do + html_body = + Gettext.dpgettext( + "static_pages", + "successful registration email body", + """ +

Hello @%{nickname},

+

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

+

No further action is required to activate your account.

+ """, + nickname: user.nickname, + instance_name: instance_name() + ) - new() - |> to(recipient(user)) - |> from(sender()) - |> subject("Account registered on #{instance_name()}") - |> html_body(html_body) + 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) + 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) + 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) + 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) + %{ + 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) + 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) + 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]) + 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 - } + 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 + 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)) + 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 - """ -

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

-

#{download_url}

- """ - else - admin = Pleroma.Repo.get(User, admin_user_id) + Gettext.with_locale_or_default user.language do + download_url = Pleroma.Web.PleromaAPI.BackupView.download_url(backup) - """ -

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

-

#{download_url}

- """ - end + html_body = + if is_nil(admin_user_id) do + Gettext.dpgettext( + "static_pages", + "account archive email body - self-requested", + """ +

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

+

%{download_url}

+ """, + download_url: download_url + ) + else + admin = Pleroma.Repo.get(User, admin_user_id) - new() - |> to(recipient(user)) - |> from(sender()) - |> subject("Your account archive is ready") - |> html_body(html_body) + Gettext.dpgettext( + "static_pages", + "account archive email body - admin requested", + """ +

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

+

%{download_url}

+ """, + 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 diff --git a/lib/pleroma/emoji-test.txt b/lib/pleroma/emoji-test.txt index d3c6d12bd..dd5493366 100644 --- a/lib/pleroma/emoji-test.txt +++ b/lib/pleroma/emoji-test.txt @@ -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 @@ -68,10 +69,13 @@ 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 @@ -105,7 +110,7 @@ 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 @@ -121,6 +126,7 @@ # 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 @@ -130,6 +136,7 @@ 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 @@ -232,8 +239,8 @@ 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 @@ -269,6 +276,30 @@ 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 @@ -302,6 +333,12 @@ 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 @@ -359,6 +396,12 @@ 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 @@ -411,6 +454,12 @@ 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 @@ -424,6 +473,31 @@ 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 @@ -501,6 +575,7 @@ 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 @@ -1472,6 +1547,12 @@ 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 @@ -1592,6 +1673,18 @@ 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 @@ -1862,6 +1955,7 @@ 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 @@ -3168,8 +3262,8 @@ 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 @@ -3304,6 +3398,7 @@ 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 @@ -3329,6 +3424,7 @@ 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 @@ -3353,9 +3449,11 @@ 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 @@ -3396,6 +3494,7 @@ 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 @@ -3491,6 +3590,7 @@ 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 @@ -3504,10 +3604,11 @@ 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 @@ -3597,6 +3698,7 @@ 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 @@ -3652,6 +3754,7 @@ 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 @@ -3660,6 +3763,7 @@ # 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 @@ -3797,8 +3901,8 @@ 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 @@ -3874,6 +3978,7 @@ 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 @@ -3882,6 +3987,7 @@ 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 @@ -3907,8 +4013,8 @@ 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 @@ -4009,6 +4115,7 @@ # 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 @@ -4207,7 +4314,9 @@ 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 @@ -4232,6 +4341,7 @@ 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 @@ -4246,9 +4356,10 @@ 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 @@ -4409,6 +4520,7 @@ 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 @@ -4581,8 +4693,8 @@ 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 @@ -4871,7 +4983,7 @@ # Flags subtotal: 275 w/o modifiers # Status Counts -# fully-qualified : 3512 +# fully-qualified : 3624 # minimally-qualified : 817 # unqualified : 252 # component : 9 diff --git a/lib/pleroma/hashtag.ex b/lib/pleroma/hashtag.ex index cdbfeab02..53e2e9c89 100644 --- a/lib/pleroma/hashtag.ex +++ b/lib/pleroma/hashtag.ex @@ -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} diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 9e0ce0329..2ab09495d 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -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) diff --git a/lib/pleroma/search.ex b/lib/pleroma/search.ex index 99bce632c..3b266e59b 100644 --- a/lib/pleroma/search.ex +++ b/lib/pleroma/search.ex @@ -1,12 +1,17 @@ defmodule Pleroma.Search do - @type search_map :: %{ - statuses: [map], - accounts: [map], - hashtags: [map] - } + alias Pleroma.Workers.SearchIndexingWorker - @doc """ - Searches for stuff - """ - @callback search(map, map, keyword) :: search_map + 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 index 3cbe2207a..000000000 --- a/lib/pleroma/search/builtin.ex +++ /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 diff --git a/lib/pleroma/activity/search.ex b/lib/pleroma/search/database_search.ex similarity index 92% rename from lib/pleroma/activity/search.ex rename to lib/pleroma/search/database_search.ex index 09671f621..3735a5fab 100644 --- a/lib/pleroma/activity/search.ex +++ b/lib/pleroma/search/database_search.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2021 Pleroma Authors # 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"]), diff --git a/lib/pleroma/search/elasticsearch.ex b/lib/pleroma/search/elasticsearch.ex index 76d2c3277..7c7ca82c8 100644 --- a/lib/pleroma/search/elasticsearch.ex +++ b/lib/pleroma/search/elasticsearch.ex @@ -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 index 000000000..4f76c4ebc --- /dev/null +++ b/lib/pleroma/search/elasticsearch/cluster.ex @@ -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 index 000000000..3a84e991b --- /dev/null +++ b/lib/pleroma/search/elasticsearch/document_mappings/activity.ex @@ -0,0 +1,61 @@ +# Akkoma: A lightweight social networking server +# Copyright © 2022-2022 Akkoma Authors +# 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 index 911dc651c..000000000 --- a/lib/pleroma/search/elasticsearch/hashtag_parser.ex +++ /dev/null @@ -1,34 +0,0 @@ -# Akkoma: A lightweight social networking server -# Copyright © 2022-2022 Akkoma Authors -# 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 index 000000000..895b76d7f --- /dev/null +++ b/lib/pleroma/search/elasticsearch/store.ex @@ -0,0 +1,52 @@ +# Akkoma: A lightweight social networking server +# Copyright © 2022-2022 Akkoma Authors +# 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 index 4176c6141..000000000 --- a/lib/pleroma/search/elasticsearch/user_paser.ex +++ /dev/null @@ -1,57 +0,0 @@ -# Akkoma: A lightweight social networking server -# Copyright © 2022-2022 Akkoma Authors -# 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 index 000000000..3db65f261 --- /dev/null +++ b/lib/pleroma/search/meilisearch.ex @@ -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 index 000000000..ed6bfd329 --- /dev/null +++ b/lib/pleroma/search/search_backend.ex @@ -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 diff --git a/lib/pleroma/telemetry/logger.ex b/lib/pleroma/telemetry/logger.ex index 35e245237..50f7fcf2a 100644 --- a/lib/pleroma/telemetry/logger.ex +++ b/lib/pleroma/telemetry/logger.ex @@ -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 diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index efe9ec5d6..dc6c661ea 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -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 diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 756096952..e6548a818 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -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 -> diff --git a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex index 851e95d22..627f52168 100644 --- a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex @@ -24,7 +24,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}"} diff --git a/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex index 0dd415732..61e95b49a 100644 --- a/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex @@ -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, diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex index 815995895..a9f395c5e 100644 --- a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex @@ -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 diff --git a/lib/pleroma/web/activity_pub/pipeline.ex b/lib/pleroma/web/activity_pub/pipeline.ex index 214647dbf..d4e507287 100644 --- a/lib/pleroma/web/activity_pub/pipeline.ex +++ b/lib/pleroma/web/activity_pub/pipeline.ex @@ -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} -> diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 9c2f89e72..e2371b693 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2021 Pleroma Authors +# Copyright © 2017-2022 Pleroma Authors # 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() diff --git a/lib/pleroma/web/activity_pub/side_effects/handling.ex b/lib/pleroma/web/activity_pub/side_effects/handling.ex index a82305155..eb012f576 100644 --- a/lib/pleroma/web/activity_pub/side_effects/handling.ex +++ b/lib/pleroma/web/activity_pub/side_effects/handling.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2021 Pleroma Authors +# Copyright © 2017-2022 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.SideEffects.Handling do diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index f5304d7d6..bdbae9b74 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -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: %{ diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex index 92afd5cb6..856fa95b9 100644 --- a/lib/pleroma/web/common_api.ex +++ b/lib/pleroma/web/common_api.ex @@ -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 diff --git a/lib/pleroma/web/feed/feed_view.ex b/lib/pleroma/web/feed/feed_view.ex index c0fb35e01..52771205e 100644 --- a/lib/pleroma/web/feed/feed_view.ex +++ b/lib/pleroma/web/feed/feed_view.ex @@ -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 diff --git a/lib/pleroma/web/gettext.ex b/lib/pleroma/web/gettext.ex index c0ca4d0e9..7afcd38f0 100644 --- a/lib/pleroma/web/gettext.ex +++ b/lib/pleroma/web/gettext.ex @@ -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 diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index a307807a9..83cebbb96 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -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: # diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index 86ad388fd..e4acba226 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -1,13 +1,16 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2021 Pleroma Authors +# Copyright © 2017-2022 Pleroma Authors # 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) + 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" => []} - search_provider = Pleroma.Config.get([:search, :provider]) - json(conn, search_provider.search(conn, params, options)) + 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) diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index 2eff4d9d0..60f4c44d7 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -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 diff --git a/lib/pleroma/web/o_auth/mfa_view.ex b/lib/pleroma/web/o_auth/mfa_view.ex index 3d473f29c..952c90efe 100644 --- a/lib/pleroma/web/o_auth/mfa_view.ex +++ b/lib/pleroma/web/o_auth/mfa_view.ex @@ -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 %{ diff --git a/lib/pleroma/web/o_auth/o_auth_view.ex b/lib/pleroma/web/o_auth/o_auth_view.ex index 1419c96a2..57a315705 100644 --- a/lib/pleroma/web/o_auth/o_auth_view.ex +++ b/lib/pleroma/web/o_auth/o_auth_view.ex @@ -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 diff --git a/lib/pleroma/web/plugs/set_locale_plug.ex b/lib/pleroma/web/plugs/set_locale_plug.ex index d77191cff..e78917199 100644 --- a/lib/pleroma/web/plugs/set_locale_plug.ex +++ b/lib/pleroma/web/plugs/set_locale_plug.ex @@ -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 diff --git a/lib/pleroma/web/templates/email/digest.html.eex b/lib/pleroma/web/templates/email/digest.html.eex index 60eceff22..1efc76e1a 100644 --- a/lib/pleroma/web/templates/email/digest.html.eex +++ b/lib/pleroma/web/templates/email/digest.html.eex @@ -160,7 +160,7 @@

Hey <%= @user.nickname %>, here is what you've missed!

+ 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) %>

@@ -382,7 +382,7 @@

<%= length(@followers) %> New Followers<%= Gettext.dpngettext("static_pages", "new followers count header", "%{count} New Follower", "%{count} New Followers", length(@followers), count: length(@followers)) %>

@@ -535,16 +535,16 @@ 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;">

- You have received this email because you have signed up to receive digest emails from <%= @instance %> Pleroma instance.

+ <%= raw Gettext.dpgettext("static_pages", "digest email sending reason", "You have received this email because you have signed up to receive digest emails from %{instance} Pleroma instance.", instance: safe_to_string(html_escape(@instance))) %>

 

- The email address you are subscribed as is <%= @user.email %>.

+ <%= raw Gettext.dpgettext("static_pages", "digest email receiver address", "The email address you are subscribed as is %{email}. ", color: safe_to_string(html_escape(@styling.link_color)), email: safe_to_string(html_escape(@user.email))) %>

- To unsubscribe, please go <%= link "here", style: "color: #{@styling.link_color};text-decoration: none;", to: @unsubscribe_link %>.

+ <%= 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)) %>

diff --git a/lib/pleroma/web/templates/feed/feed/tag.atom.eex b/lib/pleroma/web/templates/feed/feed/tag.atom.eex index de0731085..6d497e84c 100644 --- a/lib/pleroma/web/templates/feed/feed/tag.atom.eex +++ b/lib/pleroma/web/templates/feed/feed/tag.atom.eex @@ -1,6 +1,6 @@ -<%= '#{Routes.tag_feed_url(@conn, :feed, @tag)}.rss' %> #<%= @tag %> - These are public toots tagged with #<%= @tag %>. You can interact with them if you have an account anywhere in the fediverse. + <%= 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) %> <%= feed_logo() %> <%= most_recent_update(@activities) %> diff --git a/lib/pleroma/web/templates/feed/feed/tag.rss.eex b/lib/pleroma/web/templates/feed/feed/tag.rss.eex index 9c3613feb..edcc3e436 100644 --- a/lib/pleroma/web/templates/feed/feed/tag.rss.eex +++ b/lib/pleroma/web/templates/feed/feed/tag.rss.eex @@ -4,7 +4,7 @@ #<%= @tag %> - These are public toots tagged with #<%= @tag %>. You can interact with them if you have an account anywhere in the fediverse. + <%= 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) %> <%= '#{Routes.tag_feed_url(@conn, :feed, @tag)}.rss' %> <%= feed_logo() %> 2b90d9 diff --git a/lib/pleroma/web/templates/layout/app.html.eex b/lib/pleroma/web/templates/layout/app.html.eex index 1ede59fd8..e33bada85 100644 --- a/lib/pleroma/web/templates/layout/app.html.eex +++ b/lib/pleroma/web/templates/layout/app.html.eex @@ -1,5 +1,5 @@ - + diff --git a/lib/pleroma/web/templates/layout/email.html.eex b/lib/pleroma/web/templates/layout/email.html.eex index f6dcd7f0f..087aa4fc0 100644 --- a/lib/pleroma/web/templates/layout/email.html.eex +++ b/lib/pleroma/web/templates/layout/email.html.eex @@ -1,5 +1,5 @@ - + <%= @email.subject %> @@ -7,4 +7,4 @@ <%= render @view_module, @view_template, assigns %> - \ No newline at end of file + diff --git a/lib/pleroma/web/templates/mailer/subscription/unsubscribe_failure.html.eex b/lib/pleroma/web/templates/mailer/subscription/unsubscribe_failure.html.eex index 7b476f02d..df090ffcd 100644 --- a/lib/pleroma/web/templates/mailer/subscription/unsubscribe_failure.html.eex +++ b/lib/pleroma/web/templates/mailer/subscription/unsubscribe_failure.html.eex @@ -1 +1 @@ -

UNSUBSCRIBE FAILURE

+

<%= Gettext.dpgettext("static_pages", "mailer unsubscribe failed message", "UNSUBSCRIBE FAILURE") %>

diff --git a/lib/pleroma/web/templates/mailer/subscription/unsubscribe_success.html.eex b/lib/pleroma/web/templates/mailer/subscription/unsubscribe_success.html.eex index 6dfa2c185..cbce495d4 100644 --- a/lib/pleroma/web/templates/mailer/subscription/unsubscribe_success.html.eex +++ b/lib/pleroma/web/templates/mailer/subscription/unsubscribe_success.html.eex @@ -1 +1 @@ -

UNSUBSCRIBE SUCCESSFUL

+

<%= Gettext.dpgettext("static_pages", "mailer unsubscribe successful message", "UNSUBSCRIBE SUCCESSFUL") %>

diff --git a/lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex b/lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex index b9daa8d8b..e45d13bdf 100644 --- a/lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex +++ b/lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex @@ -5,11 +5,11 @@ <% end %> -

Two-factor recovery

+

<%= Gettext.dpgettext("static_pages", "mfa recover page title", "Two-factor recovery") %>

<%= form_for @conn, Routes.mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %>
- <%= 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" %>
-<%= submit "Verify" %> +<%= submit Gettext.dpgettext("static_pages", "mfa recover verify recovery code button", "Verify") %> <% end %> "> - Enter a two-factor code + <%= Gettext.dpgettext("static_pages", "mfa recover use 2fa code link", "Enter a two-factor code") %> diff --git a/lib/pleroma/web/templates/o_auth/mfa/totp.html.eex b/lib/pleroma/web/templates/o_auth/mfa/totp.html.eex index 29ea7c5fb..50e6c04b6 100644 --- a/lib/pleroma/web/templates/o_auth/mfa/totp.html.eex +++ b/lib/pleroma/web/templates/o_auth/mfa/totp.html.eex @@ -5,20 +5,20 @@ <% end %> -

Two-factor authentication

+

<%= Gettext.dpgettext("static_pages", "mfa auth page title", "Two-factor authentication") %>

<%= form_for @conn, Routes.mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %>
- <%= 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" %>
-<%= submit "Verify" %> +<%= submit Gettext.dpgettext("static_pages", "mfa auth verify code button", "Verify") %> <% end %> "> - Enter a two-factor recovery code + <%= Gettext.dpgettext("static_pages", "mfa auth page use recovery code link", "Enter a two-factor recovery code") %> diff --git a/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex index c9ec1ecbf..73115e92a 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex @@ -1,5 +1,5 @@
- <%= 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") %>
<%= 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 %> diff --git a/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex index dc4521a62..8b894cd58 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex @@ -1,4 +1,4 @@ -

Sign in with external provider

+

<%= Gettext.dpgettext("static_pages", "oauth external provider page title", "Sign in with external provider") %>

<%= form_for @conn, Routes.o_auth_path(@conn, :prepare_request), [as: "authorization", method: "get"], fn f -> %>
@@ -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 %> diff --git a/lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex index ffabe29a6..76ed3fda5 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex @@ -1,2 +1,2 @@ -

Successfully authorized

-

Token code is
<%= @auth.token %>

+

<%= Gettext.dpgettext("static_pages", "oauth authorized page title", "Successfully authorized") %>

+

<%= raw Gettext.dpgettext("static_pages", "oauth token code message", "Token code is
%{token}", token: safe_to_string(html_escape(@auth.token))) %>

diff --git a/lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex index 82785c4b9..754bf2eb0 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex @@ -1,2 +1,2 @@ -

Authorization exists

-

Access token is
<%= @token.token %>

+

<%= Gettext.dpgettext("static_pages", "oauth authorization exists page title", "Authorization exists") %>

+

<%= raw Gettext.dpgettext("static_pages", "oauth token code message", "Token code is
%{token}", token: safe_to_string(html_escape(@token.token))) %>

diff --git a/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex index 99f900fb7..1f661efb2 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex @@ -5,34 +5,34 @@ <% end %> -

Registration Details

+

<%= Gettext.dpgettext("static_pages", "oauth register page title", "Registration Details") %>

-

If you'd like to register a new account, please provide the details below.

+

<%= Gettext.dpgettext("static_pages", "oauth register page fill form prompt", "If you'd like to register a new account, please provide the details below.") %>

<%= form_for @conn, Routes.o_auth_path(@conn, :register), [as: "authorization"], fn f -> %>
- <%= 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" %>
- <%= 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" %>
-<%= 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" %> -

Alternatively, sign in to connect to existing account.

+

<%= Gettext.dpgettext("static_pages", "oauth register page login prompt", "Alternatively, sign in to connect to existing account.") %>

- <%= 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" %>
- <%= 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" %>
-<%= 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 %> diff --git a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex index 181a9519a..a2f41618e 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex @@ -20,36 +20,38 @@
<%= if @app do %> -

Application <%= @app.client_name %> is requesting access to your account.

+

<%= raw Gettext.dpgettext("static_pages", "oauth authorize message", "Application %{client_name} is requesting access to your account.", client_name: safe_to_string(html_escape(@app.client_name))) %>

<%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f}) %> <% end %> <%= if @user do %>
- Cancel - <%= submit "Approve", class: "button--approve" %> + + <%= Gettext.dpgettext("static_pages", "oauth authorize cancel button", "Cancel") %> + + <%= submit Gettext.dpgettext("static_pages", "oauth authorize approve button", "Approve"), class: "button--approve" %>
<% else %> <%= if @params["registration"] in ["true", true] do %> -

This is the first time you visit! Please enter your Pleroma handle.

-

Choose carefully! You won't be able to change this later. You will be able to change your display name, though.

+

<%= Gettext.dpgettext("static_pages", "oauth register page title", "This is the first time you visit! Please enter your Pleroma handle.") %>

+

<%= 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.") %>

- <%= 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" %>
<%= hidden_input f, :name, value: @params["name"] %> <%= hidden_input f, :password, value: @params["password"] %>
<% else %>
- <%= label f, :name, "Username" %> + <%= label f, :name, Gettext.dpgettext("static_pages", "oauth login username prompt", "Username") %> <%= text_input f, :name %>
- <%= label f, :password, "Password" %> + <%= label f, :password, Gettext.dpgettext("static_pages", "oauth login password prompt", "Password") %> <%= password_input f, :password %>
- <%= submit "Log In" %> + <%= submit Gettext.dpgettext("static_pages", "oauth login button", "Log In") %> <% end %> <% end %>
diff --git a/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex index 3191bf450..a14ca305e 100644 --- a/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex +++ b/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex @@ -5,7 +5,7 @@
- +
<%= raw Formatter.emojify(@user.name, @user.emoji) %> | <%= link "@#{@user.nickname}@#{Endpoint.host()}", to: (@user.uri || @user.ap_id) %> diff --git a/lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex b/lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex index ee84750c7..5ac0aa4e0 100644 --- a/lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex +++ b/lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex @@ -1 +1 @@ -

Invalid Token

+

<%= Gettext.dpgettext("static_pages", "password reset invalid token message", "Invalid Token") %>

diff --git a/lib/pleroma/web/templates/twitter_api/password/reset.html.eex b/lib/pleroma/web/templates/twitter_api/password/reset.html.eex index fbcacdc14..6a544af51 100644 --- a/lib/pleroma/web/templates/twitter_api/password/reset.html.eex +++ b/lib/pleroma/web/templates/twitter_api/password/reset.html.eex @@ -1,13 +1,13 @@

Password Reset for <%= @user.nickname %>

<%= form_for @conn, Routes.reset_password_path(@conn, :do_reset), [as: "data"], fn f -> %>
- <%= label f, :password, "Password" %> + <%= label f, :password, Gettext.dpgettext("static_pages", "password reset form password prompt", "Password") %> <%= password_input f, :password %>
- <%= 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 %>
<%= hidden_input f, :token, value: @token.token %> - <%= submit "Reset" %> + <%= submit Gettext.dpgettext("static_pages", "password reset button", "Reset") %> <% end %> diff --git a/lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex b/lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex index 4ed4ac8bc..774e3462a 100644 --- a/lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex +++ b/lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex @@ -1,2 +1,6 @@ -

Password reset failed

-

Homepage

+

<%= Gettext.dpgettext("static_pages", "password reset failed message", "Password reset failed") %>

+

+ + <%= Gettext.dpgettext("static_pages", "password reset failed homepage link", "Homepage") %> + +

diff --git a/lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex b/lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex index 086d4e08b..40f6bb3fc 100644 --- a/lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex +++ b/lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex @@ -1,2 +1,2 @@ -

Password changed!

-

Homepage

+

<%= Gettext.dpgettext("static_pages", "password reset successful message", "Password changed!") %>

+

<%= Gettext.dpgettext("static_pages", "password reset successful homepage link", "Homepage") %>

diff --git a/lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex b/lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex index a7be53091..e2d251fac 100644 --- a/lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex +++ b/lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex @@ -1,11 +1,11 @@ <%= if @error == :error do %> -

Error fetching user

+

<%= Gettext.dpgettext("static_pages", "remote follow error", "Error fetching user") %>

<% else %> -

Remote follow

+

<%= Gettext.dpgettext("static_pages", "remote follow header", "Remote follow") %>

<%= @followee.nickname %>

<%= 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 %> diff --git a/lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex b/lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex index a8026fa9d..26340a906 100644 --- a/lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex +++ b/lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex @@ -1,14 +1,14 @@ <%= if @error do %>

<%= @error %>

<% end %> -

Log in to follow

+

<%= Gettext.dpgettext("static_pages", "remote follow header, need login", "Log in to follow") %>

<%= @followee.nickname %>

<%= 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" %>
-<%= 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" %>
<%= hidden_input f, :id, value: @followee.id %> -<%= submit "Authorize" %> +<%= submit Gettext.dpgettext("static_pages", "remote follow authorization button for login", "Authorize") %> <% end %> diff --git a/lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex b/lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex index a54ed83b5..638212c1e 100644 --- a/lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex +++ b/lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex @@ -1,13 +1,13 @@ <%= if @error do %>

<%= @error %>

<% end %> -

Two-factor authentication

+

<%= Gettext.dpgettext("static_pages", "remote follow mfa header", "Two-factor authentication") %>

<%= @followee.nickname %>

<%= 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 %>
<%= 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 %> diff --git a/lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex b/lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex index da473d502..2fb4cc5d3 100644 --- a/lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex +++ b/lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex @@ -1,6 +1,5 @@ <%= if @error do %> -

Error following account

+

<%= Gettext.dpgettext("static_pages", "remote follow error", "Error following account") %>

<% else %> -

Account followed!

+

<%= Gettext.dpgettext("static_pages", "remote follow success", "Account followed!") %>

<% end %> - diff --git a/lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex b/lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex index a6b313d8a..848660f26 100644 --- a/lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex +++ b/lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex @@ -1,10 +1,10 @@ <%= if @error do %> -

Error: <%= @error %>

+

<%= Gettext.dpgettext("static_pages", "remote follow error", "Error: %{error}", error: @error) %>

<% else %> -

Remotely follow <%= @nickname %>

+

<%= Gettext.dpgettext("static_pages", "remote follow header", "Remotely follow %{nickname}", nickname: @nickname) %>

<%= 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 %> diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex index 76ca82d20..7921653a8 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -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) diff --git a/lib/pleroma/web/twitter_api/views/password_view.ex b/lib/pleroma/web/twitter_api/views/password_view.ex index a9bb95a2c..40e7fca49 100644 --- a/lib/pleroma/web/twitter_api/views/password_view.ex +++ b/lib/pleroma/web/twitter_api/views/password_view.ex @@ -5,4 +5,5 @@ defmodule Pleroma.Web.TwitterAPI.PasswordView do use Pleroma.Web, :view import Phoenix.HTML.Form + alias Pleroma.Web.Gettext end diff --git a/lib/pleroma/web/twitter_api/views/remote_follow_view.ex b/lib/pleroma/web/twitter_api/views/remote_follow_view.ex index ac3f15eec..618ba2ba5 100644 --- a/lib/pleroma/web/twitter_api/views/remote_follow_view.ex +++ b/lib/pleroma/web/twitter_api/views/remote_follow_view.ex @@ -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 diff --git a/lib/pleroma/web/twitter_api/views/util_view.ex b/lib/pleroma/web/twitter_api/views/util_view.ex index 87cb79dd7..a03020290 100644 --- a/lib/pleroma/web/twitter_api/views/util_view.ex +++ b/lib/pleroma/web/twitter_api/views/util_view.ex @@ -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 """ diff --git a/lib/pleroma/web/views/email_view.ex b/lib/pleroma/web/views/email_view.ex index f7659b994..2ef049d27 100644 --- a/lib/pleroma/web/views/email_view.ex +++ b/lib/pleroma/web/views/email_view.ex @@ -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) diff --git a/lib/pleroma/web/views/mailer/subscription_view.ex b/lib/pleroma/web/views/mailer/subscription_view.ex index 1dc80987b..01e96c61c 100644 --- a/lib/pleroma/web/views/mailer/subscription_view.ex +++ b/lib/pleroma/web/views/mailer/subscription_view.ex @@ -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 index 000000000..70a8d42d0 --- /dev/null +++ b/lib/pleroma/workers/search_indexing_worker.ex @@ -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 8df00154e..558e71262 100644 --- 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, diff --git a/mix.lock b/mix.lock index 8c39d2199..422bbea5e 100644 --- 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"}, diff --git a/priv/es-mappings/activity.json b/priv/es-mappings/activity.json index e476fd59f..052633496 100644 --- a/priv/es-mappings/activity.json +++ b/priv/es-mappings/activity.json @@ -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 index 000000000..fed111ccb --- /dev/null +++ b/priv/gettext/default.pot @@ -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 index 000000000..63db74608 --- /dev/null +++ b/priv/gettext/en_test/LC_MESSAGES/default.po @@ -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 index 000000000..a40de7f8b --- /dev/null +++ b/priv/gettext/en_test/LC_MESSAGES/errors.po @@ -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 index 000000000..663fc5924 --- /dev/null +++ b/priv/gettext/en_test/LC_MESSAGES/posix_errors.po @@ -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 index 000000000..1a3b7b355 --- /dev/null +++ b/priv/gettext/en_test/LC_MESSAGES/static_pages.po @@ -0,0 +1,529 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR Free Software Foundation, Inc. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"PO-Revision-Date: 2022-03-06 11:27-0500\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \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 %{client_name} 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
%{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 %{email}. " +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 %{instance} 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 "

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

\n

%{download_url}

\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 "

Awaiting Approval

\n

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

\n" +msgstr "" + +#, elixir-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 "

Thank you for registering on %{instance_name}

\n

Email confirmation is required to activate the account.

\n

Please click the following link to activate your account.

\n" +msgstr "" + +#, elixir-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 "

Reset your password at %{instance_name}

\n

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

\n

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

\n

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

\n" +msgstr "" + +#, elixir-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 "

Hello @%{nickname},

\n

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

\n

No further action is required to activate your account.

\n" +msgstr "" + +#, elixir-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 "

You are invited to %{instance_name}

\n

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

\n

Click the following link to register: accept invitation.

\n" +msgstr "" + +#, elixir-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 "

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

\n

%{download_url}

\n" +msgstr "" diff --git a/priv/gettext/errors.pot b/priv/gettext/errors.pot index e337226a7..7644fc230 100644 --- a/priv/gettext/errors.pot +++ b/priv/gettext/errors.pot @@ -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,120 +410,101 @@ 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." -msgstr "" - -#, elixir-format -#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:138 -msgid "Unexpected error occurred while creating pack." -msgstr "" - -#, elixir-format -#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:278 -msgid "Unexpected error occurred while removing file from pack." -msgstr "" - -#, elixir-format -#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:250 -msgid "Unexpected error occurred while updating file in pack." -msgstr "" - -#, elixir-format -#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:179 -msgid "Unexpected error occurred while updating pack metadata." -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:451 +#: 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:126 +#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:129 msgid "authorization required for timeline view" msgstr "" @@ -560,11 +514,50 @@ msgid "Access denied" msgstr "" #, elixir-format -#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:282 +#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:321 msgid "This API requires an authenticated user" msgstr "" #, elixir-format -#: lib/pleroma/plugs/user_is_admin_plug.ex:21 +#: 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/posix_errors.pot b/priv/gettext/posix_errors.pot index c9f593944..3533639e0 100644 --- a/priv/gettext/posix_errors.pot +++ b/priv/gettext/posix_errors.pot @@ -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 index 000000000..fbc3e61a3 --- /dev/null +++ b/priv/gettext/static_pages.pot @@ -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 %{client_name} 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
%{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 %{email}. " +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 %{instance} 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 "

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

\n

%{download_url}

\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 "

Awaiting Approval

\n

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

\n" +msgstr "" + +#, elixir-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 "

Thank you for registering on %{instance_name}

\n

Email confirmation is required to activate the account.

\n

Please click the following link to activate your account.

\n" +msgstr "" + +#, elixir-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 "

Reset your password at %{instance_name}

\n

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

\n

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

\n

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

\n" +msgstr "" + +#, elixir-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 "

Hello @%{nickname},

\n

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

\n

No further action is required to activate your account.

\n" +msgstr "" + +#, elixir-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 "

You are invited to %{instance_name}

\n

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

\n

Click the following link to register: accept invitation.

\n" +msgstr "" + +#, elixir-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 "

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

\n

%{download_url}

\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 index 000000000..7a63c36aa --- /dev/null +++ b/priv/repo/migrations/20220302013920_add_language_to_users.exs @@ -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 index 000000000..68cb6bbf7 --- /dev/null +++ b/test/fixtures/owncast-note-with-attachment.json @@ -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": "

I've gone live!

#owncast #streaming

https://owncast.localhost.localdomain", + "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" +} diff --git a/test/mix/tasks/pleroma/digest_test.exs b/test/mix/tasks/pleroma/digest_test.exs index 4a9e461a9..b8050c7af 100644 --- a/test/mix/tasks/pleroma/digest_test.exs +++ b/test/mix/tasks/pleroma/digest_test.exs @@ -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 diff --git a/test/pleroma/emails/user_email_test.exs b/test/pleroma/emails/user_email_test.exs index 21fd06ea6..771a9a490 100644 --- a/test/pleroma/emails/user_email_test.exs +++ b/test/pleroma/emails/user_email_test.exs @@ -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 diff --git a/test/pleroma/emoji_test.exs b/test/pleroma/emoji_test.exs index fe7fd111c..978473b14 100644 --- a/test/pleroma/emoji_test.exs +++ b/test/pleroma/emoji_test.exs @@ -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?("🇵") diff --git a/test/pleroma/notification_test.exs b/test/pleroma/notification_test.exs index 716af496d..b47edd0a3 100644 --- a/test/pleroma/notification_test.exs +++ b/test/pleroma/notification_test.exs @@ -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) diff --git a/test/pleroma/activity/search_test.exs b/test/pleroma/search/database_search_test.exs similarity index 81% rename from test/pleroma/activity/search_test.exs rename to test/pleroma/search/database_search_test.exs index 657fbc627..2387ac29b 100644 --- a/test/pleroma/activity/search_test.exs +++ b/test/pleroma/search/database_search_test.exs @@ -2,8 +2,8 @@ # Copyright © 2017-2021 Pleroma Authors # 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 index 000000000..cc5eb6792 --- /dev/null +++ b/test/pleroma/search/elasticsearch_test.exs @@ -0,0 +1,120 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# 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'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 index 000000000..04a2d75d9 --- /dev/null +++ b/test/pleroma/search/meilisearch_test.exs @@ -0,0 +1,129 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# 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'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'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 diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs index 7c30f39ad..756281a46 100644 --- a/test/pleroma/user_test.exs +++ b/test/pleroma/user_test.exs @@ -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 diff --git a/test/pleroma/web/activity_pub/mrf/anti_followbot_policy_test.exs b/test/pleroma/web/activity_pub/mrf/anti_followbot_policy_test.exs index d5af3a9b6..14a6ae52b 100644 --- a/test/pleroma/web/activity_pub/mrf/anti_followbot_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/anti_followbot_policy_test.exs @@ -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 diff --git a/test/pleroma/web/activity_pub/mrf/steal_emoji_policy_test.exs b/test/pleroma/web/activity_pub/mrf/steal_emoji_policy_test.exs index 1b37e4c26..b0a7e8993 100644 --- a/test/pleroma/web/activity_pub/mrf/steal_emoji_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/steal_emoji_policy_test.exs @@ -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() diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs index 2bd1e46c1..717a704d4 100644 --- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs @@ -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 diff --git a/test/pleroma/web/activity_pub/pipeline_test.exs b/test/pleroma/web/activity_pub/pipeline_test.exs index 30fd5651b..e606fa3d1 100644 --- a/test/pleroma/web/activity_pub/pipeline_test.exs +++ b/test/pleroma/web/activity_pub/pipeline_test.exs @@ -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 diff --git a/test/pleroma/web/admin_api/controllers/report_controller_test.exs b/test/pleroma/web/admin_api/controllers/report_controller_test.exs index 642b05f3f..2d526527b 100644 --- a/test/pleroma/web/admin_api/controllers/report_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/report_controller_test.exs @@ -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 index 000000000..e186f1ab3 --- /dev/null +++ b/test/pleroma/web/gettext_test.exs @@ -0,0 +1,173 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# 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 diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs index 374e2048a..de38a9798 100644 --- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs @@ -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"]) diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs index ed66d370a..3e0660031 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -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) diff --git a/test/pleroma/web/o_auth/app_test.exs b/test/pleroma/web/o_auth/app_test.exs index a5223b0a5..3c5ca07ae 100644 --- a/test/pleroma/web/o_auth/app_test.exs +++ b/test/pleroma/web/o_auth/app_test.exs @@ -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 diff --git a/test/pleroma/web/plugs/set_locale_plug_test.exs b/test/pleroma/web/plugs/set_locale_plug_test.exs index 5261e67ae..f9d34bbe4 100644 --- a/test/pleroma/web/plugs/set_locale_plug_test.exs +++ b/test/pleroma/web/plugs/set_locale_plug_test.exs @@ -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 index 000000000..6e203f2ef --- /dev/null +++ b/test/support/elasticsearch_mock.ex @@ -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