glibc:
when:
event:
- - tag
+ - push
+ branch:
+ - develop
secrets:
- SCW_ACCESS_KEY
- SCW_SECRET_KEY
musl:
when:
event:
- - tag
+ - push
+ branch:
+ - develop
secrets:
- SCW_ACCESS_KEY
- SCW_SECRET_KEY
when:
event:
- push
+ - pull_request
environment:
MIX_ENV: test
commands:
when:
event:
- push
+ - pull_request
environment:
MIX_ENV: test
POSTGRES_DB: pleroma_test
- 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
- 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
remote_fetcher: 2,
attachments_cleanup: 1,
new_users_digest: 1,
- mute_expire: 5
+ mute_expire: 5,
+ search_indexing: 10
],
plugins: [Oban.Plugins.Pruner],
crontab: [
config :pleroma, :workers,
retries: [
federator_incoming: 5,
- federator_outgoing: 5
+ federator_outgoing: 5,
+ search_indexing: 2
]
config :pleroma, Pleroma.Formatter,
config :pleroma, ConcurrentLimiter, [
{Pleroma.Web.RichMedia.Helpers, [max_running: 5, max_waiting: 5]},
- {Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy, [max_running: 5, max_waiting: 5]}
+ {Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy, [max_running: 5, max_waiting: 5]},
+ {Pleroma.Search, [max_running: 30, max_waiting: 50]}
]
-config :pleroma, :search, provider: Pleroma.Search.Builtin
-
-config :pleroma, :telemetry,
- slow_queries_logging: [
- enabled: false,
- min_duration: 500_000,
- exclude_sources: [nil, "oban_jobs"]
- ]
+config :pleroma, Pleroma.Search, module: Pleroma.Search.DatabaseSearch
+
+config :pleroma, Pleroma.Search.Meilisearch,
+ url: "http://127.0.0.1:7700/",
+ private_key: nil,
+ initial_indexing_chunk_size: 100_000
+
+config :pleroma, Pleroma.Search.Elasticsearch.Cluster,
+ url: "http://localhost:9200",
+ username: "elastic",
+ password: "changeme",
+ api: Elasticsearch.API.HTTP,
+ json_library: Jason,
+ indexes: %{
+ activities: %{
+ settings: "priv/es-mappings/activity.json",
+ store: Pleroma.Search.Elasticsearch.Store,
+ sources: [Pleroma.Activity],
+ bulk_page_size: 1000,
+ bulk_wait_interval: 15_000
+ }
+ }
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
]
}
]
+ },
+ %{
+ 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]
+ }
+ ]
+ }
+ ]
+ }
+ ]
}
]
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
## 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.
* `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.
--- /dev/null
+# Configuring search
+
+{! backend/administration/CLI_tasks/general_cli_task_info.include !}
+
+## Built-in search
+
+To use built-in search that has no external dependencies, set the search module to `Pleroma.Activity`:
+
+> config :pleroma, Pleroma.Search, module: Pleroma.Search.DatabaseSearch
+
+While it has no external dependencies, it has problems with performance and relevancy.
+
+## Meilisearch
+
+Note that it's quite a bit more memory hungry than PostgreSQL (around 4-5G for ~1.2 million
+posts while idle and up to 7G while indexing initially). The disk usage for this additional index is also
+around 4 gigabytes. Like [RUM](./cheatsheet.md#rum-indexing-for-full-text-search) indexes, it offers considerably
+higher performance and ordering by timestamp in a reasonable amount of time.
+Additionally, the search results seem to be more accurate.
+
+Due to high memory usage, it may be best to set it up on a different machine, if running pleroma on a low-resource
+computer, and use private key authentication to secure the remote search instance.
+
+To use [meilisearch](https://www.meilisearch.com/), set the search module to `Pleroma.Search.Meilisearch`:
+
+> config :pleroma, Pleroma.Search, module: Pleroma.Search.Meilisearch
+
+You then need to set the address of the meilisearch instance, and optionally the private key for authentication. You might
+also want to change the `initial_indexing_chunk_size` to be smaller if you're server is not very powerful, but not higher than `100_000`,
+because meilisearch will refuse to process it if it's too big. However, in general you want this to be as big as possible, because meilisearch
+indexes faster when it can process many posts in a single batch.
+
+> config :pleroma, Pleroma.Search.Meilisearch,
+> url: "http://127.0.0.1:7700/",
+> private_key: "private key",
+> initial_indexing_chunk_size: 100_000
+
+Information about setting up meilisearch can be found in the
+[official documentation](https://docs.meilisearch.com/learn/getting_started/installation.html).
+You probably want to start it with `MEILI_NO_ANALYTICS=true` environment variable to disable analytics.
+At least version 0.25.0 is required, but you are strongly adviced to use at least 0.26.0, as it introduces
+the `--enable-auto-batching` option which drastically improves performance. Without this option, the search
+is hardly usable on a somewhat big instance.
+
+### Private key authentication (optional)
+
+To set the private key, use the `MEILI_MASTER_KEY` environment variable when starting. After setting the _master key_,
+you have to get the _private key_, which is actually used for authentication.
+
+=== "OTP"
+ ```sh
+ ./bin/pleroma_ctl search.meilisearch show-keys <your master key here>
+ ```
+
+=== "From Source"
+ ```sh
+ mix pleroma.search.meilisearch show-keys <your master key here>
+ ```
+
+You will see a "Default Admin API Key", this is the key you actually put into your configuration file.
+
+### Initial indexing
+
+After setting up the configuration, you'll want to index all of your already existsing posts. Only public posts are indexed. You'll only
+have to do it one time, but it might take a while, depending on the amount of posts your instance has seen. This is also a fairly RAM
+consuming process for `meilisearch`, and it will take a lot of RAM when running if you have a lot of posts (seems to be around 5G for ~1.2
+million posts while idle and up to 7G while indexing initially, but your experience may be different).
+
+The sequence of actions is as follows:
+
+1. First, change the configuration to use `Pleroma.Search.Meilisearch` as the search backend
+2. Restart your instance, at this point it can be used while the search indexing is running, though search won't return anything
+3. Start the initial indexing process (as described below with `index`),
+ and wait until the task says it sent everything from the database to index
+4. Wait until everything is actually indexed (by checking with `stats` as described below),
+ at this point you don't have to do anything, just wait a while.
+
+To start the initial indexing, run the `index` command:
+
+=== "OTP"
+ ```sh
+ ./bin/pleroma_ctl search.meilisearch index
+ ```
+
+=== "From Source"
+ ```sh
+ mix pleroma.search.meilisearch index
+ ```
+
+This will show you the total amount of posts to index, and then show you the amount of posts indexed currently, until the numbers eventually
+become the same. The posts are indexed in big batches and meilisearch will take some time to actually index them, even after you have
+inserted all the posts into it. Depending on the amount of posts, this may be as long as several hours. To get information about the status
+of indexing and how many posts have actually been indexed, use the `stats` command:
+
+=== "OTP"
+ ```sh
+ ./bin/pleroma_ctl search.meilisearch stats
+ ```
+
+=== "From Source"
+ ```sh
+ mix pleroma.search.meilisearch stats
+ ```
+
+### Clearing the index
+
+In case you need to clear the index (for example, to re-index from scratch, if that needs to happen for some reason), you can
+use the `clear` command:
+
+=== "OTP"
+ ```sh
+ ./bin/pleroma_ctl search.meilisearch clear
+ ```
+
+=== "From Source"
+ ```sh
+ mix pleroma.search.meilisearch clear
+ ```
+
+This will clear **all** the posts from the search index. Note, that deleted posts are also removed from index by the instance itself, so
+there is no need to actually clear the whole index, unless you want **all** of it gone. That said, the index does not hold any information
+that cannot be re-created from the database, it should also generally be a lot smaller than the size of your database. Still, the size
+depends on the amount of text in posts.
+
+## Elasticsearch
+
+As with meilisearch, this can be rather memory-hungry, but it is very good at what it does.
+
+To use [elasticsearch](https://www.elastic.co/), set the search module to `Pleroma.Search.Elasticsearch`:
+
+> config :pleroma, Pleroma.Search, module: Pleroma.Search.Elasticsearch
+
+You then need to set the URL and authentication credentials if relevant.
+
+> config :pleroma, Pleroma.Search.Elasticsearch.Cluster,
+> url: "http://127.0.0.1:9200/",
+> username: "elastic",
+> password: "changeme",
+
+### Initial indexing
+
+After setting up the configuration, you'll want to index all of your already existsing posts. Only public posts are indexed. You'll only
+have to do it one time, but it might take a while, depending on the amount of posts your instance has seen.
+
+The sequence of actions is as follows:
+
+1. First, change the configuration to use `Pleroma.Search.Elasticsearch` as the search backend
+2. Restart your instance, at this point it can be used while the search indexing is running, though search won't return anything
+3. Start the initial indexing process (as described below with `index`),
+ and wait until the task says it sent everything from the database to index
+4. Wait until the index tasks exits
+
+To start the initial indexing, run the `build` command:
+
+=== "OTP"
+```sh
+./bin/pleroma_ctl search import activities
+```
+
+=== "From Source"
+```sh
+mix pleroma.search import activities
+```
- `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.
- `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
{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, []))
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
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
--- /dev/null
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Mix.Tasks.Pleroma.Search.Meilisearch do
+ require Pleroma.Constants
+
+ import Mix.Pleroma
+ import Ecto.Query
+
+ import Pleroma.Search.Meilisearch,
+ only: [meili_post: 2, meili_put: 2, meili_get: 1, meili_delete!: 1]
+
+ def run(["index"]) do
+ start_pleroma()
+
+ meili_version =
+ (
+ {:ok, result} = meili_get("/version")
+
+ result["pkgVersion"]
+ )
+
+ # The ranking rule syntax was changed but nothing about that is mentioned in the changelog
+ if not Version.match?(meili_version, ">= 0.25.0") do
+ raise "Meilisearch <0.24.0 not supported"
+ end
+
+ {:ok, _} =
+ meili_post(
+ "/indexes/objects/settings/ranking-rules",
+ [
+ "published:desc",
+ "words",
+ "exactness",
+ "proximity",
+ "typo",
+ "attribute",
+ "sort"
+ ]
+ )
+
+ {:ok, _} =
+ meili_post(
+ "/indexes/objects/settings/searchable-attributes",
+ [
+ "content"
+ ]
+ )
+
+ IO.puts("Created indices. Starting to insert posts.")
+
+ chunk_size = Pleroma.Config.get([Pleroma.Search.Meilisearch, :initial_indexing_chunk_size])
+
+ Pleroma.Repo.transaction(
+ fn ->
+ query =
+ from(Pleroma.Object,
+ # Only index public and unlisted posts which are notes and have some text
+ where:
+ fragment("data->>'type' = 'Note'") and
+ (fragment("data->'to' \\? ?", ^Pleroma.Constants.as_public()) or
+ fragment("data->'cc' \\? ?", ^Pleroma.Constants.as_public())),
+ order_by: [desc: fragment("data->'published'")]
+ )
+
+ count = query |> Pleroma.Repo.aggregate(:count, :data)
+ IO.puts("Entries to index: #{count}")
+
+ Pleroma.Repo.stream(
+ query,
+ timeout: :infinity
+ )
+ |> Stream.map(&Pleroma.Search.Meilisearch.object_to_search_data/1)
+ |> Stream.filter(fn o -> not is_nil(o) end)
+ |> Stream.chunk_every(chunk_size)
+ |> Stream.transform(0, fn objects, acc ->
+ new_acc = acc + Enum.count(objects)
+
+ # Reset to the beginning of the line and rewrite it
+ IO.write("\r")
+ IO.write("Indexed #{new_acc} entries")
+
+ {[objects], new_acc}
+ end)
+ |> Stream.each(fn objects ->
+ result =
+ meili_put(
+ "/indexes/objects/documents",
+ objects
+ )
+
+ with {:ok, res} <- result do
+ if not Map.has_key?(res, "uid") do
+ IO.puts("\nFailed to index: #{inspect(result)}")
+ end
+ else
+ e -> IO.puts("\nFailed to index due to network error: #{inspect(e)}")
+ end
+ end)
+ |> Stream.run()
+ end,
+ timeout: :infinity
+ )
+
+ IO.write("\n")
+ end
+
+ def run(["clear"]) do
+ start_pleroma()
+
+ meili_delete!("/indexes/objects/documents")
+ end
+
+ def run(["show-keys", master_key]) do
+ start_pleroma()
+
+ endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url])
+
+ {:ok, result} =
+ Pleroma.HTTP.get(
+ Path.join(endpoint, "/keys"),
+ [{"Authorization", "Bearer #{master_key}"}]
+ )
+
+ decoded = Jason.decode!(result.body)
+
+ if decoded["results"] do
+ Enum.each(decoded["results"], fn %{"description" => desc, "key" => key} ->
+ IO.puts("#{desc}: #{key}")
+ end)
+ else
+ IO.puts("Error fetching the keys, check the master key is correct: #{inspect(decoded)}")
+ end
+ end
+
+ def run(["stats"]) do
+ start_pleroma()
+
+ {:ok, result} = meili_get("/indexes/objects/stats")
+ IO.puts("Number of entries: #{result["numberOfDocuments"]}")
+ IO.puts("Indexing? #{result["isIndexing"]}")
+ end
+end
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
{Oban, Config.get(Oban)},
Pleroma.Web.Endpoint
] ++
+ elasticsearch_children() ++
task_children(@mix_env) ++
dont_run_in_test(@mix_env) ++
shout_child(shout_enabled?())
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, [])
+++ /dev/null
-# Akkoma: A lightweight social networking server
-# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Elasticsearch.DocumentMappings.Activity do
- alias Pleroma.Object
-
- def id(obj), do: obj.id
-
- def encode(%{object: %{data: %{"type" => "Note"}}} = activity) do
- %{
- _timestamp: activity.inserted_at,
- user: activity.user_actor.nickname,
- content: activity.object.data["content"],
- instance: URI.parse(activity.user_actor.ap_id).host,
- hashtags: Object.hashtags(activity.object)
- }
- end
-end
+++ /dev/null
-# Akkoma: A lightweight social networking server
-# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Elasticsearch.DocumentMappings.Hashtag do
- def id(obj), do: obj.id
-
- def encode(%{timestamp: _} = hashtag) do
- %{
- hashtag: hashtag.name,
- timestamp: hashtag.timestamp
- }
- end
-
- def encode(hashtag) do
- %{
- hashtag: hashtag.name,
- timestamp: hashtag.inserted_at
- }
- end
-end
+++ /dev/null
-# Akkoma: A lightweight social networking server
-# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Elasticsearch.DocumentMappings.User do
- def id(obj), do: obj.id
-
- def encode(%{actor_type: "Person"} = user) do
- %{
- timestamp: user.inserted_at,
- instance: URI.parse(user.ap_id).host,
- nickname: user.nickname,
- bio: user.bio,
- display_name: user.name
- }
- end
-end
+++ /dev/null
-# Akkoma: A lightweight social networking server
-# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Elasticsearch do
- alias Pleroma.Activity
- alias Pleroma.User
- alias Pleroma.Object
- alias Pleroma.Elasticsearch.DocumentMappings
- alias Pleroma.Config
- require Logger
-
- defp url do
- Config.get([:elasticsearch, :url])
- end
-
- defp enabled? do
- Config.get([:search, :provider]) == Pleroma.Search.Elasticsearch
- end
-
- def delete_by_id(:activity, id) do
- if enabled?() do
- Elastix.Document.delete(url(), "activities", "activity", id)
- end
- end
-
- def put_by_id(:activity, id) do
- id
- |> Activity.get_by_id_with_object()
- |> maybe_put_into_elasticsearch()
- end
-
- def maybe_put_into_elasticsearch({:ok, item}) do
- maybe_put_into_elasticsearch(item)
- end
-
- def maybe_put_into_elasticsearch(
- %{data: %{"type" => "Create"}, object: %{data: %{"type" => "Note"}}} = activity
- ) do
- if enabled?() do
- actor = Pleroma.Activity.user_actor(activity)
-
- activity
- |> Map.put(:user_actor, actor)
- |> put()
- end
- end
-
- def maybe_put_into_elasticsearch(%User{actor_type: "Person"} = user) do
- if enabled?() do
- put(user)
- end
- end
-
- def maybe_put_into_elasticsearch(_) do
- {:ok, :skipped}
- end
-
- def maybe_bulk_post(data, type) do
- if enabled?() do
- bulk_post(data, type)
- end
- end
-
- def put(%Activity{} = activity) do
- with {:ok, _} <-
- Elastix.Document.index(
- url(),
- "activities",
- "activity",
- DocumentMappings.Activity.id(activity),
- DocumentMappings.Activity.encode(activity)
- ) do
- activity
- |> Map.get(:object)
- |> Object.hashtags()
- |> Enum.map(fn x ->
- %{id: x, name: x, timestamp: DateTime.to_iso8601(DateTime.utc_now())}
- end)
- |> bulk_post(:hashtags)
- else
- {:error, %{reason: err}} ->
- Logger.error("Could not put activity: #{err}")
- :skipped
- end
- end
-
- def put(%User{} = user) do
- with {:ok, _} <-
- Elastix.Document.index(
- url(),
- "users",
- "user",
- DocumentMappings.User.id(user),
- DocumentMappings.User.encode(user)
- ) do
- :ok
- else
- {:error, %{reason: err}} ->
- Logger.error("Could not put user: #{err}")
- :skipped
- end
- end
-
- def bulk_post(data, :activities) do
- d =
- data
- |> Enum.filter(fn x ->
- t =
- x.object
- |> Map.get(:data, %{})
- |> Map.get("type", "")
-
- t == "Note"
- end)
- |> Enum.map(fn d ->
- [
- %{index: %{_id: DocumentMappings.Activity.id(d)}},
- DocumentMappings.Activity.encode(d)
- ]
- end)
- |> List.flatten()
-
- with {:ok, %{body: %{"errors" => false}}} <-
- Elastix.Bulk.post(
- url(),
- d,
- index: "activities",
- type: "activity"
- ) do
- :ok
- else
- {:error, %{reason: err}} ->
- Logger.error("Could not bulk put activity: #{err}")
- :skipped
-
- {:ok, %{body: _}} ->
- :skipped
- end
- end
-
- def bulk_post(data, :users) do
- d =
- data
- |> Enum.filter(fn x -> x.actor_type == "Person" end)
- |> Enum.map(fn d ->
- [
- %{index: %{_id: DocumentMappings.User.id(d)}},
- DocumentMappings.User.encode(d)
- ]
- end)
- |> List.flatten()
-
- with {:ok, %{body: %{"errors" => false}}} <-
- Elastix.Bulk.post(
- url(),
- d,
- index: "users",
- type: "user"
- ) do
- :ok
- else
- {:error, %{reason: err}} ->
- Logger.error("Could not bulk put users: #{err}")
- :skipped
-
- {:ok, %{body: _}} ->
- :skipped
- end
- end
-
- def bulk_post(data, :hashtags) when is_list(data) do
- d =
- data
- |> Enum.map(fn d ->
- [
- %{index: %{_id: DocumentMappings.Hashtag.id(d)}},
- DocumentMappings.Hashtag.encode(d)
- ]
- end)
- |> List.flatten()
-
- with {:ok, %{body: %{"errors" => false}}} <-
- Elastix.Bulk.post(
- url(),
- d,
- index: "hashtags",
- type: "hashtag"
- ) do
- :ok
- else
- {:error, %{reason: err}} ->
- Logger.error("Could not bulk put hashtags: #{err}")
- :skipped
-
- {:ok, %{body: _}} ->
- :skipped
- end
- end
-
- def bulk_post(_, :hashtags), do: {:ok, nil}
-
- def search(_, _, _, :skip), do: []
-
- def search(:raw, index, type, q) do
- with {:ok, raw_results} <- Elastix.Search.search(url(), index, [type], q) do
- results =
- raw_results
- |> Map.get(:body, %{})
- |> Map.get("hits", %{})
- |> Map.get("hits", [])
-
- {:ok, results}
- else
- {:error, e} ->
- Logger.error(e)
- {:error, e}
- end
- end
-
- def search(:activities, q) do
- with {:ok, results} <- search(:raw, "activities", "activity", q) do
- results
- |> Enum.map(fn result -> result["_id"] end)
- |> Pleroma.Activity.all_by_ids_with_object()
- |> Enum.sort(&(&1.inserted_at >= &2.inserted_at))
- else
- e ->
- Logger.error(e)
- []
- end
- end
-
- def search(:users, q) do
- with {:ok, results} <- search(:raw, "users", "user", q) do
- results
- |> Enum.map(fn result -> result["_id"] end)
- |> Pleroma.User.get_all_by_ids()
- else
- e ->
- Logger.error(e)
- []
- end
- end
-
- def search(:hashtags, q) do
- with {:ok, results} <- search(:raw, "hashtags", "hashtag", q) do
- results
- |> Enum.map(fn result -> result["_source"]["hashtag"] end)
- else
- e ->
- Logger.error(e)
- []
- end
- end
-end
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
@spec welcome(User.t(), map()) :: Swoosh.Email.t()
def welcome(user, opts \\ %{}) do
- new()
- |> to(recipient(user))
- |> from(Map.get(opts, :sender, sender()))
- |> subject(Map.get(opts, :subject, "Welcome to #{instance_name()}!"))
- |> html_body(Map.get(opts, :html, "Welcome to #{instance_name()}!"))
- |> text_body(Map.get(opts, :text, "Welcome to #{instance_name()}!"))
+ Gettext.with_locale_or_default user.language do
+ new()
+ |> to(recipient(user))
+ |> from(Map.get(opts, :sender, sender()))
+ |> subject(
+ Map.get(
+ opts,
+ :subject,
+ Gettext.dpgettext(
+ "static_pages",
+ "welcome email subject",
+ "Welcome to %{instance_name}!",
+ instance_name: instance_name()
+ )
+ )
+ )
+ |> html_body(
+ Map.get(
+ opts,
+ :html,
+ Gettext.dpgettext(
+ "static_pages",
+ "welcome email html body",
+ "Welcome to %{instance_name}!",
+ instance_name: instance_name()
+ )
+ )
+ )
+ |> text_body(
+ Map.get(
+ opts,
+ :text,
+ Gettext.dpgettext(
+ "static_pages",
+ "welcome email text body",
+ "Welcome to %{instance_name}!",
+ instance_name: instance_name()
+ )
+ )
+ )
+ end
end
def password_reset_email(user, token) when is_binary(token) do
- password_reset_url = Router.Helpers.reset_password_url(Endpoint, :reset, token)
-
- html_body = """
- <h3>Reset your password at #{instance_name()}</h3>
- <p>Someone has requested password change for your account at #{instance_name()}.</p>
- <p>If it was you, visit the following link to proceed: <a href="#{password_reset_url}">reset password</a>.</p>
- <p>If it was someone else, nothing to worry about: your data is secure and your password has not been changed.</p>
- """
-
- new()
- |> to(recipient(user))
- |> from(sender())
- |> subject("Password reset")
- |> html_body(html_body)
+ Gettext.with_locale_or_default user.language do
+ password_reset_url = Router.Helpers.reset_password_url(Endpoint, :reset, token)
+
+ html_body =
+ Gettext.dpgettext(
+ "static_pages",
+ "password reset email body",
+ """
+ <h3>Reset your password at %{instance_name}</h3>
+ <p>Someone has requested password change for your account at %{instance_name}.</p>
+ <p>If it was you, visit the following link to proceed: <a href="%{password_reset_url}">reset password</a>.</p>
+ <p>If it was someone else, nothing to worry about: your data is secure and your password has not been changed.</p>
+ """,
+ instance_name: instance_name(),
+ password_reset_url: password_reset_url
+ )
+
+ new()
+ |> to(recipient(user))
+ |> from(sender())
+ |> subject(
+ Gettext.dpgettext("static_pages", "password reset email subject", "Password reset")
+ )
+ |> html_body(html_body)
+ end
end
def user_invitation_email(
to_email,
to_name \\ nil
) do
- registration_url =
- Router.Helpers.redirect_url(
- Endpoint,
- :registration_page,
- user_invite_token.token
- )
+ Gettext.with_locale_or_default user.language do
+ registration_url =
+ Router.Helpers.redirect_url(
+ Endpoint,
+ :registration_page,
+ user_invite_token.token
+ )
+
+ html_body =
+ Gettext.dpgettext(
+ "static_pages",
+ "user invitation email body",
+ """
+ <h3>You are invited to %{instance_name}</h3>
+ <p>%{inviter_name} invites you to join %{instance_name}, an instance of Pleroma federated social networking platform.</p>
+ <p>Click the following link to register: <a href="%{registration_url}">accept invitation</a>.</p>
+ """,
+ instance_name: instance_name(),
+ inviter_name: user.name,
+ registration_url: registration_url
+ )
- html_body = """
- <h3>You are invited to #{instance_name()}</h3>
- <p>#{user.name} invites you to join #{instance_name()}, an instance of Pleroma federated social networking platform.</p>
- <p>Click the following link to register: <a href="#{registration_url}">accept invitation</a>.</p>
- """
-
- new()
- |> to(recipient(to_email, to_name))
- |> from(sender())
- |> subject("Invitation to #{instance_name()}")
- |> html_body(html_body)
+ new()
+ |> to(recipient(to_email, to_name))
+ |> from(sender())
+ |> subject(
+ Gettext.dpgettext(
+ "static_pages",
+ "user invitation email subject",
+ "Invitation to %{instance_name}",
+ instance_name: instance_name()
+ )
+ )
+ |> html_body(html_body)
+ end
end
def account_confirmation_email(user) do
- confirmation_url =
- Router.Helpers.confirm_email_url(
- Endpoint,
- :confirm_email,
- user.id,
- to_string(user.confirmation_token)
- )
+ Gettext.with_locale_or_default user.language do
+ confirmation_url =
+ Router.Helpers.confirm_email_url(
+ Endpoint,
+ :confirm_email,
+ user.id,
+ to_string(user.confirmation_token)
+ )
+
+ html_body =
+ Gettext.dpgettext(
+ "static_pages",
+ "confirmation email body",
+ """
+ <h3>Thank you for registering on %{instance_name}</h3>
+ <p>Email confirmation is required to activate the account.</p>
+ <p>Please click the following link to <a href="%{confirmation_url}">activate your account</a>.</p>
+ """,
+ instance_name: instance_name(),
+ confirmation_url: confirmation_url
+ )
- html_body = """
- <h3>Thank you for registering on #{instance_name()}</h3>
- <p>Email confirmation is required to activate the account.</p>
- <p>Please click the following link to <a href="#{confirmation_url}">activate your account</a>.</p>
- """
-
- new()
- |> to(recipient(user))
- |> from(sender())
- |> subject("#{instance_name()} account confirmation")
- |> html_body(html_body)
+ new()
+ |> to(recipient(user))
+ |> from(sender())
+ |> subject(
+ Gettext.dpgettext(
+ "static_pages",
+ "confirmation email subject",
+ "%{instance_name} account confirmation",
+ instance_name: instance_name()
+ )
+ )
+ |> html_body(html_body)
+ end
end
def approval_pending_email(user) do
- html_body = """
- <h3>Awaiting Approval</h3>
- <p>Your account at #{instance_name()} is being reviewed by staff. You will receive another email once your account is approved.</p>
- """
-
- new()
- |> to(recipient(user))
- |> from(sender())
- |> subject("Your account is awaiting approval")
- |> html_body(html_body)
+ Gettext.with_locale_or_default user.language do
+ html_body =
+ Gettext.dpgettext(
+ "static_pages",
+ "approval pending email body",
+ """
+ <h3>Awaiting Approval</h3>
+ <p>Your account at %{instance_name} is being reviewed by staff. You will receive another email once your account is approved.</p>
+ """,
+ instance_name: instance_name()
+ )
+
+ new()
+ |> to(recipient(user))
+ |> from(sender())
+ |> subject(
+ Gettext.dpgettext(
+ "static_pages",
+ "approval pending email subject",
+ "Your account is awaiting approval"
+ )
+ )
+ |> html_body(html_body)
+ end
end
def successful_registration_email(user) do
- html_body = """
- <h3>Hello @#{user.nickname},</h3>
- <p>Your account at #{instance_name()} has been registered successfully.</p>
- <p>No further action is required to activate your account.</p>
- """
-
- new()
- |> to(recipient(user))
- |> from(sender())
- |> subject("Account registered on #{instance_name()}")
- |> html_body(html_body)
+ Gettext.with_locale_or_default user.language do
+ html_body =
+ Gettext.dpgettext(
+ "static_pages",
+ "successful registration email body",
+ """
+ <h3>Hello @%{nickname},</h3>
+ <p>Your account at %{instance_name} has been registered successfully.</p>
+ <p>No further action is required to activate your account.</p>
+ """,
+ nickname: user.nickname,
+ instance_name: instance_name()
+ )
+
+ new()
+ |> to(recipient(user))
+ |> from(sender())
+ |> subject(
+ Gettext.dpgettext(
+ "static_pages",
+ "successful registration email subject",
+ "Account registered on %{instance_name}",
+ instance_name: instance_name()
+ )
+ )
+ |> html_body(html_body)
+ end
end
@doc """
"""
@spec digest_email(User.t()) :: Swoosh.Email.t() | nil
def digest_email(user) do
- notifications = Pleroma.Notification.for_user_since(user, user.last_digest_emailed_at)
-
- mentions =
- notifications
- |> Enum.filter(&(&1.activity.data["type"] == "Create"))
- |> Enum.map(fn notification ->
- object = Pleroma.Object.normalize(notification.activity, fetch: false)
-
- if not is_nil(object) do
- object = update_in(object.data["content"], &format_links/1)
-
- %{
- data: notification,
- object: object,
- from: User.get_by_ap_id(notification.activity.actor)
- }
- end
- end)
- |> Enum.filter(& &1)
-
- followers =
- notifications
- |> Enum.filter(&(&1.activity.data["type"] == "Follow"))
- |> Enum.map(fn notification ->
- from = User.get_by_ap_id(notification.activity.actor)
-
- if not is_nil(from) do
- %{
- data: notification,
- object: Pleroma.Object.normalize(notification.activity, fetch: false),
- from: User.get_by_ap_id(notification.activity.actor)
- }
- end
- end)
- |> Enum.filter(& &1)
-
- unless Enum.empty?(mentions) do
- styling = Config.get([__MODULE__, :styling])
- logo = Config.get([__MODULE__, :logo])
-
- html_data = %{
- instance: instance_name(),
- user: user,
- mentions: mentions,
- followers: followers,
- unsubscribe_link: unsubscribe_url(user, "digest"),
- styling: styling
- }
-
- logo_path =
- if is_nil(logo) do
- Path.join(:code.priv_dir(:pleroma), "static/static/logo.svg")
- else
- Path.join(Config.get([:instance, :static_dir]), logo)
- end
-
- new()
- |> to(recipient(user))
- |> from(sender())
- |> subject("Your digest from #{instance_name()}")
- |> put_layout(false)
- |> render_body("digest.html", html_data)
- |> attachment(Swoosh.Attachment.new(logo_path, filename: "logo.svg", type: :inline))
+ Gettext.with_locale_or_default user.language do
+ notifications = Pleroma.Notification.for_user_since(user, user.last_digest_emailed_at)
+
+ mentions =
+ notifications
+ |> Enum.filter(&(&1.activity.data["type"] == "Create"))
+ |> Enum.map(fn notification ->
+ object = Pleroma.Object.normalize(notification.activity, fetch: false)
+
+ if not is_nil(object) do
+ object = update_in(object.data["content"], &format_links/1)
+
+ %{
+ data: notification,
+ object: object,
+ from: User.get_by_ap_id(notification.activity.actor)
+ }
+ end
+ end)
+ |> Enum.filter(& &1)
+
+ followers =
+ notifications
+ |> Enum.filter(&(&1.activity.data["type"] == "Follow"))
+ |> Enum.map(fn notification ->
+ from = User.get_by_ap_id(notification.activity.actor)
+
+ if not is_nil(from) do
+ %{
+ data: notification,
+ object: Pleroma.Object.normalize(notification.activity, fetch: false),
+ from: User.get_by_ap_id(notification.activity.actor)
+ }
+ end
+ end)
+ |> Enum.filter(& &1)
+
+ unless Enum.empty?(mentions) do
+ styling = Config.get([__MODULE__, :styling])
+ logo = Config.get([__MODULE__, :logo])
+
+ html_data = %{
+ instance: instance_name(),
+ user: user,
+ mentions: mentions,
+ followers: followers,
+ unsubscribe_link: unsubscribe_url(user, "digest"),
+ styling: styling
+ }
+
+ logo_path =
+ if is_nil(logo) do
+ Path.join(:code.priv_dir(:pleroma), "static/static/logo.svg")
+ else
+ Path.join(Config.get([:instance, :static_dir]), logo)
+ end
+
+ new()
+ |> to(recipient(user))
+ |> from(sender())
+ |> subject(
+ Gettext.dpgettext(
+ "static_pages",
+ "digest email subject",
+ "Your digest from %{instance_name}",
+ instance_name: instance_name()
+ )
+ )
+ |> put_layout(false)
+ |> render_body("digest.html", html_data)
+ |> attachment(Swoosh.Attachment.new(logo_path, filename: "logo.svg", type: :inline))
+ end
end
end
def backup_is_ready_email(backup, admin_user_id \\ nil) do
%{user: user} = Pleroma.Repo.preload(backup, :user)
- download_url = Pleroma.Web.PleromaAPI.BackupView.download_url(backup)
-
- html_body =
- if is_nil(admin_user_id) do
- """
- <p>You requested a full backup of your Pleroma account. It's ready for download:</p>
- <p><a href="#{download_url}">#{download_url}</a></p>
- """
- else
- admin = Pleroma.Repo.get(User, admin_user_id)
-
- """
- <p>Admin @#{admin.nickname} requested a full backup of your Pleroma account. It's ready for download:</p>
- <p><a href="#{download_url}">#{download_url}</a></p>
- """
- end
- new()
- |> to(recipient(user))
- |> from(sender())
- |> subject("Your account archive is ready")
- |> html_body(html_body)
+ Gettext.with_locale_or_default user.language do
+ download_url = Pleroma.Web.PleromaAPI.BackupView.download_url(backup)
+
+ html_body =
+ if is_nil(admin_user_id) do
+ Gettext.dpgettext(
+ "static_pages",
+ "account archive email body - self-requested",
+ """
+ <p>You requested a full backup of your Pleroma account. It's ready for download:</p>
+ <p><a href="%{download_url}">%{download_url}</a></p>
+ """,
+ download_url: download_url
+ )
+ else
+ admin = Pleroma.Repo.get(User, admin_user_id)
+
+ Gettext.dpgettext(
+ "static_pages",
+ "account archive email body - admin requested",
+ """
+ <p>Admin @%{admin_nickname} requested a full backup of your Pleroma account. It's ready for download:</p>
+ <p><a href="%{download_url}">%{download_url}</a></p>
+ """,
+ admin_nickname: admin.nickname,
+ download_url: download_url
+ )
+ end
+
+ new()
+ |> to(recipient(user))
+ |> from(sender())
+ |> subject(
+ Gettext.dpgettext(
+ "static_pages",
+ "account archive email subject",
+ "Your account archive is ready"
+ )
+ )
+ |> html_body(html_body)
+ end
end
end
# 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
#
1F602 ; fully-qualified # 😂 E0.6 face with tears of joy
1F642 ; fully-qualified # 🙂 E1.0 slightly smiling face
1F643 ; fully-qualified # 🙃 E1.0 upside-down face
+1FAE0 ; fully-qualified # 🫠 E14.0 melting face
1F609 ; fully-qualified # 😉 E0.6 winking face
1F60A ; fully-qualified # 😊 E0.6 smiling face with smiling eyes
1F607 ; fully-qualified # 😇 E1.0 smiling face with halo
1F911 ; fully-qualified # 🤑 E1.0 money-mouth face
# subgroup: face-hand
-1F917 ; fully-qualified # 🤗 E1.0 hugging face
+1F917 ; fully-qualified # 🤗 E1.0 smiling face with open hands
1F92D ; fully-qualified # 🤭 E5.0 face with hand over mouth
+1FAE2 ; fully-qualified # 🫢 E14.0 face with open eyes and hand over mouth
+1FAE3 ; fully-qualified # 🫣 E14.0 face with peeking eye
1F92B ; fully-qualified # 🤫 E5.0 shushing face
1F914 ; fully-qualified # 🤔 E1.0 thinking face
+1FAE1 ; fully-qualified # 🫡 E14.0 saluting face
# subgroup: face-neutral-skeptical
1F910 ; fully-qualified # 🤐 E1.0 zipper-mouth face
1F610 ; fully-qualified # 😐 E0.7 neutral face
1F611 ; fully-qualified # 😑 E1.0 expressionless face
1F636 ; fully-qualified # 😶 E1.0 face without mouth
+1FAE5 ; fully-qualified # 🫥 E14.0 dotted line face
1F636 200D 1F32B FE0F ; fully-qualified # 😶🌫️ E13.1 face in clouds
1F636 200D 1F32B ; minimally-qualified # 😶🌫 E13.1 face in clouds
1F60F ; fully-qualified # 😏 E0.6 smirking face
1F975 ; fully-qualified # 🥵 E11.0 hot face
1F976 ; fully-qualified # 🥶 E11.0 cold face
1F974 ; fully-qualified # 🥴 E11.0 woozy face
-1F635 ; fully-qualified # 😵 E0.6 knocked-out face
+1F635 ; fully-qualified # 😵 E0.6 face with crossed-out eyes
1F635 200D 1F4AB ; fully-qualified # 😵💫 E13.1 face with spiral eyes
1F92F ; fully-qualified # 🤯 E5.0 exploding head
# subgroup: face-concerned
1F615 ; fully-qualified # 😕 E1.0 confused face
+1FAE4 ; fully-qualified # 🫤 E14.0 face with diagonal mouth
1F61F ; fully-qualified # 😟 E1.0 worried face
1F641 ; fully-qualified # 🙁 E1.0 slightly frowning face
2639 FE0F ; fully-qualified # ☹️ E0.7 frowning face
1F632 ; fully-qualified # 😲 E0.6 astonished face
1F633 ; fully-qualified # 😳 E0.6 flushed face
1F97A ; fully-qualified # 🥺 E11.0 pleading face
+1F979 ; fully-qualified # 🥹 E14.0 face holding back tears
1F626 ; fully-qualified # 😦 E1.0 frowning face with open mouth
1F627 ; fully-qualified # 😧 E1.0 anguished face
1F628 ; fully-qualified # 😨 E0.6 fearful face
1F4AD ; fully-qualified # 💭 E1.0 thought balloon
1F4A4 ; fully-qualified # 💤 E0.6 zzz
-# Smileys & Emotion subtotal: 170
-# Smileys & Emotion subtotal: 170 w/o modifiers
+# Smileys & Emotion subtotal: 177
+# Smileys & Emotion subtotal: 177 w/o modifiers
# group: People & Body
1F596 1F3FD ; fully-qualified # 🖖🏽 E1.0 vulcan salute: medium skin tone
1F596 1F3FE ; fully-qualified # 🖖🏾 E1.0 vulcan salute: medium-dark skin tone
1F596 1F3FF ; fully-qualified # 🖖🏿 E1.0 vulcan salute: dark skin tone
+1FAF1 ; fully-qualified # 🫱 E14.0 rightwards hand
+1FAF1 1F3FB ; fully-qualified # 🫱🏻 E14.0 rightwards hand: light skin tone
+1FAF1 1F3FC ; fully-qualified # 🫱🏼 E14.0 rightwards hand: medium-light skin tone
+1FAF1 1F3FD ; fully-qualified # 🫱🏽 E14.0 rightwards hand: medium skin tone
+1FAF1 1F3FE ; fully-qualified # 🫱🏾 E14.0 rightwards hand: medium-dark skin tone
+1FAF1 1F3FF ; fully-qualified # 🫱🏿 E14.0 rightwards hand: dark skin tone
+1FAF2 ; fully-qualified # 🫲 E14.0 leftwards hand
+1FAF2 1F3FB ; fully-qualified # 🫲🏻 E14.0 leftwards hand: light skin tone
+1FAF2 1F3FC ; fully-qualified # 🫲🏼 E14.0 leftwards hand: medium-light skin tone
+1FAF2 1F3FD ; fully-qualified # 🫲🏽 E14.0 leftwards hand: medium skin tone
+1FAF2 1F3FE ; fully-qualified # 🫲🏾 E14.0 leftwards hand: medium-dark skin tone
+1FAF2 1F3FF ; fully-qualified # 🫲🏿 E14.0 leftwards hand: dark skin tone
+1FAF3 ; fully-qualified # 🫳 E14.0 palm down hand
+1FAF3 1F3FB ; fully-qualified # 🫳🏻 E14.0 palm down hand: light skin tone
+1FAF3 1F3FC ; fully-qualified # 🫳🏼 E14.0 palm down hand: medium-light skin tone
+1FAF3 1F3FD ; fully-qualified # 🫳🏽 E14.0 palm down hand: medium skin tone
+1FAF3 1F3FE ; fully-qualified # 🫳🏾 E14.0 palm down hand: medium-dark skin tone
+1FAF3 1F3FF ; fully-qualified # 🫳🏿 E14.0 palm down hand: dark skin tone
+1FAF4 ; fully-qualified # 🫴 E14.0 palm up hand
+1FAF4 1F3FB ; fully-qualified # 🫴🏻 E14.0 palm up hand: light skin tone
+1FAF4 1F3FC ; fully-qualified # 🫴🏼 E14.0 palm up hand: medium-light skin tone
+1FAF4 1F3FD ; fully-qualified # 🫴🏽 E14.0 palm up hand: medium skin tone
+1FAF4 1F3FE ; fully-qualified # 🫴🏾 E14.0 palm up hand: medium-dark skin tone
+1FAF4 1F3FF ; fully-qualified # 🫴🏿 E14.0 palm up hand: dark skin tone
# subgroup: hand-fingers-partial
1F44C ; fully-qualified # 👌 E0.6 OK hand
1F91E 1F3FD ; fully-qualified # 🤞🏽 E3.0 crossed fingers: medium skin tone
1F91E 1F3FE ; fully-qualified # 🤞🏾 E3.0 crossed fingers: medium-dark skin tone
1F91E 1F3FF ; fully-qualified # 🤞🏿 E3.0 crossed fingers: dark skin tone
+1FAF0 ; fully-qualified # 🫰 E14.0 hand with index finger and thumb crossed
+1FAF0 1F3FB ; fully-qualified # 🫰🏻 E14.0 hand with index finger and thumb crossed: light skin tone
+1FAF0 1F3FC ; fully-qualified # 🫰🏼 E14.0 hand with index finger and thumb crossed: medium-light skin tone
+1FAF0 1F3FD ; fully-qualified # 🫰🏽 E14.0 hand with index finger and thumb crossed: medium skin tone
+1FAF0 1F3FE ; fully-qualified # 🫰🏾 E14.0 hand with index finger and thumb crossed: medium-dark skin tone
+1FAF0 1F3FF ; fully-qualified # 🫰🏿 E14.0 hand with index finger and thumb crossed: dark skin tone
1F91F ; fully-qualified # 🤟 E5.0 love-you gesture
1F91F 1F3FB ; fully-qualified # 🤟🏻 E5.0 love-you gesture: light skin tone
1F91F 1F3FC ; fully-qualified # 🤟🏼 E5.0 love-you gesture: medium-light skin tone
261D 1F3FD ; fully-qualified # ☝🏽 E1.0 index pointing up: medium skin tone
261D 1F3FE ; fully-qualified # ☝🏾 E1.0 index pointing up: medium-dark skin tone
261D 1F3FF ; fully-qualified # ☝🏿 E1.0 index pointing up: dark skin tone
+1FAF5 ; fully-qualified # 🫵 E14.0 index pointing at the viewer
+1FAF5 1F3FB ; fully-qualified # 🫵🏻 E14.0 index pointing at the viewer: light skin tone
+1FAF5 1F3FC ; fully-qualified # 🫵🏼 E14.0 index pointing at the viewer: medium-light skin tone
+1FAF5 1F3FD ; fully-qualified # 🫵🏽 E14.0 index pointing at the viewer: medium skin tone
+1FAF5 1F3FE ; fully-qualified # 🫵🏾 E14.0 index pointing at the viewer: medium-dark skin tone
+1FAF5 1F3FF ; fully-qualified # 🫵🏿 E14.0 index pointing at the viewer: dark skin tone
# subgroup: hand-fingers-closed
1F44D ; fully-qualified # 👍 E0.6 thumbs up
1F64C 1F3FD ; fully-qualified # 🙌🏽 E1.0 raising hands: medium skin tone
1F64C 1F3FE ; fully-qualified # 🙌🏾 E1.0 raising hands: medium-dark skin tone
1F64C 1F3FF ; fully-qualified # 🙌🏿 E1.0 raising hands: dark skin tone
+1FAF6 ; fully-qualified # 🫶 E14.0 heart hands
+1FAF6 1F3FB ; fully-qualified # 🫶🏻 E14.0 heart hands: light skin tone
+1FAF6 1F3FC ; fully-qualified # 🫶🏼 E14.0 heart hands: medium-light skin tone
+1FAF6 1F3FD ; fully-qualified # 🫶🏽 E14.0 heart hands: medium skin tone
+1FAF6 1F3FE ; fully-qualified # 🫶🏾 E14.0 heart hands: medium-dark skin tone
+1FAF6 1F3FF ; fully-qualified # 🫶🏿 E14.0 heart hands: dark skin tone
1F450 ; fully-qualified # 👐 E0.6 open hands
1F450 1F3FB ; fully-qualified # 👐🏻 E1.0 open hands: light skin tone
1F450 1F3FC ; fully-qualified # 👐🏼 E1.0 open hands: medium-light skin tone
1F932 1F3FE ; fully-qualified # 🤲🏾 E5.0 palms up together: medium-dark skin tone
1F932 1F3FF ; fully-qualified # 🤲🏿 E5.0 palms up together: dark skin tone
1F91D ; fully-qualified # 🤝 E3.0 handshake
+1F91D 1F3FB ; fully-qualified # 🤝🏻 E3.0 handshake: light skin tone
+1F91D 1F3FC ; fully-qualified # 🤝🏼 E3.0 handshake: medium-light skin tone
+1F91D 1F3FD ; fully-qualified # 🤝🏽 E3.0 handshake: medium skin tone
+1F91D 1F3FE ; fully-qualified # 🤝🏾 E3.0 handshake: medium-dark skin tone
+1F91D 1F3FF ; fully-qualified # 🤝🏿 E3.0 handshake: dark skin tone
+1FAF1 1F3FB 200D 1FAF2 1F3FC ; fully-qualified # 🫱🏻🫲🏼 E14.0 handshake: light skin tone, medium-light skin tone
+1FAF1 1F3FB 200D 1FAF2 1F3FD ; fully-qualified # 🫱🏻🫲🏽 E14.0 handshake: light skin tone, medium skin tone
+1FAF1 1F3FB 200D 1FAF2 1F3FE ; fully-qualified # 🫱🏻🫲🏾 E14.0 handshake: light skin tone, medium-dark skin tone
+1FAF1 1F3FB 200D 1FAF2 1F3FF ; fully-qualified # 🫱🏻🫲🏿 E14.0 handshake: light skin tone, dark skin tone
+1FAF1 1F3FC 200D 1FAF2 1F3FB ; fully-qualified # 🫱🏼🫲🏻 E14.0 handshake: medium-light skin tone, light skin tone
+1FAF1 1F3FC 200D 1FAF2 1F3FD ; fully-qualified # 🫱🏼🫲🏽 E14.0 handshake: medium-light skin tone, medium skin tone
+1FAF1 1F3FC 200D 1FAF2 1F3FE ; fully-qualified # 🫱🏼🫲🏾 E14.0 handshake: medium-light skin tone, medium-dark skin tone
+1FAF1 1F3FC 200D 1FAF2 1F3FF ; fully-qualified # 🫱🏼🫲🏿 E14.0 handshake: medium-light skin tone, dark skin tone
+1FAF1 1F3FD 200D 1FAF2 1F3FB ; fully-qualified # 🫱🏽🫲🏻 E14.0 handshake: medium skin tone, light skin tone
+1FAF1 1F3FD 200D 1FAF2 1F3FC ; fully-qualified # 🫱🏽🫲🏼 E14.0 handshake: medium skin tone, medium-light skin tone
+1FAF1 1F3FD 200D 1FAF2 1F3FE ; fully-qualified # 🫱🏽🫲🏾 E14.0 handshake: medium skin tone, medium-dark skin tone
+1FAF1 1F3FD 200D 1FAF2 1F3FF ; fully-qualified # 🫱🏽🫲🏿 E14.0 handshake: medium skin tone, dark skin tone
+1FAF1 1F3FE 200D 1FAF2 1F3FB ; fully-qualified # 🫱🏾🫲🏻 E14.0 handshake: medium-dark skin tone, light skin tone
+1FAF1 1F3FE 200D 1FAF2 1F3FC ; fully-qualified # 🫱🏾🫲🏼 E14.0 handshake: medium-dark skin tone, medium-light skin tone
+1FAF1 1F3FE 200D 1FAF2 1F3FD ; fully-qualified # 🫱🏾🫲🏽 E14.0 handshake: medium-dark skin tone, medium skin tone
+1FAF1 1F3FE 200D 1FAF2 1F3FF ; fully-qualified # 🫱🏾🫲🏿 E14.0 handshake: medium-dark skin tone, dark skin tone
+1FAF1 1F3FF 200D 1FAF2 1F3FB ; fully-qualified # 🫱🏿🫲🏻 E14.0 handshake: dark skin tone, light skin tone
+1FAF1 1F3FF 200D 1FAF2 1F3FC ; fully-qualified # 🫱🏿🫲🏼 E14.0 handshake: dark skin tone, medium-light skin tone
+1FAF1 1F3FF 200D 1FAF2 1F3FD ; fully-qualified # 🫱🏿🫲🏽 E14.0 handshake: dark skin tone, medium skin tone
+1FAF1 1F3FF 200D 1FAF2 1F3FE ; fully-qualified # 🫱🏿🫲🏾 E14.0 handshake: dark skin tone, medium-dark skin tone
1F64F ; fully-qualified # 🙏 E0.6 folded hands
1F64F 1F3FB ; fully-qualified # 🙏🏻 E1.0 folded hands: light skin tone
1F64F 1F3FC ; fully-qualified # 🙏🏼 E1.0 folded hands: medium-light skin tone
1F441 ; unqualified # 👁 E0.7 eye
1F445 ; fully-qualified # 👅 E0.6 tongue
1F444 ; fully-qualified # 👄 E0.6 mouth
+1FAE6 ; fully-qualified # 🫦 E14.0 biting lip
# subgroup: person
1F476 ; fully-qualified # 👶 E0.6 baby
1F477 1F3FE 200D 2640 ; minimally-qualified # 👷🏾♀ E4.0 woman construction worker: medium-dark skin tone
1F477 1F3FF 200D 2640 FE0F ; fully-qualified # 👷🏿♀️ E4.0 woman construction worker: dark skin tone
1F477 1F3FF 200D 2640 ; minimally-qualified # 👷🏿♀ E4.0 woman construction worker: dark skin tone
+1FAC5 ; fully-qualified # 🫅 E14.0 person with crown
+1FAC5 1F3FB ; fully-qualified # 🫅🏻 E14.0 person with crown: light skin tone
+1FAC5 1F3FC ; fully-qualified # 🫅🏼 E14.0 person with crown: medium-light skin tone
+1FAC5 1F3FD ; fully-qualified # 🫅🏽 E14.0 person with crown: medium skin tone
+1FAC5 1F3FE ; fully-qualified # 🫅🏾 E14.0 person with crown: medium-dark skin tone
+1FAC5 1F3FF ; fully-qualified # 🫅🏿 E14.0 person with crown: dark skin tone
1F934 ; fully-qualified # 🤴 E3.0 prince
1F934 1F3FB ; fully-qualified # 🤴🏻 E3.0 prince: light skin tone
1F934 1F3FC ; fully-qualified # 🤴🏼 E3.0 prince: medium-light skin tone
1F930 1F3FD ; fully-qualified # 🤰🏽 E3.0 pregnant woman: medium skin tone
1F930 1F3FE ; fully-qualified # 🤰🏾 E3.0 pregnant woman: medium-dark skin tone
1F930 1F3FF ; fully-qualified # 🤰🏿 E3.0 pregnant woman: dark skin tone
+1FAC3 ; fully-qualified # 🫃 E14.0 pregnant man
+1FAC3 1F3FB ; fully-qualified # 🫃🏻 E14.0 pregnant man: light skin tone
+1FAC3 1F3FC ; fully-qualified # 🫃🏼 E14.0 pregnant man: medium-light skin tone
+1FAC3 1F3FD ; fully-qualified # 🫃🏽 E14.0 pregnant man: medium skin tone
+1FAC3 1F3FE ; fully-qualified # 🫃🏾 E14.0 pregnant man: medium-dark skin tone
+1FAC3 1F3FF ; fully-qualified # 🫃🏿 E14.0 pregnant man: dark skin tone
+1FAC4 ; fully-qualified # 🫄 E14.0 pregnant person
+1FAC4 1F3FB ; fully-qualified # 🫄🏻 E14.0 pregnant person: light skin tone
+1FAC4 1F3FC ; fully-qualified # 🫄🏼 E14.0 pregnant person: medium-light skin tone
+1FAC4 1F3FD ; fully-qualified # 🫄🏽 E14.0 pregnant person: medium skin tone
+1FAC4 1F3FE ; fully-qualified # 🫄🏾 E14.0 pregnant person: medium-dark skin tone
+1FAC4 1F3FF ; fully-qualified # 🫄🏿 E14.0 pregnant person: dark skin tone
1F931 ; fully-qualified # 🤱 E5.0 breast-feeding
1F931 1F3FB ; fully-qualified # 🤱🏻 E5.0 breast-feeding: light skin tone
1F931 1F3FC ; fully-qualified # 🤱🏼 E5.0 breast-feeding: medium-light skin tone
1F9DF 200D 2642 ; minimally-qualified # 🧟♂ E5.0 man zombie
1F9DF 200D 2640 FE0F ; fully-qualified # 🧟♀️ E5.0 woman zombie
1F9DF 200D 2640 ; minimally-qualified # 🧟♀ E5.0 woman zombie
+1F9CC ; fully-qualified # 🧌 E14.0 troll
# subgroup: person-activity
1F486 ; fully-qualified # 💆 E0.6 person getting massage
1FAC2 ; fully-qualified # 🫂 E13.0 people hugging
1F463 ; fully-qualified # 👣 E0.6 footprints
-# People & Body subtotal: 2899
-# People & Body subtotal: 494 w/o modifiers
+# People & Body subtotal: 2986
+# People & Body subtotal: 506 w/o modifiers
# group: Component
1F988 ; fully-qualified # 🦈 E3.0 shark
1F419 ; fully-qualified # 🐙 E0.6 octopus
1F41A ; fully-qualified # 🐚 E0.6 spiral shell
+1FAB8 ; fully-qualified # 🪸 E14.0 coral
# subgroup: animal-bug
1F40C ; fully-qualified # 🐌 E0.6 snail
1F490 ; fully-qualified # 💐 E0.6 bouquet
1F338 ; fully-qualified # 🌸 E0.6 cherry blossom
1F4AE ; fully-qualified # 💮 E0.6 white flower
+1FAB7 ; fully-qualified # 🪷 E14.0 lotus
1F3F5 FE0F ; fully-qualified # 🏵️ E0.7 rosette
1F3F5 ; unqualified # 🏵 E0.7 rosette
1F339 ; fully-qualified # 🌹 E0.6 rose
1F341 ; fully-qualified # 🍁 E0.6 maple leaf
1F342 ; fully-qualified # 🍂 E0.6 fallen leaf
1F343 ; fully-qualified # 🍃 E0.6 leaf fluttering in wind
+1FAB9 ; fully-qualified # 🪹 E14.0 empty nest
+1FABA ; fully-qualified # 🪺 E14.0 nest with eggs
-# Animals & Nature subtotal: 147
-# Animals & Nature subtotal: 147 w/o modifiers
+# Animals & Nature subtotal: 151
+# Animals & Nature subtotal: 151 w/o modifiers
# group: Food & Drink
1F9C5 ; fully-qualified # 🧅 E12.0 onion
1F344 ; fully-qualified # 🍄 E0.6 mushroom
1F95C ; fully-qualified # 🥜 E3.0 peanuts
+1FAD8 ; fully-qualified # 🫘 E14.0 beans
1F330 ; fully-qualified # 🌰 E0.6 chestnut
# subgroup: food-prepared
1F37B ; fully-qualified # 🍻 E0.6 clinking beer mugs
1F942 ; fully-qualified # 🥂 E3.0 clinking glasses
1F943 ; fully-qualified # 🥃 E3.0 tumbler glass
+1FAD7 ; fully-qualified # 🫗 E14.0 pouring liquid
1F964 ; fully-qualified # 🥤 E5.0 cup with straw
1F9CB ; fully-qualified # 🧋 E13.0 bubble tea
1F9C3 ; fully-qualified # 🧃 E12.0 beverage box
1F374 ; fully-qualified # 🍴 E0.6 fork and knife
1F944 ; fully-qualified # 🥄 E3.0 spoon
1F52A ; fully-qualified # 🔪 E0.6 kitchen knife
+1FAD9 ; fully-qualified # 🫙 E14.0 jar
1F3FA ; fully-qualified # 🏺 E1.0 amphora
-# Food & Drink subtotal: 131
-# Food & Drink subtotal: 131 w/o modifiers
+# Food & Drink subtotal: 134
+# Food & Drink subtotal: 134 w/o modifiers
# group: Travel & Places
2668 FE0F ; fully-qualified # ♨️ E0.6 hot springs
2668 ; unqualified # ♨ E0.6 hot springs
1F3A0 ; fully-qualified # 🎠 E0.6 carousel horse
+1F6DD ; fully-qualified # 🛝 E14.0 playground slide
1F3A1 ; fully-qualified # 🎡 E0.6 ferris wheel
1F3A2 ; fully-qualified # 🎢 E0.6 roller coaster
1F488 ; fully-qualified # 💈 E0.6 barber pole
1F6E2 FE0F ; fully-qualified # 🛢️ E0.7 oil drum
1F6E2 ; unqualified # 🛢 E0.7 oil drum
26FD ; fully-qualified # ⛽ E0.6 fuel pump
+1F6DE ; fully-qualified # 🛞 E14.0 wheel
1F6A8 ; fully-qualified # 🚨 E0.6 police car light
1F6A5 ; fully-qualified # 🚥 E0.6 horizontal traffic light
1F6A6 ; fully-qualified # 🚦 E1.0 vertical traffic light
# subgroup: transport-water
2693 ; fully-qualified # ⚓ E0.6 anchor
+1F6DF ; fully-qualified # 🛟 E14.0 ring buoy
26F5 ; fully-qualified # ⛵ E0.6 sailboat
1F6F6 ; fully-qualified # 🛶 E3.0 canoe
1F6A4 ; fully-qualified # 🚤 E0.6 speedboat
1F4A7 ; fully-qualified # 💧 E0.6 droplet
1F30A ; fully-qualified # 🌊 E0.6 water wave
-# Travel & Places subtotal: 264
-# Travel & Places subtotal: 264 w/o modifiers
+# Travel & Places subtotal: 267
+# Travel & Places subtotal: 267 w/o modifiers
# group: Activities
1F52E ; fully-qualified # 🔮 E0.6 crystal ball
1FA84 ; fully-qualified # 🪄 E13.0 magic wand
1F9FF ; fully-qualified # 🧿 E11.0 nazar amulet
+1FAAC ; fully-qualified # 🪬 E14.0 hamsa
1F3AE ; fully-qualified # 🎮 E0.6 video game
1F579 FE0F ; fully-qualified # 🕹️ E0.7 joystick
1F579 ; unqualified # 🕹 E0.7 joystick
1F9E9 ; fully-qualified # 🧩 E11.0 puzzle piece
1F9F8 ; fully-qualified # 🧸 E11.0 teddy bear
1FA85 ; fully-qualified # 🪅 E13.0 piñata
+1FAA9 ; fully-qualified # 🪩 E14.0 mirror ball
1FA86 ; fully-qualified # 🪆 E13.0 nesting dolls
2660 FE0F ; fully-qualified # ♠️ E0.6 spade suit
2660 ; unqualified # ♠ E0.6 spade suit
1F9F6 ; fully-qualified # 🧶 E11.0 yarn
1FAA2 ; fully-qualified # 🪢 E13.0 knot
-# Activities subtotal: 95
-# Activities subtotal: 95 w/o modifiers
+# Activities subtotal: 97
+# Activities subtotal: 97 w/o modifiers
# group: Objects
# subgroup: computer
1F50B ; fully-qualified # 🔋 E0.6 battery
+1FAAB ; fully-qualified # 🪫 E14.0 low battery
1F50C ; fully-qualified # 🔌 E0.6 electric plug
1F4BB ; fully-qualified # 💻 E0.6 laptop
1F5A5 FE0F ; fully-qualified # 🖥️ E0.7 desktop computer
1FA78 ; fully-qualified # 🩸 E12.0 drop of blood
1F48A ; fully-qualified # 💊 E0.6 pill
1FA79 ; fully-qualified # 🩹 E12.0 adhesive bandage
+1FA7C ; fully-qualified # 🩼 E14.0 crutch
1FA7A ; fully-qualified # 🩺 E12.0 stethoscope
+1FA7B ; fully-qualified # 🩻 E14.0 x-ray
# subgroup: household
1F6AA ; fully-qualified # 🚪 E0.6 door
1F9FB ; fully-qualified # 🧻 E11.0 roll of paper
1FAA3 ; fully-qualified # 🪣 E13.0 bucket
1F9FC ; fully-qualified # 🧼 E11.0 soap
+1FAE7 ; fully-qualified # 🫧 E14.0 bubbles
1FAA5 ; fully-qualified # 🪥 E13.0 toothbrush
1F9FD ; fully-qualified # 🧽 E11.0 sponge
1F9EF ; fully-qualified # 🧯 E11.0 fire extinguisher
26B1 ; unqualified # ⚱ E1.0 funeral urn
1F5FF ; fully-qualified # 🗿 E0.6 moai
1FAA7 ; fully-qualified # 🪧 E13.0 placard
+1FAAA ; fully-qualified # 🪪 E14.0 identification card
-# Objects subtotal: 299
-# Objects subtotal: 299 w/o modifiers
+# Objects subtotal: 304
+# Objects subtotal: 304 w/o modifiers
# group: Symbols
2795 ; fully-qualified # ➕ E0.6 plus
2796 ; fully-qualified # ➖ E0.6 minus
2797 ; fully-qualified # ➗ E0.6 divide
+1F7F0 ; fully-qualified # 🟰 E14.0 heavy equals sign
267E FE0F ; fully-qualified # ♾️ E11.0 infinity
267E ; unqualified # ♾ E11.0 infinity
1F533 ; fully-qualified # 🔳 E0.6 white square button
1F532 ; fully-qualified # 🔲 E0.6 black square button
-# Symbols subtotal: 301
-# Symbols subtotal: 301 w/o modifiers
+# Symbols subtotal: 302
+# Symbols subtotal: 302 w/o modifiers
# group: Flags
# Flags subtotal: 275 w/o modifiers
# Status Counts
-# fully-qualified : 3512
+# fully-qualified : 3624
# minimally-qualified : 817
# unqualified : 252
# component : 9
{: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}
|> 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)
defmodule Pleroma.Search do
- @type search_map :: %{
- statuses: [map],
- accounts: [map],
- hashtags: [map]
- }
-
- @doc """
- Searches for stuff
- """
- @callback search(map, map, keyword) :: search_map
+ alias Pleroma.Workers.SearchIndexingWorker
+
+ def add_to_index(%Pleroma.Activity{id: activity_id}) do
+ SearchIndexingWorker.enqueue("add_to_index", %{"activity" => activity_id})
+ end
+
+ def remove_from_index(%Pleroma.Object{id: object_id}) do
+ SearchIndexingWorker.enqueue("remove_from_index", %{"object" => object_id})
+ end
+
+ def search(query, options) do
+ search_module = Pleroma.Config.get([Pleroma.Search, :module], Pleroma.Activity)
+
+ search_module.search(options[:for_user], query, options)
+ end
end
+++ /dev/null
-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
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Activity.Search do
+defmodule Pleroma.Search.DatabaseSearch do
alias Pleroma.Activity
alias Pleroma.Object.Fetcher
alias Pleroma.Pagination
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])
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
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
)
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
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"]),
# 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: [
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),
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()
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 =
[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
--- /dev/null
+defmodule Pleroma.Search.Elasticsearch.Cluster do
+ @moduledoc false
+ use Elasticsearch.Cluster, otp_app: :pleroma
+end
--- /dev/null
+# Akkoma: A lightweight social networking server
+# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defimpl Elasticsearch.Document, for: Pleroma.Activity do
+ alias Pleroma.Object
+ require Pleroma.Constants
+
+ def id(obj), do: obj.id
+ def routing(_), do: false
+
+ def object_to_search_data(object) do
+ # Only index public or unlisted Notes
+ if not is_nil(object) and object.data["type"] == "Note" and
+ not is_nil(object.data["content"]) and
+ (Pleroma.Constants.as_public() in object.data["to"] or
+ Pleroma.Constants.as_public() in object.data["cc"]) and
+ String.length(object.data["content"]) > 1 do
+ data = object.data
+
+ content_str =
+ case data["content"] do
+ [nil | rest] -> to_string(rest)
+ str -> str
+ end
+
+ content =
+ with {:ok, scrubbed} <- FastSanitize.strip_tags(content_str),
+ trimmed <- String.trim(scrubbed) do
+ trimmed
+ end
+
+ if String.length(content) > 1 do
+ {:ok, published, _} = DateTime.from_iso8601(data["published"])
+
+ %{
+ _timestamp: published,
+ content: content,
+ instance: URI.parse(object.data["actor"]).host,
+ hashtags: Object.hashtags(object),
+ user: Pleroma.User.get_cached_by_ap_id(object.data["actor"]).nickname
+ }
+ else
+ %{}
+ end
+ else
+ %{}
+ end
+ end
+
+ def encode(activity) do
+ object = Pleroma.Object.normalize(activity)
+ object_to_search_data(object)
+ end
+end
+
+defimpl Elasticsearch.Document, for: Pleroma.Object do
+ def id(obj), do: obj.id
+ def routing(_), do: false
+ def encode(_), do: nil
+end
+++ /dev/null
-# Akkoma: A lightweight social networking server
-# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Search.Elasticsearch.Parsers.Hashtag do
- defp to_es(term) when is_binary(term) do
- %{
- term: %{
- hashtag: %{
- value: String.downcase(term)
- }
- }
- }
- end
-
- defp to_es({:quoted, term}), do: to_es(term)
-
- defp to_es({:filter, ["hashtag", query]}) do
- %{
- term: %{
- hashtag: %{
- value: String.downcase(query)
- }
- }
- }
- end
-
- defp to_es({:filter, _}), do: nil
-
- def parse(q) do
- Enum.map(q, &to_es/1)
- |> Enum.filter(fn x -> x != nil end)
- end
-end
--- /dev/null
+# Akkoma: A lightweight social networking server
+# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Search.Elasticsearch.Store do
+ @behaviour Elasticsearch.Store
+ alias Pleroma.Search.Elasticsearch.Cluster
+ require Logger
+
+ alias Pleroma.Repo
+
+ @impl true
+ def stream(schema) do
+ Repo.stream(schema)
+ end
+
+ @impl true
+ def transaction(fun) do
+ {:ok, result} = Repo.transaction(fun, timeout: :infinity)
+ result
+ end
+
+ def search(_, _, _, :skip), do: []
+
+ def search(:raw, index, q) do
+ with {:ok, raw_results} <- Elasticsearch.post(Cluster, "/#{index}/_search", q) do
+ results =
+ raw_results
+ |> Map.get("hits", %{})
+ |> Map.get("hits", [])
+
+ {:ok, results}
+ else
+ {:error, e} ->
+ Logger.error(e)
+ {:error, e}
+ end
+ end
+
+ def search(:activities, q) do
+ with {:ok, results} <- search(:raw, "activities", q) do
+ results
+ |> Enum.map(fn result -> result["_id"] end)
+ |> Pleroma.Activity.all_by_ids_with_object()
+ |> Enum.sort(&(&1.inserted_at >= &2.inserted_at))
+ else
+ e ->
+ Logger.error(e)
+ []
+ end
+ end
+end
+++ /dev/null
-# Akkoma: A lightweight social networking server
-# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Search.Elasticsearch.Parsers.User do
- defp to_es(term) when is_binary(term) do
- %{
- bool: %{
- minimum_should_match: 1,
- should: [
- %{
- match: %{
- bio: %{
- query: term,
- operator: "AND"
- }
- }
- },
- %{
- term: %{
- nickname: %{
- value: term
- }
- }
- },
- %{
- match: %{
- display_name: %{
- query: term,
- operator: "AND"
- }
- }
- }
- ]
- }
- }
- end
-
- defp to_es({:quoted, term}), do: to_es(term)
-
- defp to_es({:filter, ["user", query]}) do
- %{
- term: %{
- nickname: %{
- value: query
- }
- }
- }
- end
-
- defp to_es({:filter, _}), do: nil
-
- def parse(q) do
- Enum.map(q, &to_es/1)
- |> Enum.filter(fn x -> x != nil end)
- end
-end
--- /dev/null
+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
--- /dev/null
+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
[: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(
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
field(:pinned_objects, :map, default: %{})
field(:is_suggested, :boolean, default: false)
field(:last_status_at, :naive_datetime)
+ field(:language, :string)
embeds_one(
:notification_settings,
:password_confirmation,
:emoji,
:accepts_chat_messages,
- :registration_reason
+ :registration_reason,
+ :language
])
|> validate_required([:name, :nickname, :password, :password_confirmation])
|> validate_confirmation(:password)
|> 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
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 ->
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
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}"}
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)
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)
%{
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,
|> fix_replies()
|> fix_source()
|> fix_misskey_content()
+ |> Transmogrifier.fix_attachments()
|> Transmogrifier.fix_emoji()
|> Transmogrifier.fix_content_map()
end
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} ->
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.SideEffects do
# - 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),
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)
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}
# - 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 =
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}
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()
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.SideEffects.Handling do
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: %{
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
alias Pleroma.Formatter
alias Pleroma.Object
alias Pleroma.User
+ alias Pleroma.Web.Gettext
alias Pleroma.Web.MediaProxy
require Pleroma.Constants
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
|> 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:
#
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.SearchController do
use Pleroma.Web, :controller
+ alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ControllerHelper
+ alias Pleroma.Web.Endpoint
alias Pleroma.Web.MastodonAPI.AccountView
+ alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.Plugs.RateLimiter
def search2(conn, params), do: do_search(:v2, conn, params)
def search(conn, params), do: do_search(:v1, conn, params)
- defp do_search(version, %{assigns: %{user: user}} = conn, params) do
- options =
- search_options(params, user)
- |> Keyword.put(:version, version)
-
- search_provider = Pleroma.Config.get([:search, :provider])
- json(conn, search_provider.search(conn, params, options))
+ defp do_search(version, %{assigns: %{user: user}} = conn, %{q: query} = params) do
+ query = String.trim(query)
+ options = search_options(params, user)
+ timeout = Keyword.get(Repo.config(), :timeout, 15_000)
+ default_values = %{"statuses" => [], "accounts" => [], "hashtags" => []}
+
+ result =
+ default_values
+ |> Enum.map(fn {resource, default_value} ->
+ if params[:type] in [nil, resource] do
+ {resource, fn -> resource_search(version, resource, query, options) end}
+ else
+ {resource, fn -> default_value end}
+ end
+ end)
+ |> Task.async_stream(fn {resource, f} -> {resource, with_fallback(f)} end,
+ timeout: timeout,
+ on_timeout: :kill_task
+ )
+ |> Enum.reduce(default_values, fn
+ {:ok, {resource, result}}, acc ->
+ Map.put(acc, resource, result)
+
+ _error, acc ->
+ acc
+ end)
+
+ json(conn, result)
end
defp search_options(params, user) do
|> 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)
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
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
%{
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
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
|> Enum.sort(&(&1.quality > &2.quality))
|> Enum.map(& &1.tag)
|> Enum.reject(&is_nil/1)
- |> ensure_language_fallbacks()
_ ->
[]
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
%{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
<div
style="font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height: 14px; color: <%= @styling.header_color %>;">
<p style="line-height: 36px; text-align: center; margin: 0;"><span
- style="font-size: 30px; color: <%= @styling.header_color %>;">Hey <%= @user.nickname %>, here is what you've missed!</span></p>
+ style="font-size: 30px; color: <%= @styling.header_color %>;"><%= Gettext.dpgettext("static_pages", "digest email header line", "Hey %{nickname}, here is what you've missed!", nickname: @user.nickname) %></span></p>
</div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<div
style="font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size: 12px; line-height: 14px; color: <%= @styling.text_color %>;">
<p style="font-size: 12px; line-height: 24px; text-align: center; margin: 0;"><span
- style="font-size: 20px;"><%= length(@followers) %> New Followers</span><span
+ style="font-size: 20px;"><%= Gettext.dpngettext("static_pages", "new followers count header", "%{count} New Follower", "%{count} New Followers", length(@followers), count: length(@followers)) %></span><span
style="font-size: 20px; line-height: 24px;"></span></p>
</div>
</div>
style="color:<%= @styling.text_color %>;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<p
style="font-size: 12px; line-height: 16px; text-align: center; color: <%= @styling.text_color %>; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin: 0;">
- <span style="font-size: 14px;">You have received this email because you have signed up to receive digest emails from <b><%= @instance %></b> Pleroma instance.</span></p>
+ <span style="font-size: 14px;"><%= raw Gettext.dpgettext("static_pages", "digest email sending reason", "You have received this email because you have signed up to receive digest emails from <b>%{instance}</b> Pleroma instance.", instance: safe_to_string(html_escape(@instance))) %></span></p>
<p
style="font-size: 12px; line-height: 14px; text-align: center; color: <%= @styling.text_color %>; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin: 0;">
</p>
<p
style="font-size: 12px; line-height: 16px; text-align: center; color: <%= @styling.text_color %>; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin: 0;">
- <span style="font-size: 14px;">The email address you are subscribed as is <a href="mailto:<%= @user.email %>" style="color: <%= @styling.link_color %>;text-decoration: none;"><%= @user.email %></a>. </span></p>
+ <span style="font-size: 14px;"><%= raw Gettext.dpgettext("static_pages", "digest email receiver address", "The email address you are subscribed as is <a href='mailto:%{@user.email}' style='color: %{color};text-decoration: none;'>%{email}</a>. ", color: safe_to_string(html_escape(@styling.link_color)), email: safe_to_string(html_escape(@user.email))) %></span></p>
<p
style="font-size: 12px; line-height: 16px; text-align: center; color: <%= @styling.text_color %>; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin: 0;">
- <span style="font-size: 14px;">To unsubscribe, please go <%= link "here", style: "color: #{@styling.link_color};text-decoration: none;", to: @unsubscribe_link %>.</span></p>
+ <span style="font-size: 14px;"><%= raw Gettext.dpgettext("static_pages", "digest email unsubscribe action", "To unsubscribe, please go %{here}.", here: safe_to_string link(Gettext.dpgettext("static_pages", "digest email unsubscribe action link text", "here"), style: "color: #{@styling.link_color};text-decoration: none;", to: @unsubscribe_link)) %></span></p>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if (!mso)&(!IE)]><!-->
<?xml version="1.0" encoding="UTF-8"?>
-<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom"
+<feed xml:lang="<%= Gettext.language_tag() %>" xmlns="http://www.w3.org/2005/Atom"
xmlns:thr="http://purl.org/syndication/thread/1.0"
xmlns:georss="http://www.georss.org/georss"
xmlns:activity="http://activitystrea.ms/spec/1.0/"
<id><%= '#{Routes.tag_feed_url(@conn, :feed, @tag)}.rss' %></id>
<title>#<%= @tag %></title>
- <subtitle>These are public toots tagged with #<%= @tag %>. You can interact with them if you have an account anywhere in the fediverse.</subtitle>
+ <subtitle><%= Gettext.dpgettext("static_pages", "tag feed description", "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse.", tag: @tag) %></subtitle>
<logo><%= feed_logo() %></logo>
<updated><%= most_recent_update(@activities) %></updated>
<link rel="self" href="<%= '#{Routes.tag_feed_url(@conn, :feed, @tag)}.atom' %>" type="application/atom+xml"/>
<title>#<%= @tag %></title>
- <description>These are public toots tagged with #<%= @tag %>. You can interact with them if you have an account anywhere in the fediverse.</description>
+ <description><%= Gettext.dpgettext("static_pages", "tag feed description", "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse.", tag: @tag) %></description>
<link><%= '#{Routes.tag_feed_url(@conn, :feed, @tag)}.rss' %></link>
<webfeeds:logo><%= feed_logo() %></webfeeds:logo>
<webfeeds:accentColor>2b90d9</webfeeds:accentColor>
<!DOCTYPE html>
-<html lang="en">
+<html lang="<%= Pleroma.Web.Gettext.language_tag() %>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui">
<!DOCTYPE html>
-<html lang="en">
+<html lang="<%= Pleroma.Web.Gettext.language_tag() %>">
<head>
<meta charset="utf-8">
<title><%= @email.subject %></title>
<body>
<%= render @view_module, @view_template, assigns %>
</body>
-</html>
\ No newline at end of file
+</html>
-<h1>UNSUBSCRIBE FAILURE</h1>
+<h1><%= Gettext.dpgettext("static_pages", "mailer unsubscribe failed message", "UNSUBSCRIBE FAILURE") %></h1>
-<h1>UNSUBSCRIBE SUCCESSFUL</h1>
+<h1><%= Gettext.dpgettext("static_pages", "mailer unsubscribe successful message", "UNSUBSCRIBE SUCCESSFUL") %></h1>
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
<% end %>
-<h2>Two-factor recovery</h2>
+<h2><%= Gettext.dpgettext("static_pages", "mfa recover page title", "Two-factor recovery") %></h2>
<%= form_for @conn, Routes.mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %>
<div class="input">
- <%= label f, :code, "Recovery code" %>
+ <%= label f, :code, Gettext.dpgettext("static_pages", "mfa recover recovery code prompt", "Recovery code") %>
<%= text_input f, :code, [autocomplete: false, autocorrect: "off", autocapitalize: "off", autofocus: true, spellcheck: false] %>
<%= hidden_input f, :mfa_token, value: @mfa_token %>
<%= hidden_input f, :state, value: @state %>
<%= hidden_input f, :challenge_type, value: "recovery" %>
</div>
-<%= submit "Verify" %>
+<%= submit Gettext.dpgettext("static_pages", "mfa recover verify recovery code button", "Verify") %>
<% end %>
<a href="<%= Routes.mfa_path(@conn, :show, %{challenge_type: "totp", mfa_token: @mfa_token, state: @state, redirect_uri: @redirect_uri}) %>">
- Enter a two-factor code
+ <%= Gettext.dpgettext("static_pages", "mfa recover use 2fa code link", "Enter a two-factor code") %>
</a>
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
<% end %>
-<h2>Two-factor authentication</h2>
+<h2><%= Gettext.dpgettext("static_pages", "mfa auth page title", "Two-factor authentication") %></h2>
<%= form_for @conn, Routes.mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %>
<div class="input">
- <%= label f, :code, "Authentication code" %>
- <%= text_input f, :code, [autocomplete: false, autocorrect: "off", autocapitalize: "off", autofocus: true, pattern: "[0-9]*", spellcheck: false] %>
+ <%= label f, :code, Gettext.dpgettext("static_pages", "mfa auth code prompt", "Authentication code") %>
+ <%= text_input f, :code, [autocomplete: "one-time-code", autocorrect: "off", autocapitalize: "off", autofocus: true, pattern: "[0-9]*", spellcheck: false] %>
<%= hidden_input f, :mfa_token, value: @mfa_token %>
<%= hidden_input f, :state, value: @state %>
<%= hidden_input f, :redirect_uri, value: @redirect_uri %>
<%= hidden_input f, :challenge_type, value: "totp" %>
</div>
-<%= submit "Verify" %>
+<%= submit Gettext.dpgettext("static_pages", "mfa auth verify code button", "Verify") %>
<% end %>
<a href="<%= Routes.mfa_path(@conn, :show, %{challenge_type: "recovery", mfa_token: @mfa_token, state: @state, redirect_uri: @redirect_uri}) %>">
- Enter a two-factor recovery code
+ <%= Gettext.dpgettext("static_pages", "mfa auth page use recovery code link", "Enter a two-factor recovery code") %>
</a>
<div class="scopes-input">
- <%= label @form, :scope, "The following permissions will be granted" %>
+ <%= label @form, :scope, Gettext.dpgettext("static_pages", "oauth scopes message", "The following permissions will be granted") %>
<div class="scopes">
<%= for scope <- @available_scopes do %>
<%# Note: using hidden input with `unchecked_value` in order to distinguish user's empty selection from `scope` param being omitted %>
-<h2>Sign in with external provider</h2>
+<h2><%= Gettext.dpgettext("static_pages", "oauth external provider page title", "Sign in with external provider") %></h2>
<%= form_for @conn, Routes.o_auth_path(@conn, :prepare_request), [as: "authorization", method: "get"], fn f -> %>
<div style="display: none">
<%= 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 %>
-<h1>Successfully authorized</h1>
-<h2>Token code is <br><%= @auth.token %></h2>
+<h1><%= Gettext.dpgettext("static_pages", "oauth authorized page title", "Successfully authorized") %></h1>
+<h2><%= raw Gettext.dpgettext("static_pages", "oauth token code message", "Token code is <br>%{token}", token: safe_to_string(html_escape(@auth.token))) %></h2>
-<h1>Authorization exists</h1>
-<h2>Access token is <br><%= @token.token %></h2>
+<h1><%= Gettext.dpgettext("static_pages", "oauth authorization exists page title", "Authorization exists") %></h1>
+<h2><%= raw Gettext.dpgettext("static_pages", "oauth token code message", "Token code is <br>%{token}", token: safe_to_string(html_escape(@token.token))) %></h2>
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
<% end %>
-<h2>Registration Details</h2>
+<h2><%= Gettext.dpgettext("static_pages", "oauth register page title", "Registration Details") %></h2>
-<p>If you'd like to register a new account, please provide the details below.</p>
+<p><%= Gettext.dpgettext("static_pages", "oauth register page fill form prompt", "If you'd like to register a new account, please provide the details below.") %></p>
<%= form_for @conn, Routes.o_auth_path(@conn, :register), [as: "authorization"], fn f -> %>
<div class="input">
- <%= label f, :nickname, "Nickname" %>
- <%= text_input f, :nickname, value: @nickname %>
+ <%= label f, :nickname, Gettext.dpgettext("static_pages", "oauth register page nickname prompt", "Nickname") %>
+ <%= text_input f, :nickname, value: @nickname, autocomplete: "username" %>
</div>
<div class="input">
- <%= label f, :email, "Email" %>
- <%= text_input f, :email, value: @email %>
+ <%= label f, :email, Gettext.dpgettext("static_pages", "oauth register page email prompt", "Email") %>
+ <%= text_input f, :email, value: @email, autocomplete: "email" %>
</div>
-<%= submit "Proceed as new user", name: "op", value: "register" %>
+<%= submit Gettext.dpgettext("static_pages", "oauth register page register button", "Proceed as new user"), name: "op", value: "register" %>
-<p>Alternatively, sign in to connect to existing account.</p>
+<p><%= Gettext.dpgettext("static_pages", "oauth register page login prompt", "Alternatively, sign in to connect to existing account.") %></p>
<div class="input">
- <%= label f, :name, "Name or email" %>
- <%= text_input f, :name %>
+ <%= label f, :name, Gettext.dpgettext("static_pages", "oauth register page login username prompt", "Name or email") %>
+ <%= text_input f, :name, autocomplete: "username" %>
</div>
<div class="input">
- <%= label f, :password, "Password" %>
- <%= password_input f, :password %>
+ <%= label f, :password, Gettext.dpgettext("static_pages", "oauth register page login password prompt", "Password") %>
+ <%= password_input f, :password, autocomplete: "password" %>
</div>
-<%= submit "Proceed as existing user", name: "op", value: "connect" %>
+<%= submit Gettext.dpgettext("static_pages", "oauth register page login button", "Proceed as existing user"), name: "op", value: "connect" %>
<%= hidden_input f, :client_id, value: @client_id %>
<%= hidden_input f, :redirect_uri, value: @redirect_uri %>
<div class="container__content">
<%= if @app do %>
- <p>Application <strong><%= @app.client_name %></strong> is requesting access to your account.</p>
+ <p><%= raw Gettext.dpgettext("static_pages", "oauth authorize message", "Application <strong>%{client_name}</strong> is requesting access to your account.", client_name: safe_to_string(html_escape(@app.client_name))) %></p>
<%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f}) %>
<% end %>
<%= if @user do %>
<div class="actions">
- <a class="button button--cancel" href="/">Cancel</a>
- <%= submit "Approve", class: "button--approve" %>
+ <a class="button button--cancel" href="/">
+ <%= Gettext.dpgettext("static_pages", "oauth authorize cancel button", "Cancel") %>
+ </a>
+ <%= submit Gettext.dpgettext("static_pages", "oauth authorize approve button", "Approve"), class: "button--approve" %>
</div>
<% else %>
<%= if @params["registration"] in ["true", true] do %>
- <h3>This is the first time you visit! Please enter your Pleroma handle.</h3>
- <p>Choose carefully! You won't be able to change this later. You will be able to change your display name, though.</p>
+ <h3><%= Gettext.dpgettext("static_pages", "oauth register page title", "This is the first time you visit! Please enter your Pleroma handle.") %></h3>
+ <p><%= Gettext.dpgettext("static_pages", "oauth register nickname unchangeable warning", "Choose carefully! You won't be able to change this later. You will be able to change your display name, though.") %></p>
<div class="input">
- <%= label f, :nickname, "Pleroma Handle" %>
- <%= text_input f, :nickname, placeholder: "lain" %>
+ <%= label f, :nickname, Gettext.dpgettext("static_pages", "oauth register nickname prompt", "Pleroma Handle") %>
+ <%= text_input f, :nickname, placeholder: "lain", autocomplete: "username" %>
</div>
<%= hidden_input f, :name, value: @params["name"] %>
<%= hidden_input f, :password, value: @params["password"] %>
<br>
<% else %>
<div class="input">
- <%= label f, :name, "Username" %>
+ <%= label f, :name, Gettext.dpgettext("static_pages", "oauth login username prompt", "Username") %>
<%= text_input f, :name %>
</div>
<div class="input">
- <%= label f, :password, "Password" %>
+ <%= label f, :password, Gettext.dpgettext("static_pages", "oauth login password prompt", "Password") %>
<%= password_input f, :password %>
</div>
- <%= submit "Log In" %>
+ <%= submit Gettext.dpgettext("static_pages", "oauth login button", "Log In") %>
<% end %>
<% end %>
</div>
<form class="pull-right collapse" method="POST" action="<%= Helpers.util_path(@conn, :remote_subscribe) %>">
<input type="hidden" name="nickname" value="<%= @user.nickname %>">
<input type="hidden" name="profile" value="">
- <button type="submit" class="collapse">Remote follow</button>
+ <button type="submit" class="collapse"><%= Gettext.dpgettext("static_pages", "static fe profile page remote follow button", "Remote follow") %></button>
</form>
<%= raw Formatter.emojify(@user.name, @user.emoji) %> |
<%= link "@#{@user.nickname}@#{Endpoint.host()}", to: (@user.uri || @user.ap_id) %>
-<h2>Invalid Token</h2>
+<h2><%= Gettext.dpgettext("static_pages", "password reset invalid token message", "Invalid Token") %></h2>
<h2>Password Reset for <%= @user.nickname %></h2>
<%= form_for @conn, Routes.reset_password_path(@conn, :do_reset), [as: "data"], fn f -> %>
<div class="form-row">
- <%= label f, :password, "Password" %>
+ <%= label f, :password, Gettext.dpgettext("static_pages", "password reset form password prompt", "Password") %>
<%= password_input f, :password %>
</div>
<div class="form-row">
- <%= label f, :password_confirmation, "Confirmation" %>
+ <%= label f, :password_confirmation, Gettext.dpgettext("static_pages", "password reset form confirm password prompt", "Confirmation") %>
<%= password_input f, :password_confirmation %>
</div>
<%= hidden_input f, :token, value: @token.token %>
- <%= submit "Reset" %>
+ <%= submit Gettext.dpgettext("static_pages", "password reset button", "Reset") %>
<% end %>
-<h2>Password reset failed</h2>
-<h3><a href="<%= Pleroma.Web.Endpoint.url() %>">Homepage</a></h3>
+<h2><%= Gettext.dpgettext("static_pages", "password reset failed message", "Password reset failed") %></h2>
+<h3>
+ <a href="<%= Pleroma.Web.Endpoint.url() %>">
+ <%= Gettext.dpgettext("static_pages", "password reset failed homepage link", "Homepage") %>
+ </a>
+</h3>
-<h2>Password changed!</h2>
-<h3><a href="<%= Pleroma.Web.Endpoint.url() %>">Homepage</a></h3>
+<h2><%= Gettext.dpgettext("static_pages", "password reset successful message", "Password changed!") %></h2>
+<h3><a href="<%= Pleroma.Web.Endpoint.url() %>"><%= Gettext.dpgettext("static_pages", "password reset successful homepage link", "Homepage") %></a></h3>
<%= if @error == :error do %>
- <h2>Error fetching user</h2>
+ <h2><%= Gettext.dpgettext("static_pages", "remote follow error", "Error fetching user") %></h2>
<% else %>
- <h2>Remote follow</h2>
+ <h2><%= Gettext.dpgettext("static_pages", "remote follow header", "Remote follow") %></h2>
<img height="128" width="128" src="<%= avatar_url(@followee) %>">
<p><%= @followee.nickname %></p>
<%= form_for @conn, Routes.remote_follow_path(@conn, :do_follow), [as: "user"], fn f -> %>
<%= hidden_input f, :id, value: @followee.id %>
- <%= submit "Authorize" %>
+ <%= submit Gettext.dpgettext("static_pages", "remote follow authorization button", "Authorize") %>
<% end %>
<% end %>
<%= if @error do %>
<h2><%= @error %></h2>
<% end %>
-<h2>Log in to follow</h2>
+<h2><%= Gettext.dpgettext("static_pages", "remote follow header, need login", "Log in to follow") %></h2>
<p><%= @followee.nickname %></p>
<img height="128" width="128" src="<%= avatar_url(@followee) %>">
<%= form_for @conn, Routes.remote_follow_path(@conn, :do_follow), [as: "authorization"], fn f -> %>
-<%= text_input f, :name, placeholder: "Username", required: true %>
+<%= text_input f, :name, placeholder: Gettext.dpgettext("static_pages", "placeholder text for username entry", "Username"), required: true, autocomplete: "username" %>
<br>
-<%= password_input f, :password, placeholder: "Password", required: true %>
+<%= password_input f, :password, placeholder: Gettext.dpgettext("static_pages", "placeholder text for password entry", "Password"), required: true, autocomplete: "password" %>
<br>
<%= hidden_input f, :id, value: @followee.id %>
-<%= submit "Authorize" %>
+<%= submit Gettext.dpgettext("static_pages", "remote follow authorization button for login", "Authorize") %>
<% end %>
<%= if @error do %>
<h2><%= @error %></h2>
<% end %>
-<h2>Two-factor authentication</h2>
+<h2><%= Gettext.dpgettext("static_pages", "remote follow mfa header", "Two-factor authentication") %></h2>
<p><%= @followee.nickname %></p>
<img height="128" width="128" src="<%= avatar_url(@followee) %>">
<%= form_for @conn, Routes.remote_follow_path(@conn, :do_follow), [as: "mfa"], fn f -> %>
-<%= text_input f, :code, placeholder: "Authentication code", required: true %>
+<%= text_input f, :code, placeholder: Gettext.dpgettext("static_pages", "placeholder text for auth code entry", "Authentication code"), required: true %>
<br>
<%= hidden_input f, :id, value: @followee.id %>
<%= hidden_input f, :token, value: @mfa_token %>
-<%= submit "Authorize" %>
+<%= submit Gettext.dpgettext("static_pages", "remote follow authorization button for mfa", "Authorize") %>
<% end %>
<%= if @error do %>
-<p>Error following account</p>
+<p><%= Gettext.dpgettext("static_pages", "remote follow error", "Error following account") %></p>
<% else %>
-<h2>Account followed!</h2>
+<h2><%= Gettext.dpgettext("static_pages", "remote follow success", "Account followed!") %></h2>
<% end %>
-
<%= if @error do %>
- <h2>Error: <%= @error %></h2>
+ <h2><%= Gettext.dpgettext("static_pages", "remote follow error", "Error: %{error}", error: @error) %></h2>
<% else %>
- <h2>Remotely follow <%= @nickname %></h2>
+ <h2><%= Gettext.dpgettext("static_pages", "remote follow header", "Remotely follow %{nickname}", nickname: @nickname) %></h2>
<%= form_for @conn, Routes.util_path(@conn, :remote_subscribe), [as: "user"], fn f -> %>
<%= hidden_input f, :nickname, value: @nickname %>
- <%= text_input f, :profile, placeholder: "Your account ID, e.g. lain@quitter.se" %>
- <%= submit "Follow" %>
+ <%= text_input f, :profile, placeholder: Gettext.dpgettext("static_pages", "placeholder text for account id", "Your account ID, e.g. lain@quitter.se") %>
+ <%= submit Gettext.dpgettext("static_pages", "remote follow authorization button for following with a remote account", "Follow") %>
<% end %>
<% end %>
alias Pleroma.UserInviteToken
def register_user(params, opts \\ []) do
+ fallback_language = Gettext.get_locale()
+
params =
params
|> Map.take([:email, :token, :password])
|> 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)
defmodule Pleroma.Web.TwitterAPI.PasswordView do
use Pleroma.Web, :view
import Phoenix.HTML.Form
+ alias Pleroma.Web.Gettext
end
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
import Phoenix.HTML.Form
alias Pleroma.Config
alias Pleroma.Web.Endpoint
+ alias Pleroma.Web.Gettext
def status_net_config(instance) 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)
defmodule Pleroma.Web.Mailer.SubscriptionView do
use Pleroma.Web, :view
+ alias Pleroma.Web.Gettext
end
--- /dev/null
+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
{: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"},
{: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},
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)
# 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,
"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"},
{
- "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"
+ }
}
}
}
--- /dev/null
+## 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 ""
--- /dev/null
+## "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 ""
--- /dev/null
+## "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 ""
--- /dev/null
+## "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 ""
--- /dev/null
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR Free Software Foundation, Inc.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"PO-Revision-Date: 2022-03-06 11:27-0500\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#~ ## "msgid"s in this file come from POT (.pot) files.
+#~ ##
+#~ ## Do not add, change, or remove "msgid"s manually here as
+#~ ## they're tied to the ones in the corresponding POT file
+#~ ## (with the same domain).
+#~ ##
+#~ ## Use "mix gettext.extract --merge" or "mix gettext.merge"
+#~ ## to merge POT files into PO files.
+#~ msgid ""
+#~ msgstr ""
+#~ "Language: en_test\n"
+#~ "Plural-Forms: nplurals=2\n"
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:9
+msgctxt "remote follow authorization button"
+msgid "Authorize"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:2
+msgctxt "remote follow error"
+msgid "Error fetching user"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:4
+msgctxt "remote follow header"
+msgid "Remote follow"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:8
+msgctxt "placeholder text for auth code entry"
+msgid "Authentication code"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:10
+msgctxt "placeholder text for password entry"
+msgid "Password"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:8
+msgctxt "placeholder text for username entry"
+msgid "Username"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:13
+msgctxt "remote follow authorization button for login"
+msgid "Authorize"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:12
+msgctxt "remote follow authorization button for mfa"
+msgid "Authorize"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:2
+msgctxt "remote follow error"
+msgid "Error following account"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:4
+msgctxt "remote follow header, need login"
+msgid "Log in to follow"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:4
+msgctxt "remote follow mfa header"
+msgid "Two-factor authentication"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:4
+msgctxt "remote follow success"
+msgid "Account followed!"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:7
+msgctxt "placeholder text for account id"
+msgid "Your account ID, e.g. lain@quitter.se"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:8
+msgctxt "remote follow authorization button for following with a remote account"
+msgid "Follow"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:2
+msgctxt "remote follow error"
+msgid "Error: %{error}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:4
+msgctxt "remote follow header"
+msgid "Remotely follow %{nickname}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:12
+msgctxt "password reset button"
+msgid "Reset"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:4
+msgctxt "password reset failed homepage link"
+msgid "Homepage"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:1
+msgctxt "password reset failed message"
+msgid "Password reset failed"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:8
+msgctxt "password reset form confirm password prompt"
+msgid "Confirmation"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:4
+msgctxt "password reset form password prompt"
+msgid "Password"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex:1
+msgctxt "password reset invalid token message"
+msgid "Invalid Token"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:2
+msgctxt "password reset successful homepage link"
+msgid "Homepage"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:1
+msgctxt "password reset successful message"
+msgid "Password changed!"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/feed/feed/tag.atom.eex:15
+#: lib/pleroma/web/templates/feed/feed/tag.rss.eex:7
+msgctxt "tag feed description"
+msgid "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:1
+msgctxt "oauth authorization exists page title"
+msgid "Authorization exists"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:32
+msgctxt "oauth authorize approve button"
+msgid "Approve"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:30
+msgctxt "oauth authorize cancel button"
+msgid "Cancel"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:23
+msgctxt "oauth authorize message"
+msgid "Application <strong>%{client_name}</strong> is requesting access to your account."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:1
+msgctxt "oauth authorized page title"
+msgid "Successfully authorized"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:1
+msgctxt "oauth external provider page title"
+msgid "Sign in with external provider"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:13
+msgctxt "oauth external provider sign in button"
+msgid "Sign in with %{strategy}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:54
+msgctxt "oauth login button"
+msgid "Log In"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:51
+msgctxt "oauth login password prompt"
+msgid "Password"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:47
+msgctxt "oauth login username prompt"
+msgid "Username"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:39
+msgctxt "oauth register nickname prompt"
+msgid "Pleroma Handle"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:37
+msgctxt "oauth register nickname unchangeable warning"
+msgid "Choose carefully! You won't be able to change this later. You will be able to change your display name, though."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:18
+msgctxt "oauth register page email prompt"
+msgid "Email"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:10
+msgctxt "oauth register page fill form prompt"
+msgid "If you'd like to register a new account, please provide the details below."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:35
+msgctxt "oauth register page login button"
+msgid "Proceed as existing user"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:31
+msgctxt "oauth register page login password prompt"
+msgid "Password"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:24
+msgctxt "oauth register page login prompt"
+msgid "Alternatively, sign in to connect to existing account."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:27
+msgctxt "oauth register page login username prompt"
+msgid "Name or email"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:14
+msgctxt "oauth register page nickname prompt"
+msgid "Nickname"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:22
+msgctxt "oauth register page register button"
+msgid "Proceed as new user"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:8
+msgctxt "oauth register page title"
+msgid "Registration Details"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:36
+msgctxt "oauth register page title"
+msgid "This is the first time you visit! Please enter your Pleroma handle."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex:2
+msgctxt "oauth scopes message"
+msgid "The following permissions will be granted"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:2
+#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:2
+msgctxt "oauth token code message"
+msgid "Token code is <br>%{token}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:12
+msgctxt "mfa auth code prompt"
+msgid "Authentication code"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:8
+msgctxt "mfa auth page title"
+msgid "Two-factor authentication"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:23
+msgctxt "mfa auth page use recovery code link"
+msgid "Enter a two-factor recovery code"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:20
+msgctxt "mfa auth verify code button"
+msgid "Verify"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:8
+msgctxt "mfa recover page title"
+msgid "Two-factor recovery"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:12
+msgctxt "mfa recover recovery code prompt"
+msgid "Recovery code"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:23
+msgctxt "mfa recover use 2fa code link"
+msgid "Enter a two-factor code"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:20
+msgctxt "mfa recover verify recovery code button"
+msgid "Verify"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex:8
+msgctxt "static fe profile page remote follow button"
+msgid "Remote follow"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/email/digest.html.eex:163
+msgctxt "digest email header line"
+msgid "Hey %{nickname}, here is what you've missed!"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/email/digest.html.eex:544
+msgctxt "digest email receiver address"
+msgid "The email address you are subscribed as is <a href='mailto:%{@user.email}' style='color: %{color};text-decoration: none;'>%{email}</a>. "
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/email/digest.html.eex:538
+msgctxt "digest email sending reason"
+msgid "You have received this email because you have signed up to receive digest emails from <b>%{instance}</b> Pleroma instance."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/email/digest.html.eex:547
+msgctxt "digest email unsubscribe action"
+msgid "To unsubscribe, please go %{here}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/email/digest.html.eex:547
+msgctxt "digest email unsubscribe action link text"
+msgid "here"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_failure.html.eex:1
+msgctxt "mailer unsubscribe failed message"
+msgid "UNSUBSCRIBE FAILURE"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_success.html.eex:1
+msgctxt "mailer unsubscribe successful message"
+msgid "UNSUBSCRIBE SUCCESSFUL"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/email/digest.html.eex:385
+msgctxt "new followers count header"
+msgid "%{count} New Follower"
+msgid_plural "%{count} New Followers"
+msgstr[0] "xx%{count} New Followerxx"
+msgstr[1] "xx%{count} New Followersxx"
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:356
+msgctxt "account archive email body - self-requested"
+msgid "<p>You requested a full backup of your Pleroma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:384
+msgctxt "account archive email subject"
+msgid "Your account archive is ready"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:188
+msgctxt "approval pending email body"
+msgid "<h3>Awaiting Approval</h3>\n<p>Your account at %{instance_name} is being reviewed by staff. You will receive another email once your account is approved.</p>\n"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:202
+msgctxt "approval pending email subject"
+msgid "Your account is awaiting approval"
+msgstr "xxYour account is awaiting approvalxx"
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:158
+msgctxt "confirmation email body"
+msgid "<h3>Thank you for registering on %{instance_name}</h3>\n<p>Email confirmation is required to activate the account.</p>\n<p>Please click the following link to <a href=\"%{confirmation_url}\">activate your account</a>.</p>\n"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:174
+msgctxt "confirmation email subject"
+msgid "%{instance_name} account confirmation"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:310
+msgctxt "digest email subject"
+msgid "Your digest from %{instance_name}"
+msgstr "xxYour digest from %{instance_name}xx"
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:81
+msgctxt "password reset email body"
+msgid "<h3>Reset your password at %{instance_name}</h3>\n<p>Someone has requested password change for your account at %{instance_name}.</p>\n<p>If it was you, visit the following link to proceed: <a href=\"%{password_reset_url}\">reset password</a>.</p>\n<p>If it was someone else, nothing to worry about: your data is secure and your password has not been changed.</p>\n"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:98
+msgctxt "password reset email subject"
+msgid "Password reset"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:215
+msgctxt "successful registration email body"
+msgid "<h3>Hello @%{nickname},</h3>\n<p>Your account at %{instance_name} has been registered successfully.</p>\n<p>No further action is required to activate your account.</p>\n"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:231
+msgctxt "successful registration email subject"
+msgid "Account registered on %{instance_name}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:119
+msgctxt "user invitation email body"
+msgid "<h3>You are invited to %{instance_name}</h3>\n<p>%{inviter_name} invites you to join %{instance_name}, an instance of Pleroma federated social networking platform.</p>\n<p>Click the following link to register: <a href=\"%{registration_url}\">accept invitation</a>.</p>\n"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:136
+msgctxt "user invitation email subject"
+msgid "Invitation to %{instance_name}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:53
+msgctxt "welcome email html body"
+msgid "Welcome to %{instance_name}!"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:41
+msgctxt "welcome email subject"
+msgid "Welcome to %{instance_name}!"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:65
+msgctxt "welcome email text body"
+msgid "Welcome to %{instance_name}!"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:368
+msgctxt "account archive email body - admin requested"
+msgid "<p>Admin @%{admin_nickname} requested a full backup of your Pleroma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
+msgstr ""
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 ""
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 ""
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 ""
msgstr ""
#, elixir-format
-#: lib/pleroma/web/twitter_api/twitter_api.ex:103
+#: lib/pleroma/web/twitter_api/twitter_api.ex:96
msgid "CAPTCHA Error"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:290
+#: lib/pleroma/web/common_api.ex:266
msgid "Could not add reaction emoji"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/common_api/common_api.ex:301
+#: lib/pleroma/web/common_api.ex:277
msgid "Could not remove reaction emoji"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/twitter_api/twitter_api.ex:129
+#: lib/pleroma/web/twitter_api/twitter_api.ex:122
msgid "Invalid CAPTCHA (Missing parameter: %{name})"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:92
+#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:96
msgid "List not found"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:123
+#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:151
msgid "Missing parameter: %{name}"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/oauth/oauth_controller.ex:210
-#: lib/pleroma/web/oauth/oauth_controller.ex:321
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:232
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:346
msgid "Password reset is required"
msgstr ""
#, elixir-format
#: lib/pleroma/tests/auth_test_controller.ex:9
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:6 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:6
-#: lib/pleroma/web/admin_api/controllers/config_controller.ex:6 lib/pleroma/web/admin_api/controllers/fallback_controller.ex:6
+#: lib/pleroma/web/admin_api/controllers/chat_controller.ex:6 lib/pleroma/web/admin_api/controllers/config_controller.ex:6
+#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:6 lib/pleroma/web/admin_api/controllers/frontend_controller.ex:6
+#: lib/pleroma/web/admin_api/controllers/instance_controller.ex:6 lib/pleroma/web/admin_api/controllers/instance_document_controller.ex:6
#: lib/pleroma/web/admin_api/controllers/invite_controller.ex:6 lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex:6
-#: lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex:6 lib/pleroma/web/admin_api/controllers/relay_controller.ex:6
+#: lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex:6 lib/pleroma/web/admin_api/controllers/relay_controller.ex:6
#: lib/pleroma/web/admin_api/controllers/report_controller.ex:6 lib/pleroma/web/admin_api/controllers/status_controller.ex:6
-#: lib/pleroma/web/controller_helper.ex:6 lib/pleroma/web/embed_controller.ex:6
-#: lib/pleroma/web/fallback_redirect_controller.ex:6 lib/pleroma/web/feed/tag_controller.ex:6
-#: lib/pleroma/web/feed/user_controller.ex:6 lib/pleroma/web/mailer/subscription_controller.ex:2
-#: lib/pleroma/web/masto_fe_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/account_controller.ex:6
-#: lib/pleroma/web/mastodon_api/controllers/app_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/auth_controller.ex:6
+#: lib/pleroma/web/admin_api/controllers/user_controller.ex:6 lib/pleroma/web/controller_helper.ex:6 lib/pleroma/web/embed_controller.ex:6
+#: lib/pleroma/web/fallback/redirect_controller.ex:6 lib/pleroma/web/feed/tag_controller.ex:6
+#: lib/pleroma/web/feed/user_controller.ex:6 lib/pleroma/web/mailer/subscription_controller.ex:6
+#: lib/pleroma/web/manifest_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/account_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/app_controller.ex:11 lib/pleroma/web/mastodon_api/controllers/auth_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex:6
-#: lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:6
-#: lib/pleroma/web/mastodon_api/controllers/filter_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex:6
-#: lib/pleroma/web/mastodon_api/controllers/instance_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/list_controller.ex:6
-#: lib/pleroma/web/mastodon_api/controllers/marker_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex:14
-#: lib/pleroma/web/mastodon_api/controllers/media_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/notification_controller.ex:6
-#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/report_controller.ex:8
-#: lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/search_controller.ex:6
-#: lib/pleroma/web/mastodon_api/controllers/status_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:7
-#: lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:6
-#: lib/pleroma/web/media_proxy/media_proxy_controller.ex:6 lib/pleroma/web/mongooseim/mongoose_im_controller.ex:6
-#: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:6 lib/pleroma/web/oauth/fallback_controller.ex:6
-#: lib/pleroma/web/oauth/mfa_controller.ex:10 lib/pleroma/web/oauth/oauth_controller.ex:6
-#: lib/pleroma/web/ostatus/ostatus_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/account_controller.ex:6
-#: lib/pleroma/web/pleroma_api/controllers/chat_controller.ex:5 lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex:6
-#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:2 lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex:6
-#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/notification_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/directory_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/filter_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/instance_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/marker_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex:14 lib/pleroma/web/mastodon_api/controllers/media_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/notification_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/report_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/search_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/status_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:7 lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex:6
+#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:6 lib/pleroma/web/media_proxy/media_proxy_controller.ex:6
+#: lib/pleroma/web/mongoose_im/mongoose_im_controller.ex:6 lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:6
+#: lib/pleroma/web/o_auth/fallback_controller.ex:6 lib/pleroma/web/o_auth/mfa_controller.ex:10
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:6 lib/pleroma/web/o_status/o_status_controller.ex:6
+#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/app_controller.ex:6
+#: lib/pleroma/web/pleroma_api/controllers/backup_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/chat_controller.ex:5
+#: lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex:6
+#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex:6
+#: lib/pleroma/web/pleroma_api/controllers/instances_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:6
+#: lib/pleroma/web/pleroma_api/controllers/notification_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/report_controller.ex:6
#: lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex:6
-#: lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex:7 lib/pleroma/web/static_fe/static_fe_controller.ex:6
+#: lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex:7 lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex:6
+#: lib/pleroma/web/static_fe/static_fe_controller.ex:6 lib/pleroma/web/twitter_api/controller.ex:6
#: lib/pleroma/web/twitter_api/controllers/password_controller.ex:10 lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex:6
-#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:6 lib/pleroma/web/twitter_api/twitter_api_controller.ex:6
-#: lib/pleroma/web/uploader_controller.ex:6 lib/pleroma/web/web_finger/web_finger_controller.ex:6
+#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:6 lib/pleroma/web/uploader_controller.ex:6
+#: lib/pleroma/web/web_finger/web_finger_controller.ex:6
msgid "Security violation: OAuth scopes check was neither handled nor explicitly skipped."
msgstr ""
#, elixir-format
-#: lib/pleroma/plugs/ensure_authenticated_plug.ex:28
+#: lib/pleroma/web/plugs/ensure_authenticated_plug.ex:32
msgid "Two-factor authentication enabled, you must use a access token."
msgstr ""
#, elixir-format
-#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:210
-msgid "Unexpected error occurred while adding file to pack."
+#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61
+msgid "Web push subscription is disabled on this Pleroma instance"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:234
+msgid "You can't revoke your own admin/moderator status."
msgstr ""
#, elixir-format
-#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:138
-msgid "Unexpected error occurred while creating pack."
+#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:129
+msgid "authorization required for timeline view"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:278
-msgid "Unexpected error occurred while removing file from pack."
+#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:24
+msgid "Access denied"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:250
-msgid "Unexpected error occurred while updating file in pack."
+#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:321
+msgid "This API requires an authenticated user"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:179
-msgid "Unexpected error occurred while updating pack metadata."
+#: lib/pleroma/web/plugs/ensure_staff_privileged_plug.ex:26
+#: lib/pleroma/web/plugs/user_is_admin_plug.ex:21
+msgid "User is not an admin."
msgstr ""
#, elixir-format
-#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61
-msgid "Web push subscription is disabled on this Pleroma instance"
+#: lib/pleroma/user/backup.ex:75
+msgid "Last export was less than a day ago"
+msgid_plural "Last export was less than %{days} days ago"
+msgstr[0] ""
+msgstr[1] ""
+
+#, elixir-format
+#: lib/pleroma/user/backup.ex:93
+msgid "Backups require enabled email"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:451
-msgid "You can't revoke your own admin/moderator status."
+#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:423
+msgid "Character limit (%{limit} characters) exceeded, contains %{length} characters"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:126
-msgid "authorization required for timeline view"
+#: lib/pleroma/user/backup.ex:98
+msgid "Email is required"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:24
-msgid "Access denied"
+#: lib/pleroma/web/common_api/utils.ex:507
+msgid "Too many attachments"
msgstr ""
#, elixir-format
-#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:282
-msgid "This API requires an authenticated user"
+#: lib/pleroma/web/plugs/ensure_staff_privileged_plug.ex:33
+#: lib/pleroma/web/plugs/user_is_staff_plug.ex:20
+msgid "User is not a staff member."
msgstr ""
#, elixir-format
-#: lib/pleroma/plugs/user_is_admin_plug.ex:21
-msgid "User is not an admin."
+#: lib/pleroma/web/o_auth/o_auth_controller.ex:366
+msgid "Your account is awaiting approval."
msgstr ""
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 ""
--- /dev/null
+## This file is a PO Template file.
+##
+## "msgid"s here are often extracted from source code.
+## Add new translations manually only if they're dynamic
+## translations that can't be statically extracted.
+##
+## Run "mix gettext.extract" to bring this file up to
+## date. Leave "msgstr"s empty as changing them here as no
+## effect: edit them in PO (.po) files instead.
+msgid ""
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:9
+msgctxt "remote follow authorization button"
+msgid "Authorize"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:2
+msgctxt "remote follow error"
+msgid "Error fetching user"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:4
+msgctxt "remote follow header"
+msgid "Remote follow"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:8
+msgctxt "placeholder text for auth code entry"
+msgid "Authentication code"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:10
+msgctxt "placeholder text for password entry"
+msgid "Password"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:8
+msgctxt "placeholder text for username entry"
+msgid "Username"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:13
+msgctxt "remote follow authorization button for login"
+msgid "Authorize"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:12
+msgctxt "remote follow authorization button for mfa"
+msgid "Authorize"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:2
+msgctxt "remote follow error"
+msgid "Error following account"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:4
+msgctxt "remote follow header, need login"
+msgid "Log in to follow"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:4
+msgctxt "remote follow mfa header"
+msgid "Two-factor authentication"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:4
+msgctxt "remote follow success"
+msgid "Account followed!"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:7
+msgctxt "placeholder text for account id"
+msgid "Your account ID, e.g. lain@quitter.se"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:8
+msgctxt "remote follow authorization button for following with a remote account"
+msgid "Follow"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:2
+msgctxt "remote follow error"
+msgid "Error: %{error}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:4
+msgctxt "remote follow header"
+msgid "Remotely follow %{nickname}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:12
+msgctxt "password reset button"
+msgid "Reset"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:4
+msgctxt "password reset failed homepage link"
+msgid "Homepage"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:1
+msgctxt "password reset failed message"
+msgid "Password reset failed"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:8
+msgctxt "password reset form confirm password prompt"
+msgid "Confirmation"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:4
+msgctxt "password reset form password prompt"
+msgid "Password"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex:1
+msgctxt "password reset invalid token message"
+msgid "Invalid Token"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:2
+msgctxt "password reset successful homepage link"
+msgid "Homepage"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:1
+msgctxt "password reset successful message"
+msgid "Password changed!"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/feed/feed/tag.atom.eex:15
+#: lib/pleroma/web/templates/feed/feed/tag.rss.eex:7
+msgctxt "tag feed description"
+msgid "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:1
+msgctxt "oauth authorization exists page title"
+msgid "Authorization exists"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:32
+msgctxt "oauth authorize approve button"
+msgid "Approve"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:30
+msgctxt "oauth authorize cancel button"
+msgid "Cancel"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:23
+msgctxt "oauth authorize message"
+msgid "Application <strong>%{client_name}</strong> is requesting access to your account."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:1
+msgctxt "oauth authorized page title"
+msgid "Successfully authorized"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:1
+msgctxt "oauth external provider page title"
+msgid "Sign in with external provider"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:13
+msgctxt "oauth external provider sign in button"
+msgid "Sign in with %{strategy}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:54
+msgctxt "oauth login button"
+msgid "Log In"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:51
+msgctxt "oauth login password prompt"
+msgid "Password"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:47
+msgctxt "oauth login username prompt"
+msgid "Username"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:39
+msgctxt "oauth register nickname prompt"
+msgid "Pleroma Handle"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:37
+msgctxt "oauth register nickname unchangeable warning"
+msgid "Choose carefully! You won't be able to change this later. You will be able to change your display name, though."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:18
+msgctxt "oauth register page email prompt"
+msgid "Email"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:10
+msgctxt "oauth register page fill form prompt"
+msgid "If you'd like to register a new account, please provide the details below."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:35
+msgctxt "oauth register page login button"
+msgid "Proceed as existing user"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:31
+msgctxt "oauth register page login password prompt"
+msgid "Password"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:24
+msgctxt "oauth register page login prompt"
+msgid "Alternatively, sign in to connect to existing account."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:27
+msgctxt "oauth register page login username prompt"
+msgid "Name or email"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:14
+msgctxt "oauth register page nickname prompt"
+msgid "Nickname"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:22
+msgctxt "oauth register page register button"
+msgid "Proceed as new user"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:8
+msgctxt "oauth register page title"
+msgid "Registration Details"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:36
+msgctxt "oauth register page title"
+msgid "This is the first time you visit! Please enter your Pleroma handle."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex:2
+msgctxt "oauth scopes message"
+msgid "The following permissions will be granted"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:2
+#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:2
+msgctxt "oauth token code message"
+msgid "Token code is <br>%{token}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:12
+msgctxt "mfa auth code prompt"
+msgid "Authentication code"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:8
+msgctxt "mfa auth page title"
+msgid "Two-factor authentication"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:23
+msgctxt "mfa auth page use recovery code link"
+msgid "Enter a two-factor recovery code"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:20
+msgctxt "mfa auth verify code button"
+msgid "Verify"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:8
+msgctxt "mfa recover page title"
+msgid "Two-factor recovery"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:12
+msgctxt "mfa recover recovery code prompt"
+msgid "Recovery code"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:23
+msgctxt "mfa recover use 2fa code link"
+msgid "Enter a two-factor code"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:20
+msgctxt "mfa recover verify recovery code button"
+msgid "Verify"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex:8
+msgctxt "static fe profile page remote follow button"
+msgid "Remote follow"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/email/digest.html.eex:163
+msgctxt "digest email header line"
+msgid "Hey %{nickname}, here is what you've missed!"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/email/digest.html.eex:544
+msgctxt "digest email receiver address"
+msgid "The email address you are subscribed as is <a href='mailto:%{@user.email}' style='color: %{color};text-decoration: none;'>%{email}</a>. "
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/email/digest.html.eex:538
+msgctxt "digest email sending reason"
+msgid "You have received this email because you have signed up to receive digest emails from <b>%{instance}</b> Pleroma instance."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/email/digest.html.eex:547
+msgctxt "digest email unsubscribe action"
+msgid "To unsubscribe, please go %{here}."
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/email/digest.html.eex:547
+msgctxt "digest email unsubscribe action link text"
+msgid "here"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_failure.html.eex:1
+msgctxt "mailer unsubscribe failed message"
+msgid "UNSUBSCRIBE FAILURE"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_success.html.eex:1
+msgctxt "mailer unsubscribe successful message"
+msgid "UNSUBSCRIBE SUCCESSFUL"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/web/templates/email/digest.html.eex:385
+msgctxt "new followers count header"
+msgid "%{count} New Follower"
+msgid_plural "%{count} New Followers"
+msgstr[0] ""
+msgstr[1] ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:356
+msgctxt "account archive email body - self-requested"
+msgid "<p>You requested a full backup of your Pleroma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:384
+msgctxt "account archive email subject"
+msgid "Your account archive is ready"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:188
+msgctxt "approval pending email body"
+msgid "<h3>Awaiting Approval</h3>\n<p>Your account at %{instance_name} is being reviewed by staff. You will receive another email once your account is approved.</p>\n"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:202
+msgctxt "approval pending email subject"
+msgid "Your account is awaiting approval"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:158
+msgctxt "confirmation email body"
+msgid "<h3>Thank you for registering on %{instance_name}</h3>\n<p>Email confirmation is required to activate the account.</p>\n<p>Please click the following link to <a href=\"%{confirmation_url}\">activate your account</a>.</p>\n"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:174
+msgctxt "confirmation email subject"
+msgid "%{instance_name} account confirmation"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:310
+msgctxt "digest email subject"
+msgid "Your digest from %{instance_name}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:81
+msgctxt "password reset email body"
+msgid "<h3>Reset your password at %{instance_name}</h3>\n<p>Someone has requested password change for your account at %{instance_name}.</p>\n<p>If it was you, visit the following link to proceed: <a href=\"%{password_reset_url}\">reset password</a>.</p>\n<p>If it was someone else, nothing to worry about: your data is secure and your password has not been changed.</p>\n"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:98
+msgctxt "password reset email subject"
+msgid "Password reset"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:215
+msgctxt "successful registration email body"
+msgid "<h3>Hello @%{nickname},</h3>\n<p>Your account at %{instance_name} has been registered successfully.</p>\n<p>No further action is required to activate your account.</p>\n"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:231
+msgctxt "successful registration email subject"
+msgid "Account registered on %{instance_name}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:119
+msgctxt "user invitation email body"
+msgid "<h3>You are invited to %{instance_name}</h3>\n<p>%{inviter_name} invites you to join %{instance_name}, an instance of Pleroma federated social networking platform.</p>\n<p>Click the following link to register: <a href=\"%{registration_url}\">accept invitation</a>.</p>\n"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:136
+msgctxt "user invitation email subject"
+msgid "Invitation to %{instance_name}"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:53
+msgctxt "welcome email html body"
+msgid "Welcome to %{instance_name}!"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:41
+msgctxt "welcome email subject"
+msgid "Welcome to %{instance_name}!"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:65
+msgctxt "welcome email text body"
+msgid "Welcome to %{instance_name}!"
+msgstr ""
+
+#, elixir-format
+#: lib/pleroma/emails/user_email.ex:368
+msgctxt "account archive email body - admin requested"
+msgid "<p>Admin @%{admin_nickname} requested a full backup of your Pleroma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
+msgstr ""
--- /dev/null
+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
--- /dev/null
+{
+ "attachment": {
+ "content": "Live stream preview",
+ "type": "Image",
+ "url": "https://owncast.localhost.localdomain/preview.gif?us=KjfNX387gm"
+ },
+ "attributedTo": "https://owncast.localhost.localdomain/federation/user/streamer",
+ "audience": "https://www.w3.org/ns/activitystreams#Public",
+ "content": "<p>I've gone live!</p><p></p><p><a class=\"hashtag\" href=\"https://directory.owncast.online/tags/owncast\">#owncast</a> <a class=\"hashtag\" href=\"https://directory.owncast.online/tags/streaming\">#streaming</a></p><a href=\"https://owncast.localhost.localdomain\">https://owncast.localhost.localdomain</a>",
+ "id": "https://owncast.localhost.localdomain/federation/KjBNuq8ng",
+ "published": "2022-04-17T15:42:03Z",
+ "tag": [
+ {
+ "href": "https://directory.owncast.online/tags/owncast",
+ "name": "#owncast",
+ "type": "Hashtag"
+ },
+ {
+ "href": "https://directory.owncast.online/tags/streaming",
+ "name": "#streaming",
+ "type": "Hashtag"
+ },
+ {
+ "href": "https://directory.owncast.online/tags/owncast",
+ "name": "#owncast",
+ "type": "Hashtag"
+ }
+ ],
+ "to": "https://www.w3.org/ns/activitystreams#Public",
+ "type": "Note"
+}
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
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
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?("🇵")
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)
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Activity.SearchTest do
- alias Pleroma.Activity.Search
+defmodule Pleroma.Search.DatabaseSearchTest do
+ alias Pleroma.Search.DatabaseSearch
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
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
{: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
{: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
--- /dev/null
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Search.ElasticsearchTest do
+ require Pleroma.Constants
+
+ use Pleroma.DataCase
+ use Oban.Testing, repo: Pleroma.Repo
+
+ import Pleroma.Factory
+ import Tesla.Mock
+ import Mock
+
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Workers.SearchIndexingWorker
+
+ describe "elasticsearch" do
+ setup do
+ clear_config([Pleroma.Search, :module], Pleroma.Search.Elasticsearch)
+ clear_config([Pleroma.Search.Elasticsearch.Cluster, :api], Pleroma.ElasticsearchMock)
+ end
+
+ setup_with_mocks(
+ [
+ {Pleroma.Search.Elasticsearch, [:passthrough],
+ [
+ add_to_index: fn a -> passthrough([a]) end,
+ remove_from_index: fn a -> passthrough([a]) end
+ ]},
+ {Elasticsearch, [:passthrough],
+ [
+ put_document: fn _, _, _ -> :ok end,
+ delete_document: fn _, _, _ -> :ok end
+ ]}
+ ],
+ context,
+ do: {:ok, context}
+ )
+
+ test "indexes a local post on creation" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "guys i just don't wanna leave the swamp",
+ visibility: "public"
+ })
+
+ args = %{"op" => "add_to_index", "activity" => activity.id}
+
+ assert_enqueued(
+ worker: SearchIndexingWorker,
+ args: args
+ )
+
+ assert :ok = perform_job(SearchIndexingWorker, args)
+
+ assert_called(Pleroma.Search.Elasticsearch.add_to_index(activity))
+ end
+
+ test "doesn't index posts that are not public" do
+ user = insert(:user)
+
+ Enum.each(["private", "direct"], fn visibility ->
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "guys i just don't wanna leave the swamp",
+ visibility: visibility
+ })
+
+ args = %{"op" => "add_to_index", "activity" => activity.id}
+
+ assert_enqueued(worker: SearchIndexingWorker, args: args)
+ assert :ok = perform_job(SearchIndexingWorker, args)
+
+ assert_not_called(Elasticsearch.put_document(:_))
+ end)
+
+ history = call_history(Pleroma.Search.Elasticsearch)
+ assert Enum.count(history) == 2
+ end
+
+ test "deletes posts from index when deleted locally" do
+ user = insert(:user)
+
+ mock_global(fn
+ %{method: :put, url: "http://127.0.0.1:7700/indexes/objects/documents", body: body} ->
+ assert match?(
+ [%{"content" => "guys i just don'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
--- /dev/null
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Search.MeilisearchTest do
+ require Pleroma.Constants
+
+ use Pleroma.DataCase
+ use Oban.Testing, repo: Pleroma.Repo
+
+ import Pleroma.Factory
+ import Tesla.Mock
+ import Mock
+
+ alias Pleroma.Search.Meilisearch
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Workers.SearchIndexingWorker
+
+ setup_all do
+ Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
+ :ok
+ end
+
+ describe "meilisearch" do
+ setup do: clear_config([Pleroma.Search, :module], Meilisearch)
+
+ setup_with_mocks(
+ [
+ {Meilisearch, [:passthrough],
+ [
+ add_to_index: fn a -> passthrough([a]) end,
+ remove_from_index: fn a -> passthrough([a]) end,
+ meili_put: fn u, a -> passthrough([u, a]) end
+ ]}
+ ],
+ context,
+ do: {:ok, context}
+ )
+
+ test "indexes a local post on creation" do
+ user = insert(:user)
+
+ mock_global(fn
+ %{method: :put, url: "http://127.0.0.1:7700/indexes/objects/documents", body: body} ->
+ assert match?(
+ [%{"content" => "guys i just don'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
defmodule Pleroma.UserTest do
alias Pleroma.Activity
alias Pleroma.Builders.UserBuilder
+ alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers
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
use Pleroma.DataCase, async: true
import Pleroma.Factory
+ alias Pleroma.User
alias Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy
describe "blocking based on attributes" 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
|> 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,
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()
%{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
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
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
--- /dev/null
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.GettextTest do
+ use ExUnit.Case
+
+ require Pleroma.Web.Gettext
+
+ test "put_locales/1: set the first in the list to Gettext's locale" do
+ Pleroma.Web.Gettext.put_locales(["zh_Hans", "en_test"])
+
+ assert "zh_Hans" == Gettext.get_locale(Pleroma.Web.Gettext)
+ end
+
+ test "with_locales/2: reset locale on exit" do
+ old_first_locale = Gettext.get_locale(Pleroma.Web.Gettext)
+ old_locales = Pleroma.Web.Gettext.get_locales()
+
+ Pleroma.Web.Gettext.with_locales ["zh_Hans", "en_test"] do
+ assert "zh_Hans" == Gettext.get_locale(Pleroma.Web.Gettext)
+ assert ["zh_Hans", "en_test"] == Pleroma.Web.Gettext.get_locales()
+ end
+
+ assert old_first_locale == Gettext.get_locale(Pleroma.Web.Gettext)
+ assert old_locales == Pleroma.Web.Gettext.get_locales()
+ end
+
+ describe "handle_missing_translation/5" do
+ test "fallback to next locale if some translation is not available" do
+ Pleroma.Web.Gettext.with_locales ["x_unsupported", "en_test"] do
+ assert "xxYour account is awaiting approvalxx" ==
+ Pleroma.Web.Gettext.dpgettext(
+ "static_pages",
+ "approval pending email subject",
+ "Your account is awaiting approval"
+ )
+ end
+ end
+
+ test "putting en locale at the front should not make gettext fallback unexpectedly" do
+ Pleroma.Web.Gettext.with_locales ["en", "en_test"] do
+ assert "Your account is awaiting approval" ==
+ Pleroma.Web.Gettext.dpgettext(
+ "static_pages",
+ "approval pending email subject",
+ "Your account is awaiting approval"
+ )
+ end
+ end
+
+ test "duplicated locale in list should not result in infinite loops" do
+ Pleroma.Web.Gettext.with_locales ["x_unsupported", "x_unsupported", "en_test"] do
+ assert "xxYour account is awaiting approvalxx" ==
+ Pleroma.Web.Gettext.dpgettext(
+ "static_pages",
+ "approval pending email subject",
+ "Your account is awaiting approval"
+ )
+ end
+ end
+
+ test "direct interpolation" do
+ Pleroma.Web.Gettext.with_locales ["en_test"] do
+ assert "xxYour digest from some instancexx" ==
+ Pleroma.Web.Gettext.dpgettext(
+ "static_pages",
+ "digest email subject",
+ "Your digest from %{instance_name}",
+ instance_name: "some instance"
+ )
+ end
+ end
+
+ test "fallback with interpolation" do
+ Pleroma.Web.Gettext.with_locales ["x_unsupported", "en_test"] do
+ assert "xxYour digest from some instancexx" ==
+ Pleroma.Web.Gettext.dpgettext(
+ "static_pages",
+ "digest email subject",
+ "Your digest from %{instance_name}",
+ instance_name: "some instance"
+ )
+ end
+ end
+
+ test "fallback to msgid" do
+ Pleroma.Web.Gettext.with_locales ["x_unsupported"] do
+ assert "Your digest from some instance" ==
+ Pleroma.Web.Gettext.dpgettext(
+ "static_pages",
+ "digest email subject",
+ "Your digest from %{instance_name}",
+ instance_name: "some instance"
+ )
+ end
+ end
+ end
+
+ describe "handle_missing_plural_translation/7" do
+ test "direct interpolation" do
+ Pleroma.Web.Gettext.with_locales ["en_test"] do
+ assert "xx1 New Followerxx" ==
+ Pleroma.Web.Gettext.dpngettext(
+ "static_pages",
+ "new followers count header",
+ "%{count} New Follower",
+ "%{count} New Followers",
+ 1,
+ count: 1
+ )
+
+ assert "xx5 New Followersxx" ==
+ Pleroma.Web.Gettext.dpngettext(
+ "static_pages",
+ "new followers count header",
+ "%{count} New Follower",
+ "%{count} New Followers",
+ 5,
+ count: 5
+ )
+ end
+ end
+
+ test "fallback with interpolation" do
+ Pleroma.Web.Gettext.with_locales ["x_unsupported", "en_test"] do
+ assert "xx1 New Followerxx" ==
+ Pleroma.Web.Gettext.dpngettext(
+ "static_pages",
+ "new followers count header",
+ "%{count} New Follower",
+ "%{count} New Followers",
+ 1,
+ count: 1
+ )
+
+ assert "xx5 New Followersxx" ==
+ Pleroma.Web.Gettext.dpngettext(
+ "static_pages",
+ "new followers count header",
+ "%{count} New Follower",
+ "%{count} New Followers",
+ 5,
+ count: 5
+ )
+ end
+ end
+
+ test "fallback to msgid" do
+ Pleroma.Web.Gettext.with_locales ["x_unsupported"] do
+ assert "1 New Follower" ==
+ Pleroma.Web.Gettext.dpngettext(
+ "static_pages",
+ "new followers count header",
+ "%{count} New Follower",
+ "%{count} New Followers",
+ 1,
+ count: 1
+ )
+
+ assert "5 New Followers" ==
+ Pleroma.Web.Gettext.dpngettext(
+ "static_pages",
+ "new followers count header",
+ "%{count} New Follower",
+ "%{count} New Followers",
+ 5,
+ count: 5
+ )
+ end
+ end
+ end
+end
alias Pleroma.Web.ActivityPub.InternalFetchActor
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.OAuth.Token
+ alias Pleroma.Web.Plugs.SetLocalePlug
import Pleroma.Factory
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"])
} = 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)
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
|> 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
|> 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
|> SetLocalePlug.call([])
assert "en" == Gettext.get_locale()
- assert %{locale: "en"} == conn.assigns
+ assert %{locale: "en"} = conn.assigns
end
end
--- /dev/null
+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