Merge branch 'documentation-migration' of akkoma.dev:AkkomaGang/akkoma into documentation-migration

This commit is contained in:
FloatingGhost 2022-07-01 13:15:28 +01:00
commit ff89d4526a
117 changed files with 5049 additions and 1331 deletions

View file

@ -16,7 +16,9 @@ pipeline:
glibc:
when:
event:
- tag
- push
branch:
- develop
secrets:
- SCW_ACCESS_KEY
- SCW_SECRET_KEY
@ -44,7 +46,9 @@ pipeline:
musl:
when:
event:
- tag
- push
branch:
- develop
secrets:
- SCW_ACCESS_KEY
- SCW_SECRET_KEY

View file

@ -11,6 +11,7 @@ pipeline:
when:
event:
- push
- pull_request
environment:
MIX_ENV: test
commands:
@ -25,6 +26,7 @@ pipeline:
when:
event:
- push
- pull_request
environment:
MIX_ENV: test
POSTGRES_DB: pleroma_test

View file

@ -54,6 +54,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Readded mastoFE
- Added support for custom emoji reactions
- Added `emoji_url` in notifications to allow for custom emoji rendering
- Make backend-rendered pages translatable. This includes emails. Pages returned as a HTTP response are translated using the language specified in the `userLanguage` cookie, or the `Accept-Language` header. Emails are translated using the `language` field when registering. This language can be changed by `PATCH /api/v1/accounts/update_credentials` with the `language` field.
### Fixed
- Subscription(Bell) Notifications: Don't create from Pipeline Ingested replies
@ -116,6 +117,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Improved Twittercard and OpenGraph meta tag generation including thumbnails and image dimension metadata when available.
- AdminAPI: sort users so the newest are at the top.
- ActivityPub Client-to-Server(C2S): Limitation on the type of Activity/Object are lifted as they are now passed through ObjectValidators
- MRF (`AntiFollowbotPolicy`): Bot accounts are now also considered followbots. Users can still allow bots to follow them by first following the bot.
### Added

View file

@ -568,7 +568,8 @@
remote_fetcher: 2,
attachments_cleanup: 1,
new_users_digest: 1,
mute_expire: 5
mute_expire: 5,
search_indexing: 10
],
plugins: [Oban.Plugins.Pruner],
crontab: [
@ -579,7 +580,8 @@
config :pleroma, :workers,
retries: [
federator_incoming: 5,
federator_outgoing: 5
federator_outgoing: 5,
search_indexing: 2
]
config :pleroma, Pleroma.Formatter,
@ -842,17 +844,32 @@
config :pleroma, ConcurrentLimiter, [
{Pleroma.Web.RichMedia.Helpers, [max_running: 5, max_waiting: 5]},
{Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy, [max_running: 5, max_waiting: 5]}
{Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy, [max_running: 5, max_waiting: 5]},
{Pleroma.Search, [max_running: 30, max_waiting: 50]}
]
config :pleroma, :search, provider: Pleroma.Search.Builtin
config :pleroma, Pleroma.Search, module: Pleroma.Search.DatabaseSearch
config :pleroma, :telemetry,
slow_queries_logging: [
enabled: false,
min_duration: 500_000,
exclude_sources: [nil, "oban_jobs"]
]
config :pleroma, Pleroma.Search.Meilisearch,
url: "http://127.0.0.1:7700/",
private_key: nil,
initial_indexing_chunk_size: 100_000
config :pleroma, Pleroma.Search.Elasticsearch.Cluster,
url: "http://localhost:9200",
username: "elastic",
password: "changeme",
api: Elasticsearch.API.HTTP,
json_library: Jason,
indexes: %{
activities: %{
settings: "priv/es-mappings/activity.json",
store: Pleroma.Search.Elasticsearch.Store,
sources: [Pleroma.Activity],
bulk_page_size: 1000,
bulk_wait_interval: 15_000
}
}
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.

View file

@ -3358,5 +3358,133 @@
]
}
]
},
%{
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]
}
]
}
]
}
]
}
]

View file

@ -134,6 +134,10 @@
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

View file

@ -17,11 +17,11 @@ su pleroma -s $SHELL -lc "./bin/pleroma_ctl migrate"
## For from source installations (using git)
1. Go to the working directory of Pleroma (default is `/opt/pleroma`)
2. Run `git pull`. This pulls the latest changes from upstream.
2. Run `git pull` [^1]. This pulls the latest changes from upstream.
3. Run `mix deps.get` [^1]. This pulls in any new dependencies.
4. Stop the Pleroma service.
5. Run `mix ecto.migrate` [^1] [^2]. This task performs database migrations, if there were any.
6. Start the Pleroma service.
[^1]: Depending on which install guide you followed (for example on Debian/Ubuntu), you want to run `mix` tasks as `pleroma` user by adding `sudo -Hu pleroma` before the command.
[^1]: Depending on which install guide you followed (for example on Debian/Ubuntu), you want to run `git` and `mix` tasks as `pleroma` user by adding `sudo -Hu pleroma` before the command.
[^2]: Prefix with `MIX_ENV=prod` to run it using the production config file.

View file

@ -125,6 +125,8 @@ To add configuration to your config file, you can copy it from the base config.
* `Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy`: Sets a default expiration on all posts made by users of the local instance. Requires `Pleroma.Workers.PurgeExpiredActivity` to be enabled for processing the scheduled delections.
* `Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy`: Makes all bot posts to disappear from public timelines.
* `Pleroma.Web.ActivityPub.MRF.FollowBotPolicy`: Automatically follows newly discovered users from the specified bot account. Local accounts, locked accounts, and users with "#nobot" in their bio are respected and excluded from being followed.
* `Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy`: Drops follow requests from followbots. Users can still allow bots to follow them by first following the bot.
* `Pleroma.Web.ActivityPub.MRF.KeywordPolicy`: Rejects or removes from the federated timeline or replaces keywords. (See [`:mrf_keyword`](#mrf_keyword)).
* `transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo).
* `transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.

View file

@ -0,0 +1,163 @@
# Configuring search
{! backend/administration/CLI_tasks/general_cli_task_info.include !}
## Built-in search
To use built-in search that has no external dependencies, set the search module to `Pleroma.Activity`:
> config :pleroma, Pleroma.Search, module: Pleroma.Search.DatabaseSearch
While it has no external dependencies, it has problems with performance and relevancy.
## Meilisearch
Note that it's quite a bit more memory hungry than PostgreSQL (around 4-5G for ~1.2 million
posts while idle and up to 7G while indexing initially). The disk usage for this additional index is also
around 4 gigabytes. Like [RUM](./cheatsheet.md#rum-indexing-for-full-text-search) indexes, it offers considerably
higher performance and ordering by timestamp in a reasonable amount of time.
Additionally, the search results seem to be more accurate.
Due to high memory usage, it may be best to set it up on a different machine, if running pleroma on a low-resource
computer, and use private key authentication to secure the remote search instance.
To use [meilisearch](https://www.meilisearch.com/), set the search module to `Pleroma.Search.Meilisearch`:
> config :pleroma, Pleroma.Search, module: Pleroma.Search.Meilisearch
You then need to set the address of the meilisearch instance, and optionally the private key for authentication. You might
also want to change the `initial_indexing_chunk_size` to be smaller if you're server is not very powerful, but not higher than `100_000`,
because meilisearch will refuse to process it if it's too big. However, in general you want this to be as big as possible, because meilisearch
indexes faster when it can process many posts in a single batch.
> config :pleroma, Pleroma.Search.Meilisearch,
> url: "http://127.0.0.1:7700/",
> private_key: "private key",
> initial_indexing_chunk_size: 100_000
Information about setting up meilisearch can be found in the
[official documentation](https://docs.meilisearch.com/learn/getting_started/installation.html).
You probably want to start it with `MEILI_NO_ANALYTICS=true` environment variable to disable analytics.
At least version 0.25.0 is required, but you are strongly adviced to use at least 0.26.0, as it introduces
the `--enable-auto-batching` option which drastically improves performance. Without this option, the search
is hardly usable on a somewhat big instance.
### Private key authentication (optional)
To set the private key, use the `MEILI_MASTER_KEY` environment variable when starting. After setting the _master key_,
you have to get the _private key_, which is actually used for authentication.
=== "OTP"
```sh
./bin/pleroma_ctl search.meilisearch show-keys <your master key here>
```
=== "From Source"
```sh
mix pleroma.search.meilisearch show-keys <your master key here>
```
You will see a "Default Admin API Key", this is the key you actually put into your configuration file.
### Initial indexing
After setting up the configuration, you'll want to index all of your already existsing posts. Only public posts are indexed. You'll only
have to do it one time, but it might take a while, depending on the amount of posts your instance has seen. This is also a fairly RAM
consuming process for `meilisearch`, and it will take a lot of RAM when running if you have a lot of posts (seems to be around 5G for ~1.2
million posts while idle and up to 7G while indexing initially, but your experience may be different).
The sequence of actions is as follows:
1. First, change the configuration to use `Pleroma.Search.Meilisearch` as the search backend
2. Restart your instance, at this point it can be used while the search indexing is running, though search won't return anything
3. Start the initial indexing process (as described below with `index`),
and wait until the task says it sent everything from the database to index
4. Wait until everything is actually indexed (by checking with `stats` as described below),
at this point you don't have to do anything, just wait a while.
To start the initial indexing, run the `index` command:
=== "OTP"
```sh
./bin/pleroma_ctl search.meilisearch index
```
=== "From Source"
```sh
mix pleroma.search.meilisearch index
```
This will show you the total amount of posts to index, and then show you the amount of posts indexed currently, until the numbers eventually
become the same. The posts are indexed in big batches and meilisearch will take some time to actually index them, even after you have
inserted all the posts into it. Depending on the amount of posts, this may be as long as several hours. To get information about the status
of indexing and how many posts have actually been indexed, use the `stats` command:
=== "OTP"
```sh
./bin/pleroma_ctl search.meilisearch stats
```
=== "From Source"
```sh
mix pleroma.search.meilisearch stats
```
### Clearing the index
In case you need to clear the index (for example, to re-index from scratch, if that needs to happen for some reason), you can
use the `clear` command:
=== "OTP"
```sh
./bin/pleroma_ctl search.meilisearch clear
```
=== "From Source"
```sh
mix pleroma.search.meilisearch clear
```
This will clear **all** the posts from the search index. Note, that deleted posts are also removed from index by the instance itself, so
there is no need to actually clear the whole index, unless you want **all** of it gone. That said, the index does not hold any information
that cannot be re-created from the database, it should also generally be a lot smaller than the size of your database. Still, the size
depends on the amount of text in posts.
## Elasticsearch
As with meilisearch, this can be rather memory-hungry, but it is very good at what it does.
To use [elasticsearch](https://www.elastic.co/), set the search module to `Pleroma.Search.Elasticsearch`:
> config :pleroma, Pleroma.Search, module: Pleroma.Search.Elasticsearch
You then need to set the URL and authentication credentials if relevant.
> config :pleroma, Pleroma.Search.Elasticsearch.Cluster,
> url: "http://127.0.0.1:9200/",
> username: "elastic",
> password: "changeme",
### Initial indexing
After setting up the configuration, you'll want to index all of your already existsing posts. Only public posts are indexed. You'll only
have to do it one time, but it might take a while, depending on the amount of posts your instance has seen.
The sequence of actions is as follows:
1. First, change the configuration to use `Pleroma.Search.Elasticsearch` as the search backend
2. Restart your instance, at this point it can be used while the search indexing is running, though search won't return anything
3. Start the initial indexing process (as described below with `index`),
and wait until the task says it sent everything from the database to index
4. Wait until the index tasks exits
To start the initial indexing, run the `build` command:
=== "OTP"
```sh
./bin/pleroma_ctl search import activities
```
=== "From Source"
```sh
mix pleroma.search import activities
```

View file

@ -241,6 +241,7 @@ Additional parameters can be added to the JSON body/Form data:
- `discoverable` - if true, external services (search bots) etc. are allowed to index / list the account (regardless of this setting, user will still appear in regular search results).
- `actor_type` - the type of this account.
- `accepts_chat_messages` - if false, this account will reject all chat messages.
- `language` - user's preferred language for receiving emails (digest, confirmation, etc.)
All images (avatar, banner and background) can be reset to the default by sending an empty string ("") instead of a file.
@ -292,6 +293,7 @@ Has these additional parameters (which are the same as in Pleroma-API):
- `captcha_token`: optional, contains provider-specific captcha token
- `captcha_answer_data`: optional, contains provider-specific captcha data
- `token`: invite token required when the registrations aren't public.
- `language`: optional, user's preferred language for receiving emails (digest, confirmation, etc.), default to the language set in the `userLanguage` cookies or `Accept-Language` header.
## Instance

View file

@ -57,7 +57,8 @@ def start_pleroma do
{Majic.Pool,
[name: Pleroma.MajicPool, pool_size: Pleroma.Config.get([:majic_pool, :size], 2)]}
] ++
http_children(adapter)
http_children(adapter) ++
elasticsearch_children()
cachex_children = Enum.map(@cachex_children, &Pleroma.Application.build_cachex(&1, []))
@ -136,4 +137,14 @@ defp http_children(Tesla.Adapter.Gun) do
end
defp http_children(_), do: []
def elasticsearch_children do
config = Pleroma.Config.get([Pleroma.Search, :module])
if config == Pleroma.Search.Elasticsearch do
[Pleroma.Search.Elasticsearch.Cluster]
else
[]
end
end
end

View file

@ -5,60 +5,16 @@
defmodule Mix.Tasks.Pleroma.Search do
use Mix.Task
import Mix.Pleroma
import Ecto.Query
alias Pleroma.Activity
alias Pleroma.Pagination
alias Pleroma.User
alias Pleroma.Hashtag
@shortdoc "Manages elasticsearch"
def run(["import", "activities" | _rest]) do
start_pleroma()
from(a in Activity, where: not ilike(a.actor, "%/relay"))
|> where([a], fragment("(? ->> 'type'::text) = 'Create'", a.data))
|> Activity.with_preloaded_object()
|> Activity.with_preloaded_user_actor()
|> get_all(:activities)
end
def run(["import", "users" | _rest]) do
start_pleroma()
from(u in User, where: u.nickname not in ["internal.fetch", "relay"])
|> get_all(:users)
end
def run(["import", "hashtags" | _rest]) do
start_pleroma()
from(h in Hashtag)
|> Pleroma.Repo.all()
|> Pleroma.Elasticsearch.bulk_post(:hashtags)
end
defp get_all(query, index, max_id \\ nil) do
params = %{limit: 1000}
params =
if max_id == nil do
params
else
Map.put(params, :max_id, max_id)
end
res =
query
|> Pagination.fetch_paginated(params)
if res == [] do
:ok
else
res
|> Pleroma.Elasticsearch.bulk_post(index)
get_all(query, index, List.last(res).id)
end
Elasticsearch.Index.Bulk.upload(
Pleroma.Search.Elasticsearch.Cluster,
"activities",
Pleroma.Config.get([Pleroma.Search.Elasticsearch.Cluster, :indexes, :activities])
)
end
end

View file

@ -0,0 +1,144 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Search.Meilisearch do
require Pleroma.Constants
import Mix.Pleroma
import Ecto.Query
import Pleroma.Search.Meilisearch,
only: [meili_post: 2, meili_put: 2, meili_get: 1, meili_delete!: 1]
def run(["index"]) do
start_pleroma()
meili_version =
(
{:ok, result} = meili_get("/version")
result["pkgVersion"]
)
# The ranking rule syntax was changed but nothing about that is mentioned in the changelog
if not Version.match?(meili_version, ">= 0.25.0") do
raise "Meilisearch <0.24.0 not supported"
end
{:ok, _} =
meili_post(
"/indexes/objects/settings/ranking-rules",
[
"published:desc",
"words",
"exactness",
"proximity",
"typo",
"attribute",
"sort"
]
)
{:ok, _} =
meili_post(
"/indexes/objects/settings/searchable-attributes",
[
"content"
]
)
IO.puts("Created indices. Starting to insert posts.")
chunk_size = Pleroma.Config.get([Pleroma.Search.Meilisearch, :initial_indexing_chunk_size])
Pleroma.Repo.transaction(
fn ->
query =
from(Pleroma.Object,
# Only index public and unlisted posts which are notes and have some text
where:
fragment("data->>'type' = 'Note'") and
(fragment("data->'to' \\? ?", ^Pleroma.Constants.as_public()) or
fragment("data->'cc' \\? ?", ^Pleroma.Constants.as_public())),
order_by: [desc: fragment("data->'published'")]
)
count = query |> Pleroma.Repo.aggregate(:count, :data)
IO.puts("Entries to index: #{count}")
Pleroma.Repo.stream(
query,
timeout: :infinity
)
|> Stream.map(&Pleroma.Search.Meilisearch.object_to_search_data/1)
|> Stream.filter(fn o -> not is_nil(o) end)
|> Stream.chunk_every(chunk_size)
|> Stream.transform(0, fn objects, acc ->
new_acc = acc + Enum.count(objects)
# Reset to the beginning of the line and rewrite it
IO.write("\r")
IO.write("Indexed #{new_acc} entries")
{[objects], new_acc}
end)
|> Stream.each(fn objects ->
result =
meili_put(
"/indexes/objects/documents",
objects
)
with {:ok, res} <- result do
if not Map.has_key?(res, "uid") do
IO.puts("\nFailed to index: #{inspect(result)}")
end
else
e -> IO.puts("\nFailed to index due to network error: #{inspect(e)}")
end
end)
|> Stream.run()
end,
timeout: :infinity
)
IO.write("\n")
end
def run(["clear"]) do
start_pleroma()
meili_delete!("/indexes/objects/documents")
end
def run(["show-keys", master_key]) do
start_pleroma()
endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url])
{:ok, result} =
Pleroma.HTTP.get(
Path.join(endpoint, "/keys"),
[{"Authorization", "Bearer #{master_key}"}]
)
decoded = Jason.decode!(result.body)
if decoded["results"] do
Enum.each(decoded["results"], fn %{"description" => desc, "key" => key} ->
IO.puts("#{desc}: #{key}")
end)
else
IO.puts("Error fetching the keys, check the master key is correct: #{inspect(decoded)}")
end
end
def run(["stats"]) do
start_pleroma()
{:ok, result} = meili_get("/indexes/objects/stats")
IO.puts("Number of entries: #{result["numberOfDocuments"]}")
IO.puts("Indexing? #{result["isIndexing"]}")
end
end

View file

@ -367,7 +367,7 @@ def restrict_deactivated_users(query) do
from(activity in query, where: activity.actor not in subquery(deactivated_users_query))
end
defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search
defdelegate search(user, query, options \\ []), to: Pleroma.Search.DatabaseSearch
def direct_conversation_id(activity, for_user) do
alias Pleroma.Conversation.Participation

View file

@ -105,6 +105,7 @@ def start(_type, _args) do
{Oban, Config.get(Oban)},
Pleroma.Web.Endpoint
] ++
elasticsearch_children() ++
task_children(@mix_env) ++
dont_run_in_test(@mix_env) ++
shout_child(shout_enabled?())
@ -303,11 +304,25 @@ defp http_children(Tesla.Adapter.Gun, _) do
defp http_children(_, _), do: []
def elasticsearch_children do
config = Config.get([Pleroma.Search, :module])
if config == Pleroma.Search.Elasticsearch do
[Pleroma.Search.Elasticsearch.Cluster]
else
[]
end
end
@spec limiters_setup() :: :ok
def limiters_setup do
config = Config.get(ConcurrentLimiter, [])
[Pleroma.Web.RichMedia.Helpers, Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy]
[
Pleroma.Web.RichMedia.Helpers,
Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy,
Pleroma.Search
]
|> Enum.each(fn module ->
mod_config = Keyword.get(config, module, [])

View file

@ -1,19 +0,0 @@
# Akkoma: A lightweight social networking server
# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Elasticsearch.DocumentMappings.Activity do
alias Pleroma.Object
def id(obj), do: obj.id
def encode(%{object: %{data: %{"type" => "Note"}}} = activity) do
%{
_timestamp: activity.inserted_at,
user: activity.user_actor.nickname,
content: activity.object.data["content"],
instance: URI.parse(activity.user_actor.ap_id).host,
hashtags: Object.hashtags(activity.object)
}
end
end

View file

@ -1,21 +0,0 @@
# Akkoma: A lightweight social networking server
# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Elasticsearch.DocumentMappings.Hashtag do
def id(obj), do: obj.id
def encode(%{timestamp: _} = hashtag) do
%{
hashtag: hashtag.name,
timestamp: hashtag.timestamp
}
end
def encode(hashtag) do
%{
hashtag: hashtag.name,
timestamp: hashtag.inserted_at
}
end
end

View file

@ -1,17 +0,0 @@
# Akkoma: A lightweight social networking server
# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Elasticsearch.DocumentMappings.User do
def id(obj), do: obj.id
def encode(%{actor_type: "Person"} = user) do
%{
timestamp: user.inserted_at,
instance: URI.parse(user.ap_id).host,
nickname: user.nickname,
bio: user.bio,
display_name: user.name
}
end
end

View file

@ -1,256 +0,0 @@
# Akkoma: A lightweight social networking server
# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Elasticsearch do
alias Pleroma.Activity
alias Pleroma.User
alias Pleroma.Object
alias Pleroma.Elasticsearch.DocumentMappings
alias Pleroma.Config
require Logger
defp url do
Config.get([:elasticsearch, :url])
end
defp enabled? do
Config.get([:search, :provider]) == Pleroma.Search.Elasticsearch
end
def delete_by_id(:activity, id) do
if enabled?() do
Elastix.Document.delete(url(), "activities", "activity", id)
end
end
def put_by_id(:activity, id) do
id
|> Activity.get_by_id_with_object()
|> maybe_put_into_elasticsearch()
end
def maybe_put_into_elasticsearch({:ok, item}) do
maybe_put_into_elasticsearch(item)
end
def maybe_put_into_elasticsearch(
%{data: %{"type" => "Create"}, object: %{data: %{"type" => "Note"}}} = activity
) do
if enabled?() do
actor = Pleroma.Activity.user_actor(activity)
activity
|> Map.put(:user_actor, actor)
|> put()
end
end
def maybe_put_into_elasticsearch(%User{actor_type: "Person"} = user) do
if enabled?() do
put(user)
end
end
def maybe_put_into_elasticsearch(_) do
{:ok, :skipped}
end
def maybe_bulk_post(data, type) do
if enabled?() do
bulk_post(data, type)
end
end
def put(%Activity{} = activity) do
with {:ok, _} <-
Elastix.Document.index(
url(),
"activities",
"activity",
DocumentMappings.Activity.id(activity),
DocumentMappings.Activity.encode(activity)
) do
activity
|> Map.get(:object)
|> Object.hashtags()
|> Enum.map(fn x ->
%{id: x, name: x, timestamp: DateTime.to_iso8601(DateTime.utc_now())}
end)
|> bulk_post(:hashtags)
else
{:error, %{reason: err}} ->
Logger.error("Could not put activity: #{err}")
:skipped
end
end
def put(%User{} = user) do
with {:ok, _} <-
Elastix.Document.index(
url(),
"users",
"user",
DocumentMappings.User.id(user),
DocumentMappings.User.encode(user)
) do
:ok
else
{:error, %{reason: err}} ->
Logger.error("Could not put user: #{err}")
:skipped
end
end
def bulk_post(data, :activities) do
d =
data
|> Enum.filter(fn x ->
t =
x.object
|> Map.get(:data, %{})
|> Map.get("type", "")
t == "Note"
end)
|> Enum.map(fn d ->
[
%{index: %{_id: DocumentMappings.Activity.id(d)}},
DocumentMappings.Activity.encode(d)
]
end)
|> List.flatten()
with {:ok, %{body: %{"errors" => false}}} <-
Elastix.Bulk.post(
url(),
d,
index: "activities",
type: "activity"
) do
:ok
else
{:error, %{reason: err}} ->
Logger.error("Could not bulk put activity: #{err}")
:skipped
{:ok, %{body: _}} ->
:skipped
end
end
def bulk_post(data, :users) do
d =
data
|> Enum.filter(fn x -> x.actor_type == "Person" end)
|> Enum.map(fn d ->
[
%{index: %{_id: DocumentMappings.User.id(d)}},
DocumentMappings.User.encode(d)
]
end)
|> List.flatten()
with {:ok, %{body: %{"errors" => false}}} <-
Elastix.Bulk.post(
url(),
d,
index: "users",
type: "user"
) do
:ok
else
{:error, %{reason: err}} ->
Logger.error("Could not bulk put users: #{err}")
:skipped
{:ok, %{body: _}} ->
:skipped
end
end
def bulk_post(data, :hashtags) when is_list(data) do
d =
data
|> Enum.map(fn d ->
[
%{index: %{_id: DocumentMappings.Hashtag.id(d)}},
DocumentMappings.Hashtag.encode(d)
]
end)
|> List.flatten()
with {:ok, %{body: %{"errors" => false}}} <-
Elastix.Bulk.post(
url(),
d,
index: "hashtags",
type: "hashtag"
) do
:ok
else
{:error, %{reason: err}} ->
Logger.error("Could not bulk put hashtags: #{err}")
:skipped
{:ok, %{body: _}} ->
:skipped
end
end
def bulk_post(_, :hashtags), do: {:ok, nil}
def search(_, _, _, :skip), do: []
def search(:raw, index, type, q) do
with {:ok, raw_results} <- Elastix.Search.search(url(), index, [type], q) do
results =
raw_results
|> Map.get(:body, %{})
|> Map.get("hits", %{})
|> Map.get("hits", [])
{:ok, results}
else
{:error, e} ->
Logger.error(e)
{:error, e}
end
end
def search(:activities, q) do
with {:ok, results} <- search(:raw, "activities", "activity", q) do
results
|> Enum.map(fn result -> result["_id"] end)
|> Pleroma.Activity.all_by_ids_with_object()
|> Enum.sort(&(&1.inserted_at >= &2.inserted_at))
else
e ->
Logger.error(e)
[]
end
end
def search(:users, q) do
with {:ok, results} <- search(:raw, "users", "user", q) do
results
|> Enum.map(fn result -> result["_id"] end)
|> Pleroma.User.get_all_by_ids()
else
e ->
Logger.error(e)
[]
end
end
def search(:hashtags, q) do
with {:ok, results} <- search(:raw, "hashtags", "hashtag", q) do
results
|> Enum.map(fn result -> result["_source"]["hashtag"] end)
else
e ->
Logger.error(e)
[]
end
end
end

View file

@ -5,9 +5,12 @@
defmodule Pleroma.Emails.UserEmail do
@moduledoc "User emails"
require Pleroma.Web.Gettext
alias Pleroma.Config
alias Pleroma.User
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Gettext
alias Pleroma.Web.Router
import Swoosh.Email
@ -27,29 +30,75 @@ defp recipient(%User{} = user), do: recipient(user.email, user.name)
@spec welcome(User.t(), map()) :: Swoosh.Email.t()
def welcome(user, opts \\ %{}) do
new()
|> to(recipient(user))
|> from(Map.get(opts, :sender, sender()))
|> subject(Map.get(opts, :subject, "Welcome to #{instance_name()}!"))
|> html_body(Map.get(opts, :html, "Welcome to #{instance_name()}!"))
|> text_body(Map.get(opts, :text, "Welcome to #{instance_name()}!"))
Gettext.with_locale_or_default user.language do
new()
|> to(recipient(user))
|> from(Map.get(opts, :sender, sender()))
|> subject(
Map.get(
opts,
:subject,
Gettext.dpgettext(
"static_pages",
"welcome email subject",
"Welcome to %{instance_name}!",
instance_name: instance_name()
)
)
)
|> html_body(
Map.get(
opts,
:html,
Gettext.dpgettext(
"static_pages",
"welcome email html body",
"Welcome to %{instance_name}!",
instance_name: instance_name()
)
)
)
|> text_body(
Map.get(
opts,
:text,
Gettext.dpgettext(
"static_pages",
"welcome email text body",
"Welcome to %{instance_name}!",
instance_name: instance_name()
)
)
)
end
end
def password_reset_email(user, token) when is_binary(token) do
password_reset_url = Router.Helpers.reset_password_url(Endpoint, :reset, token)
Gettext.with_locale_or_default user.language do
password_reset_url = Router.Helpers.reset_password_url(Endpoint, :reset, token)
html_body = """
<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>
"""
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("Password reset")
|> html_body(html_body)
new()
|> to(recipient(user))
|> from(sender())
|> subject(
Gettext.dpgettext("static_pages", "password reset email subject", "Password reset")
)
|> html_body(html_body)
end
end
def user_invitation_email(
@ -58,73 +107,136 @@ 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
)
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 = """
<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)
|> 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
)
new()
|> to(recipient(user))
|> from(sender())
|> subject(
Gettext.dpgettext(
"static_pages",
"confirmation email subject",
"%{instance_name} account confirmation",
instance_name: instance_name()
)
)
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)
|> 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>
"""
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("Your account is awaiting approval")
|> html_body(html_body)
new()
|> to(recipient(user))
|> from(sender())
|> subject(
Gettext.dpgettext(
"static_pages",
"approval pending email subject",
"Your account is awaiting approval"
)
)
|> html_body(html_body)
end
end
def successful_registration_email(user) do
html_body = """
<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>
"""
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("Account registered on #{instance_name()}")
|> html_body(html_body)
new()
|> to(recipient(user))
|> from(sender())
|> subject(
Gettext.dpgettext(
"static_pages",
"successful registration email subject",
"Account registered on %{instance_name}",
instance_name: instance_name()
)
)
|> html_body(html_body)
end
end
@doc """
@ -134,69 +246,78 @@ def successful_registration_email(user) do
"""
@spec digest_email(User.t()) :: Swoosh.Email.t() | nil
def digest_email(user) do
notifications = Pleroma.Notification.for_user_since(user, user.last_digest_emailed_at)
Gettext.with_locale_or_default user.language do
notifications = Pleroma.Notification.for_user_since(user, user.last_digest_emailed_at)
mentions =
notifications
|> Enum.filter(&(&1.activity.data["type"] == "Create"))
|> Enum.map(fn notification ->
object = Pleroma.Object.normalize(notification.activity, fetch: false)
mentions =
notifications
|> Enum.filter(&(&1.activity.data["type"] == "Create"))
|> Enum.map(fn notification ->
object = Pleroma.Object.normalize(notification.activity, fetch: false)
if not is_nil(object) do
object = update_in(object.data["content"], &format_links/1)
if not is_nil(object) do
object = update_in(object.data["content"], &format_links/1)
%{
data: notification,
object: object,
from: User.get_by_ap_id(notification.activity.actor)
}
end
end)
|> Enum.filter(& &1)
%{
data: notification,
object: object,
from: User.get_by_ap_id(notification.activity.actor)
}
end
end)
|> Enum.filter(& &1)
followers =
notifications
|> Enum.filter(&(&1.activity.data["type"] == "Follow"))
|> Enum.map(fn notification ->
from = User.get_by_ap_id(notification.activity.actor)
followers =
notifications
|> Enum.filter(&(&1.activity.data["type"] == "Follow"))
|> Enum.map(fn notification ->
from = User.get_by_ap_id(notification.activity.actor)
if not is_nil(from) do
%{
data: notification,
object: Pleroma.Object.normalize(notification.activity, fetch: false),
from: User.get_by_ap_id(notification.activity.actor)
}
end
end)
|> Enum.filter(& &1)
if not is_nil(from) do
%{
data: notification,
object: Pleroma.Object.normalize(notification.activity, fetch: false),
from: User.get_by_ap_id(notification.activity.actor)
}
end
end)
|> Enum.filter(& &1)
unless Enum.empty?(mentions) do
styling = Config.get([__MODULE__, :styling])
logo = Config.get([__MODULE__, :logo])
unless Enum.empty?(mentions) do
styling = Config.get([__MODULE__, :styling])
logo = Config.get([__MODULE__, :logo])
html_data = %{
instance: instance_name(),
user: user,
mentions: mentions,
followers: followers,
unsubscribe_link: unsubscribe_url(user, "digest"),
styling: styling
}
html_data = %{
instance: instance_name(),
user: user,
mentions: mentions,
followers: followers,
unsubscribe_link: unsubscribe_url(user, "digest"),
styling: styling
}
logo_path =
if is_nil(logo) do
Path.join(:code.priv_dir(:pleroma), "static/static/logo.svg")
else
Path.join(Config.get([:instance, :static_dir]), logo)
end
logo_path =
if is_nil(logo) do
Path.join(:code.priv_dir(:pleroma), "static/static/logo.svg")
else
Path.join(Config.get([:instance, :static_dir]), logo)
end
new()
|> to(recipient(user))
|> from(sender())
|> subject("Your digest from #{instance_name()}")
|> put_layout(false)
|> render_body("digest.html", html_data)
|> attachment(Swoosh.Attachment.new(logo_path, filename: "logo.svg", type: :inline))
new()
|> to(recipient(user))
|> from(sender())
|> subject(
Gettext.dpgettext(
"static_pages",
"digest email subject",
"Your digest from %{instance_name}",
instance_name: instance_name()
)
)
|> put_layout(false)
|> render_body("digest.html", html_data)
|> attachment(Swoosh.Attachment.new(logo_path, filename: "logo.svg", type: :inline))
end
end
end
@ -226,27 +347,47 @@ def unsubscribe_url(user, notifications_type) do
def backup_is_ready_email(backup, admin_user_id \\ nil) do
%{user: user} = Pleroma.Repo.preload(backup, :user)
download_url = Pleroma.Web.PleromaAPI.BackupView.download_url(backup)
html_body =
if is_nil(admin_user_id) do
"""
<p>You requested a full backup of your Pleroma account. It's ready for download:</p>
<p><a href="#{download_url}">#{download_url}</a></p>
"""
else
admin = Pleroma.Repo.get(User, admin_user_id)
Gettext.with_locale_or_default user.language do
download_url = Pleroma.Web.PleromaAPI.BackupView.download_url(backup)
"""
<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
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)
new()
|> to(recipient(user))
|> from(sender())
|> subject("Your account archive is ready")
|> html_body(html_body)
Gettext.dpgettext(
"static_pages",
"account archive email body - admin requested",
"""
<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

View file

@ -1,11 +1,11 @@
# emoji-test.txt
# Date: 2020-09-12, 22:19:50 GMT
# © 2020 Unicode®, Inc.
# Date: 2021-08-26, 17:22:23 GMT
# © 2021 Unicode®, Inc.
# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries.
# For terms of use, see http://www.unicode.org/terms_of_use.html
#
# Emoji Keyboard/Display Test Data for UTS #51
# Version: 13.1
# Version: 14.0
#
# For documentation and usage, see http://www.unicode.org/reports/tr51
#
@ -43,6 +43,7 @@
1F602 ; fully-qualified # 😂 E0.6 face with tears of joy
1F642 ; fully-qualified # 🙂 E1.0 slightly smiling face
1F643 ; fully-qualified # 🙃 E1.0 upside-down face
1FAE0 ; fully-qualified # 🫠 E14.0 melting face
1F609 ; fully-qualified # 😉 E0.6 winking face
1F60A ; fully-qualified # 😊 E0.6 smiling face with smiling eyes
1F607 ; fully-qualified # 😇 E1.0 smiling face with halo
@ -68,10 +69,13 @@
1F911 ; fully-qualified # 🤑 E1.0 money-mouth face
# subgroup: face-hand
1F917 ; fully-qualified # 🤗 E1.0 hugging face
1F917 ; fully-qualified # 🤗 E1.0 smiling face with open hands
1F92D ; fully-qualified # 🤭 E5.0 face with hand over mouth
1FAE2 ; fully-qualified # 🫢 E14.0 face with open eyes and hand over mouth
1FAE3 ; fully-qualified # 🫣 E14.0 face with peeking eye
1F92B ; fully-qualified # 🤫 E5.0 shushing face
1F914 ; fully-qualified # 🤔 E1.0 thinking face
1FAE1 ; fully-qualified # 🫡 E14.0 saluting face
# subgroup: face-neutral-skeptical
1F910 ; fully-qualified # 🤐 E1.0 zipper-mouth face
@ -79,6 +83,7 @@
1F610 ; fully-qualified # 😐 E0.7 neutral face
1F611 ; fully-qualified # 😑 E1.0 expressionless face
1F636 ; fully-qualified # 😶 E1.0 face without mouth
1FAE5 ; fully-qualified # 🫥 E14.0 dotted line face
1F636 200D 1F32B FE0F ; fully-qualified # 😶‍🌫️ E13.1 face in clouds
1F636 200D 1F32B ; minimally-qualified # 😶‍🌫 E13.1 face in clouds
1F60F ; fully-qualified # 😏 E0.6 smirking face
@ -105,7 +110,7 @@
1F975 ; fully-qualified # 🥵 E11.0 hot face
1F976 ; fully-qualified # 🥶 E11.0 cold face
1F974 ; fully-qualified # 🥴 E11.0 woozy face
1F635 ; fully-qualified # 😵 E0.6 knocked-out face
1F635 ; fully-qualified # 😵 E0.6 face with crossed-out eyes
1F635 200D 1F4AB ; fully-qualified # 😵‍💫 E13.1 face with spiral eyes
1F92F ; fully-qualified # 🤯 E5.0 exploding head
@ -121,6 +126,7 @@
# subgroup: face-concerned
1F615 ; fully-qualified # 😕 E1.0 confused face
1FAE4 ; fully-qualified # 🫤 E14.0 face with diagonal mouth
1F61F ; fully-qualified # 😟 E1.0 worried face
1F641 ; fully-qualified # 🙁 E1.0 slightly frowning face
2639 FE0F ; fully-qualified # ☹️ E0.7 frowning face
@ -130,6 +136,7 @@
1F632 ; fully-qualified # 😲 E0.6 astonished face
1F633 ; fully-qualified # 😳 E0.6 flushed face
1F97A ; fully-qualified # 🥺 E11.0 pleading face
1F979 ; fully-qualified # 🥹 E14.0 face holding back tears
1F626 ; fully-qualified # 😦 E1.0 frowning face with open mouth
1F627 ; fully-qualified # 😧 E1.0 anguished face
1F628 ; fully-qualified # 😨 E0.6 fearful face
@ -232,8 +239,8 @@
1F4AD ; fully-qualified # 💭 E1.0 thought balloon
1F4A4 ; fully-qualified # 💤 E0.6 zzz
# Smileys & Emotion subtotal: 170
# Smileys & Emotion subtotal: 170 w/o modifiers
# Smileys & Emotion subtotal: 177
# Smileys & Emotion subtotal: 177 w/o modifiers
# group: People & Body
@ -269,6 +276,30 @@
1F596 1F3FD ; fully-qualified # 🖖🏽 E1.0 vulcan salute: medium skin tone
1F596 1F3FE ; fully-qualified # 🖖🏾 E1.0 vulcan salute: medium-dark skin tone
1F596 1F3FF ; fully-qualified # 🖖🏿 E1.0 vulcan salute: dark skin tone
1FAF1 ; fully-qualified # 🫱 E14.0 rightwards hand
1FAF1 1F3FB ; fully-qualified # 🫱🏻 E14.0 rightwards hand: light skin tone
1FAF1 1F3FC ; fully-qualified # 🫱🏼 E14.0 rightwards hand: medium-light skin tone
1FAF1 1F3FD ; fully-qualified # 🫱🏽 E14.0 rightwards hand: medium skin tone
1FAF1 1F3FE ; fully-qualified # 🫱🏾 E14.0 rightwards hand: medium-dark skin tone
1FAF1 1F3FF ; fully-qualified # 🫱🏿 E14.0 rightwards hand: dark skin tone
1FAF2 ; fully-qualified # 🫲 E14.0 leftwards hand
1FAF2 1F3FB ; fully-qualified # 🫲🏻 E14.0 leftwards hand: light skin tone
1FAF2 1F3FC ; fully-qualified # 🫲🏼 E14.0 leftwards hand: medium-light skin tone
1FAF2 1F3FD ; fully-qualified # 🫲🏽 E14.0 leftwards hand: medium skin tone
1FAF2 1F3FE ; fully-qualified # 🫲🏾 E14.0 leftwards hand: medium-dark skin tone
1FAF2 1F3FF ; fully-qualified # 🫲🏿 E14.0 leftwards hand: dark skin tone
1FAF3 ; fully-qualified # 🫳 E14.0 palm down hand
1FAF3 1F3FB ; fully-qualified # 🫳🏻 E14.0 palm down hand: light skin tone
1FAF3 1F3FC ; fully-qualified # 🫳🏼 E14.0 palm down hand: medium-light skin tone
1FAF3 1F3FD ; fully-qualified # 🫳🏽 E14.0 palm down hand: medium skin tone
1FAF3 1F3FE ; fully-qualified # 🫳🏾 E14.0 palm down hand: medium-dark skin tone
1FAF3 1F3FF ; fully-qualified # 🫳🏿 E14.0 palm down hand: dark skin tone
1FAF4 ; fully-qualified # 🫴 E14.0 palm up hand
1FAF4 1F3FB ; fully-qualified # 🫴🏻 E14.0 palm up hand: light skin tone
1FAF4 1F3FC ; fully-qualified # 🫴🏼 E14.0 palm up hand: medium-light skin tone
1FAF4 1F3FD ; fully-qualified # 🫴🏽 E14.0 palm up hand: medium skin tone
1FAF4 1F3FE ; fully-qualified # 🫴🏾 E14.0 palm up hand: medium-dark skin tone
1FAF4 1F3FF ; fully-qualified # 🫴🏿 E14.0 palm up hand: dark skin tone
# subgroup: hand-fingers-partial
1F44C ; fully-qualified # 👌 E0.6 OK hand
@ -302,6 +333,12 @@
1F91E 1F3FD ; fully-qualified # 🤞🏽 E3.0 crossed fingers: medium skin tone
1F91E 1F3FE ; fully-qualified # 🤞🏾 E3.0 crossed fingers: medium-dark skin tone
1F91E 1F3FF ; fully-qualified # 🤞🏿 E3.0 crossed fingers: dark skin tone
1FAF0 ; fully-qualified # 🫰 E14.0 hand with index finger and thumb crossed
1FAF0 1F3FB ; fully-qualified # 🫰🏻 E14.0 hand with index finger and thumb crossed: light skin tone
1FAF0 1F3FC ; fully-qualified # 🫰🏼 E14.0 hand with index finger and thumb crossed: medium-light skin tone
1FAF0 1F3FD ; fully-qualified # 🫰🏽 E14.0 hand with index finger and thumb crossed: medium skin tone
1FAF0 1F3FE ; fully-qualified # 🫰🏾 E14.0 hand with index finger and thumb crossed: medium-dark skin tone
1FAF0 1F3FF ; fully-qualified # 🫰🏿 E14.0 hand with index finger and thumb crossed: dark skin tone
1F91F ; fully-qualified # 🤟 E5.0 love-you gesture
1F91F 1F3FB ; fully-qualified # 🤟🏻 E5.0 love-you gesture: light skin tone
1F91F 1F3FC ; fully-qualified # 🤟🏼 E5.0 love-you gesture: medium-light skin tone
@ -359,6 +396,12 @@
261D 1F3FD ; fully-qualified # ☝🏽 E1.0 index pointing up: medium skin tone
261D 1F3FE ; fully-qualified # ☝🏾 E1.0 index pointing up: medium-dark skin tone
261D 1F3FF ; fully-qualified # ☝🏿 E1.0 index pointing up: dark skin tone
1FAF5 ; fully-qualified # 🫵 E14.0 index pointing at the viewer
1FAF5 1F3FB ; fully-qualified # 🫵🏻 E14.0 index pointing at the viewer: light skin tone
1FAF5 1F3FC ; fully-qualified # 🫵🏼 E14.0 index pointing at the viewer: medium-light skin tone
1FAF5 1F3FD ; fully-qualified # 🫵🏽 E14.0 index pointing at the viewer: medium skin tone
1FAF5 1F3FE ; fully-qualified # 🫵🏾 E14.0 index pointing at the viewer: medium-dark skin tone
1FAF5 1F3FF ; fully-qualified # 🫵🏿 E14.0 index pointing at the viewer: dark skin tone
# subgroup: hand-fingers-closed
1F44D ; fully-qualified # 👍 E0.6 thumbs up
@ -411,6 +454,12 @@
1F64C 1F3FD ; fully-qualified # 🙌🏽 E1.0 raising hands: medium skin tone
1F64C 1F3FE ; fully-qualified # 🙌🏾 E1.0 raising hands: medium-dark skin tone
1F64C 1F3FF ; fully-qualified # 🙌🏿 E1.0 raising hands: dark skin tone
1FAF6 ; fully-qualified # 🫶 E14.0 heart hands
1FAF6 1F3FB ; fully-qualified # 🫶🏻 E14.0 heart hands: light skin tone
1FAF6 1F3FC ; fully-qualified # 🫶🏼 E14.0 heart hands: medium-light skin tone
1FAF6 1F3FD ; fully-qualified # 🫶🏽 E14.0 heart hands: medium skin tone
1FAF6 1F3FE ; fully-qualified # 🫶🏾 E14.0 heart hands: medium-dark skin tone
1FAF6 1F3FF ; fully-qualified # 🫶🏿 E14.0 heart hands: dark skin tone
1F450 ; fully-qualified # 👐 E0.6 open hands
1F450 1F3FB ; fully-qualified # 👐🏻 E1.0 open hands: light skin tone
1F450 1F3FC ; fully-qualified # 👐🏼 E1.0 open hands: medium-light skin tone
@ -424,6 +473,31 @@
1F932 1F3FE ; fully-qualified # 🤲🏾 E5.0 palms up together: medium-dark skin tone
1F932 1F3FF ; fully-qualified # 🤲🏿 E5.0 palms up together: dark skin tone
1F91D ; fully-qualified # 🤝 E3.0 handshake
1F91D 1F3FB ; fully-qualified # 🤝🏻 E3.0 handshake: light skin tone
1F91D 1F3FC ; fully-qualified # 🤝🏼 E3.0 handshake: medium-light skin tone
1F91D 1F3FD ; fully-qualified # 🤝🏽 E3.0 handshake: medium skin tone
1F91D 1F3FE ; fully-qualified # 🤝🏾 E3.0 handshake: medium-dark skin tone
1F91D 1F3FF ; fully-qualified # 🤝🏿 E3.0 handshake: dark skin tone
1FAF1 1F3FB 200D 1FAF2 1F3FC ; fully-qualified # 🫱🏻‍🫲🏼 E14.0 handshake: light skin tone, medium-light skin tone
1FAF1 1F3FB 200D 1FAF2 1F3FD ; fully-qualified # 🫱🏻‍🫲🏽 E14.0 handshake: light skin tone, medium skin tone
1FAF1 1F3FB 200D 1FAF2 1F3FE ; fully-qualified # 🫱🏻‍🫲🏾 E14.0 handshake: light skin tone, medium-dark skin tone
1FAF1 1F3FB 200D 1FAF2 1F3FF ; fully-qualified # 🫱🏻‍🫲🏿 E14.0 handshake: light skin tone, dark skin tone
1FAF1 1F3FC 200D 1FAF2 1F3FB ; fully-qualified # 🫱🏼‍🫲🏻 E14.0 handshake: medium-light skin tone, light skin tone
1FAF1 1F3FC 200D 1FAF2 1F3FD ; fully-qualified # 🫱🏼‍🫲🏽 E14.0 handshake: medium-light skin tone, medium skin tone
1FAF1 1F3FC 200D 1FAF2 1F3FE ; fully-qualified # 🫱🏼‍🫲🏾 E14.0 handshake: medium-light skin tone, medium-dark skin tone
1FAF1 1F3FC 200D 1FAF2 1F3FF ; fully-qualified # 🫱🏼‍🫲🏿 E14.0 handshake: medium-light skin tone, dark skin tone
1FAF1 1F3FD 200D 1FAF2 1F3FB ; fully-qualified # 🫱🏽‍🫲🏻 E14.0 handshake: medium skin tone, light skin tone
1FAF1 1F3FD 200D 1FAF2 1F3FC ; fully-qualified # 🫱🏽‍🫲🏼 E14.0 handshake: medium skin tone, medium-light skin tone
1FAF1 1F3FD 200D 1FAF2 1F3FE ; fully-qualified # 🫱🏽‍🫲🏾 E14.0 handshake: medium skin tone, medium-dark skin tone
1FAF1 1F3FD 200D 1FAF2 1F3FF ; fully-qualified # 🫱🏽‍🫲🏿 E14.0 handshake: medium skin tone, dark skin tone
1FAF1 1F3FE 200D 1FAF2 1F3FB ; fully-qualified # 🫱🏾‍🫲🏻 E14.0 handshake: medium-dark skin tone, light skin tone
1FAF1 1F3FE 200D 1FAF2 1F3FC ; fully-qualified # 🫱🏾‍🫲🏼 E14.0 handshake: medium-dark skin tone, medium-light skin tone
1FAF1 1F3FE 200D 1FAF2 1F3FD ; fully-qualified # 🫱🏾‍🫲🏽 E14.0 handshake: medium-dark skin tone, medium skin tone
1FAF1 1F3FE 200D 1FAF2 1F3FF ; fully-qualified # 🫱🏾‍🫲🏿 E14.0 handshake: medium-dark skin tone, dark skin tone
1FAF1 1F3FF 200D 1FAF2 1F3FB ; fully-qualified # 🫱🏿‍🫲🏻 E14.0 handshake: dark skin tone, light skin tone
1FAF1 1F3FF 200D 1FAF2 1F3FC ; fully-qualified # 🫱🏿‍🫲🏼 E14.0 handshake: dark skin tone, medium-light skin tone
1FAF1 1F3FF 200D 1FAF2 1F3FD ; fully-qualified # 🫱🏿‍🫲🏽 E14.0 handshake: dark skin tone, medium skin tone
1FAF1 1F3FF 200D 1FAF2 1F3FE ; fully-qualified # 🫱🏿‍🫲🏾 E14.0 handshake: dark skin tone, medium-dark skin tone
1F64F ; fully-qualified # 🙏 E0.6 folded hands
1F64F 1F3FB ; fully-qualified # 🙏🏻 E1.0 folded hands: light skin tone
1F64F 1F3FC ; fully-qualified # 🙏🏼 E1.0 folded hands: medium-light skin tone
@ -501,6 +575,7 @@
1F441 ; unqualified # 👁 E0.7 eye
1F445 ; fully-qualified # 👅 E0.6 tongue
1F444 ; fully-qualified # 👄 E0.6 mouth
1FAE6 ; fully-qualified # 🫦 E14.0 biting lip
# subgroup: person
1F476 ; fully-qualified # 👶 E0.6 baby
@ -1472,6 +1547,12 @@
1F477 1F3FE 200D 2640 ; minimally-qualified # 👷🏾‍♀ E4.0 woman construction worker: medium-dark skin tone
1F477 1F3FF 200D 2640 FE0F ; fully-qualified # 👷🏿‍♀️ E4.0 woman construction worker: dark skin tone
1F477 1F3FF 200D 2640 ; minimally-qualified # 👷🏿‍♀ E4.0 woman construction worker: dark skin tone
1FAC5 ; fully-qualified # 🫅 E14.0 person with crown
1FAC5 1F3FB ; fully-qualified # 🫅🏻 E14.0 person with crown: light skin tone
1FAC5 1F3FC ; fully-qualified # 🫅🏼 E14.0 person with crown: medium-light skin tone
1FAC5 1F3FD ; fully-qualified # 🫅🏽 E14.0 person with crown: medium skin tone
1FAC5 1F3FE ; fully-qualified # 🫅🏾 E14.0 person with crown: medium-dark skin tone
1FAC5 1F3FF ; fully-qualified # 🫅🏿 E14.0 person with crown: dark skin tone
1F934 ; fully-qualified # 🤴 E3.0 prince
1F934 1F3FB ; fully-qualified # 🤴🏻 E3.0 prince: light skin tone
1F934 1F3FC ; fully-qualified # 🤴🏼 E3.0 prince: medium-light skin tone
@ -1592,6 +1673,18 @@
1F930 1F3FD ; fully-qualified # 🤰🏽 E3.0 pregnant woman: medium skin tone
1F930 1F3FE ; fully-qualified # 🤰🏾 E3.0 pregnant woman: medium-dark skin tone
1F930 1F3FF ; fully-qualified # 🤰🏿 E3.0 pregnant woman: dark skin tone
1FAC3 ; fully-qualified # 🫃 E14.0 pregnant man
1FAC3 1F3FB ; fully-qualified # 🫃🏻 E14.0 pregnant man: light skin tone
1FAC3 1F3FC ; fully-qualified # 🫃🏼 E14.0 pregnant man: medium-light skin tone
1FAC3 1F3FD ; fully-qualified # 🫃🏽 E14.0 pregnant man: medium skin tone
1FAC3 1F3FE ; fully-qualified # 🫃🏾 E14.0 pregnant man: medium-dark skin tone
1FAC3 1F3FF ; fully-qualified # 🫃🏿 E14.0 pregnant man: dark skin tone
1FAC4 ; fully-qualified # 🫄 E14.0 pregnant person
1FAC4 1F3FB ; fully-qualified # 🫄🏻 E14.0 pregnant person: light skin tone
1FAC4 1F3FC ; fully-qualified # 🫄🏼 E14.0 pregnant person: medium-light skin tone
1FAC4 1F3FD ; fully-qualified # 🫄🏽 E14.0 pregnant person: medium skin tone
1FAC4 1F3FE ; fully-qualified # 🫄🏾 E14.0 pregnant person: medium-dark skin tone
1FAC4 1F3FF ; fully-qualified # 🫄🏿 E14.0 pregnant person: dark skin tone
1F931 ; fully-qualified # 🤱 E5.0 breast-feeding
1F931 1F3FB ; fully-qualified # 🤱🏻 E5.0 breast-feeding: light skin tone
1F931 1F3FC ; fully-qualified # 🤱🏼 E5.0 breast-feeding: medium-light skin tone
@ -1862,6 +1955,7 @@
1F9DF 200D 2642 ; minimally-qualified # 🧟‍♂ E5.0 man zombie
1F9DF 200D 2640 FE0F ; fully-qualified # 🧟‍♀️ E5.0 woman zombie
1F9DF 200D 2640 ; minimally-qualified # 🧟‍♀ E5.0 woman zombie
1F9CC ; fully-qualified # 🧌 E14.0 troll
# subgroup: person-activity
1F486 ; fully-qualified # 💆 E0.6 person getting massage
@ -3168,8 +3262,8 @@
1FAC2 ; fully-qualified # 🫂 E13.0 people hugging
1F463 ; fully-qualified # 👣 E0.6 footprints
# People & Body subtotal: 2899
# People & Body subtotal: 494 w/o modifiers
# People & Body subtotal: 2986
# People & Body subtotal: 506 w/o modifiers
# group: Component
@ -3304,6 +3398,7 @@
1F988 ; fully-qualified # 🦈 E3.0 shark
1F419 ; fully-qualified # 🐙 E0.6 octopus
1F41A ; fully-qualified # 🐚 E0.6 spiral shell
1FAB8 ; fully-qualified # 🪸 E14.0 coral
# subgroup: animal-bug
1F40C ; fully-qualified # 🐌 E0.6 snail
@ -3329,6 +3424,7 @@
1F490 ; fully-qualified # 💐 E0.6 bouquet
1F338 ; fully-qualified # 🌸 E0.6 cherry blossom
1F4AE ; fully-qualified # 💮 E0.6 white flower
1FAB7 ; fully-qualified # 🪷 E14.0 lotus
1F3F5 FE0F ; fully-qualified # 🏵️ E0.7 rosette
1F3F5 ; unqualified # 🏵 E0.7 rosette
1F339 ; fully-qualified # 🌹 E0.6 rose
@ -3353,9 +3449,11 @@
1F341 ; fully-qualified # 🍁 E0.6 maple leaf
1F342 ; fully-qualified # 🍂 E0.6 fallen leaf
1F343 ; fully-qualified # 🍃 E0.6 leaf fluttering in wind
1FAB9 ; fully-qualified # 🪹 E14.0 empty nest
1FABA ; fully-qualified # 🪺 E14.0 nest with eggs
# Animals & Nature subtotal: 147
# Animals & Nature subtotal: 147 w/o modifiers
# Animals & Nature subtotal: 151
# Animals & Nature subtotal: 151 w/o modifiers
# group: Food & Drink
@ -3396,6 +3494,7 @@
1F9C5 ; fully-qualified # 🧅 E12.0 onion
1F344 ; fully-qualified # 🍄 E0.6 mushroom
1F95C ; fully-qualified # 🥜 E3.0 peanuts
1FAD8 ; fully-qualified # 🫘 E14.0 beans
1F330 ; fully-qualified # 🌰 E0.6 chestnut
# subgroup: food-prepared
@ -3491,6 +3590,7 @@
1F37B ; fully-qualified # 🍻 E0.6 clinking beer mugs
1F942 ; fully-qualified # 🥂 E3.0 clinking glasses
1F943 ; fully-qualified # 🥃 E3.0 tumbler glass
1FAD7 ; fully-qualified # 🫗 E14.0 pouring liquid
1F964 ; fully-qualified # 🥤 E5.0 cup with straw
1F9CB ; fully-qualified # 🧋 E13.0 bubble tea
1F9C3 ; fully-qualified # 🧃 E12.0 beverage box
@ -3504,10 +3604,11 @@
1F374 ; fully-qualified # 🍴 E0.6 fork and knife
1F944 ; fully-qualified # 🥄 E3.0 spoon
1F52A ; fully-qualified # 🔪 E0.6 kitchen knife
1FAD9 ; fully-qualified # 🫙 E14.0 jar
1F3FA ; fully-qualified # 🏺 E1.0 amphora
# Food & Drink subtotal: 131
# Food & Drink subtotal: 131 w/o modifiers
# Food & Drink subtotal: 134
# Food & Drink subtotal: 134 w/o modifiers
# group: Travel & Places
@ -3597,6 +3698,7 @@
2668 FE0F ; fully-qualified # ♨️ E0.6 hot springs
2668 ; unqualified # ♨ E0.6 hot springs
1F3A0 ; fully-qualified # 🎠 E0.6 carousel horse
1F6DD ; fully-qualified # 🛝 E14.0 playground slide
1F3A1 ; fully-qualified # 🎡 E0.6 ferris wheel
1F3A2 ; fully-qualified # 🎢 E0.6 roller coaster
1F488 ; fully-qualified # 💈 E0.6 barber pole
@ -3652,6 +3754,7 @@
1F6E2 FE0F ; fully-qualified # 🛢️ E0.7 oil drum
1F6E2 ; unqualified # 🛢 E0.7 oil drum
26FD ; fully-qualified # ⛽ E0.6 fuel pump
1F6DE ; fully-qualified # 🛞 E14.0 wheel
1F6A8 ; fully-qualified # 🚨 E0.6 police car light
1F6A5 ; fully-qualified # 🚥 E0.6 horizontal traffic light
1F6A6 ; fully-qualified # 🚦 E1.0 vertical traffic light
@ -3660,6 +3763,7 @@
# subgroup: transport-water
2693 ; fully-qualified # ⚓ E0.6 anchor
1F6DF ; fully-qualified # 🛟 E14.0 ring buoy
26F5 ; fully-qualified # ⛵ E0.6 sailboat
1F6F6 ; fully-qualified # 🛶 E3.0 canoe
1F6A4 ; fully-qualified # 🚤 E0.6 speedboat
@ -3797,8 +3901,8 @@
1F4A7 ; fully-qualified # 💧 E0.6 droplet
1F30A ; fully-qualified # 🌊 E0.6 water wave
# Travel & Places subtotal: 264
# Travel & Places subtotal: 264 w/o modifiers
# Travel & Places subtotal: 267
# Travel & Places subtotal: 267 w/o modifiers
# group: Activities
@ -3874,6 +3978,7 @@
1F52E ; fully-qualified # 🔮 E0.6 crystal ball
1FA84 ; fully-qualified # 🪄 E13.0 magic wand
1F9FF ; fully-qualified # 🧿 E11.0 nazar amulet
1FAAC ; fully-qualified # 🪬 E14.0 hamsa
1F3AE ; fully-qualified # 🎮 E0.6 video game
1F579 FE0F ; fully-qualified # 🕹️ E0.7 joystick
1F579 ; unqualified # 🕹 E0.7 joystick
@ -3882,6 +3987,7 @@
1F9E9 ; fully-qualified # 🧩 E11.0 puzzle piece
1F9F8 ; fully-qualified # 🧸 E11.0 teddy bear
1FA85 ; fully-qualified # 🪅 E13.0 piñata
1FAA9 ; fully-qualified # 🪩 E14.0 mirror ball
1FA86 ; fully-qualified # 🪆 E13.0 nesting dolls
2660 FE0F ; fully-qualified # ♠️ E0.6 spade suit
2660 ; unqualified # ♠ E0.6 spade suit
@ -3907,8 +4013,8 @@
1F9F6 ; fully-qualified # 🧶 E11.0 yarn
1FAA2 ; fully-qualified # 🪢 E13.0 knot
# Activities subtotal: 95
# Activities subtotal: 95 w/o modifiers
# Activities subtotal: 97
# Activities subtotal: 97 w/o modifiers
# group: Objects
@ -4009,6 +4115,7 @@
# subgroup: computer
1F50B ; fully-qualified # 🔋 E0.6 battery
1FAAB ; fully-qualified # 🪫 E14.0 low battery
1F50C ; fully-qualified # 🔌 E0.6 electric plug
1F4BB ; fully-qualified # 💻 E0.6 laptop
1F5A5 FE0F ; fully-qualified # 🖥️ E0.7 desktop computer
@ -4207,7 +4314,9 @@
1FA78 ; fully-qualified # 🩸 E12.0 drop of blood
1F48A ; fully-qualified # 💊 E0.6 pill
1FA79 ; fully-qualified # 🩹 E12.0 adhesive bandage
1FA7C ; fully-qualified # 🩼 E14.0 crutch
1FA7A ; fully-qualified # 🩺 E12.0 stethoscope
1FA7B ; fully-qualified # 🩻 E14.0 x-ray
# subgroup: household
1F6AA ; fully-qualified # 🚪 E0.6 door
@ -4232,6 +4341,7 @@
1F9FB ; fully-qualified # 🧻 E11.0 roll of paper
1FAA3 ; fully-qualified # 🪣 E13.0 bucket
1F9FC ; fully-qualified # 🧼 E11.0 soap
1FAE7 ; fully-qualified # 🫧 E14.0 bubbles
1FAA5 ; fully-qualified # 🪥 E13.0 toothbrush
1F9FD ; fully-qualified # 🧽 E11.0 sponge
1F9EF ; fully-qualified # 🧯 E11.0 fire extinguisher
@ -4246,9 +4356,10 @@
26B1 ; unqualified # ⚱ E1.0 funeral urn
1F5FF ; fully-qualified # 🗿 E0.6 moai
1FAA7 ; fully-qualified # 🪧 E13.0 placard
1FAAA ; fully-qualified # 🪪 E14.0 identification card
# Objects subtotal: 299
# Objects subtotal: 299 w/o modifiers
# Objects subtotal: 304
# Objects subtotal: 304 w/o modifiers
# group: Symbols
@ -4409,6 +4520,7 @@
2795 ; fully-qualified # E0.6 plus
2796 ; fully-qualified # E0.6 minus
2797 ; fully-qualified # ➗ E0.6 divide
1F7F0 ; fully-qualified # 🟰 E14.0 heavy equals sign
267E FE0F ; fully-qualified # ♾️ E11.0 infinity
267E ; unqualified # ♾ E11.0 infinity
@ -4581,8 +4693,8 @@
1F533 ; fully-qualified # 🔳 E0.6 white square button
1F532 ; fully-qualified # 🔲 E0.6 black square button
# Symbols subtotal: 301
# Symbols subtotal: 301 w/o modifiers
# Symbols subtotal: 302
# Symbols subtotal: 302 w/o modifiers
# group: Flags
@ -4871,7 +4983,7 @@
# Flags subtotal: 275 w/o modifiers
# Status Counts
# fully-qualified : 3512
# fully-qualified : 3624
# minimally-qualified : 817
# unqualified : 252
# component : 9

View file

@ -61,7 +61,6 @@ def get_or_create_by_names(names) when is_list(names) do
{:ok, Repo.all(from(ht in Hashtag, where: ht.name in ^names))}
end)
|> Repo.transaction() do
Pleroma.Elasticsearch.maybe_bulk_post(hashtags, :hashtags)
{:ok, hashtags}
else
{:error, _name, value, _changes_so_far} -> {:error, value}

View file

@ -341,6 +341,14 @@ def destroy_multiple(%{id: user_id} = _user, ids) do
|> Repo.delete_all()
end
def destroy_multiple_from_types(%{id: user_id}, types) do
from(n in Notification,
where: n.user_id == ^user_id,
where: n.type in ^types
)
|> Repo.delete_all()
end
def dismiss(%Pleroma.Activity{} = activity) do
Notification
|> where([n], n.activity_id == ^activity.id)

View file

@ -1,12 +1,17 @@
defmodule Pleroma.Search do
@type search_map :: %{
statuses: [map],
accounts: [map],
hashtags: [map]
}
alias Pleroma.Workers.SearchIndexingWorker
@doc """
Searches for stuff
"""
@callback search(map, map, keyword) :: search_map
def add_to_index(%Pleroma.Activity{id: activity_id}) do
SearchIndexingWorker.enqueue("add_to_index", %{"activity" => activity_id})
end
def remove_from_index(%Pleroma.Object{id: object_id}) do
SearchIndexingWorker.enqueue("remove_from_index", %{"object" => object_id})
end
def search(query, options) do
search_module = Pleroma.Config.get([Pleroma.Search, :module], Pleroma.Activity)
search_module.search(options[:for_user], query, options)
end
end

View file

@ -1,138 +0,0 @@
defmodule Pleroma.Search.Builtin do
@behaviour Pleroma.Search
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Activity
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.Endpoint
require Logger
@impl Pleroma.Search
def search(_conn, %{q: query} = params, options) do
version = Keyword.get(options, :version)
timeout = Keyword.get(Repo.config(), :timeout, 15_000)
query = String.trim(query)
default_values = %{"statuses" => [], "accounts" => [], "hashtags" => []}
default_values
|> Enum.map(fn {resource, default_value} ->
if params[:type] in [nil, resource] do
{resource, fn -> resource_search(version, resource, query, options) end}
else
{resource, fn -> default_value end}
end
end)
|> Task.async_stream(fn {resource, f} -> {resource, with_fallback(f)} end,
timeout: timeout,
on_timeout: :kill_task
)
|> Enum.reduce(default_values, fn
{:ok, {resource, result}}, acc ->
Map.put(acc, resource, result)
_error, acc ->
acc
end)
end
defp resource_search(_, "accounts", query, options) do
accounts = with_fallback(fn -> User.search(query, options) end)
AccountView.render("index.json",
users: accounts,
for: options[:for_user],
embed_relationships: options[:embed_relationships]
)
end
defp resource_search(_, "statuses", query, options) do
statuses = with_fallback(fn -> Activity.search(options[:for_user], query, options) end)
StatusView.render("index.json",
activities: statuses,
for: options[:for_user],
as: :activity
)
end
defp resource_search(:v2, "hashtags", query, options) do
tags_path = Endpoint.url() <> "/tag/"
query
|> prepare_tags(options)
|> Enum.map(fn tag ->
%{name: tag, url: tags_path <> tag}
end)
end
defp resource_search(:v1, "hashtags", query, options) do
prepare_tags(query, options)
end
defp prepare_tags(query, options) do
tags =
query
|> preprocess_uri_query()
|> String.split(~r/[^#\w]+/u, trim: true)
|> Enum.uniq_by(&String.downcase/1)
explicit_tags = Enum.filter(tags, fn tag -> String.starts_with?(tag, "#") end)
tags =
if Enum.any?(explicit_tags) do
explicit_tags
else
tags
end
tags = Enum.map(tags, fn tag -> String.trim_leading(tag, "#") end)
tags =
if Enum.empty?(explicit_tags) && !options[:skip_joined_tag] do
add_joined_tag(tags)
else
tags
end
Pleroma.Pagination.paginate(tags, options)
end
# If `query` is a URI, returns last component of its path, otherwise returns `query`
defp preprocess_uri_query(query) do
if query =~ ~r/https?:\/\// do
query
|> String.trim_trailing("/")
|> URI.parse()
|> Map.get(:path)
|> String.split("/")
|> Enum.at(-1)
else
query
end
end
defp add_joined_tag(tags) do
tags
|> Kernel.++([joined_tag(tags)])
|> Enum.uniq_by(&String.downcase/1)
end
defp joined_tag(tags) do
tags
|> Enum.map(fn tag -> String.capitalize(tag) end)
|> Enum.join()
end
defp with_fallback(f, fallback \\ []) do
try do
f.()
rescue
error ->
Logger.error("#{__MODULE__} search error: #{inspect(error)}")
fallback
end
end
end

View file

@ -2,7 +2,7 @@
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Activity.Search do
defmodule Pleroma.Search.DatabaseSearch do
alias Pleroma.Activity
alias Pleroma.Object.Fetcher
alias Pleroma.Pagination
@ -13,6 +13,8 @@ defmodule Pleroma.Activity.Search do
import Ecto.Query
@behaviour Pleroma.Search.SearchBackend
def search(user, search_query, options \\ []) do
index_type = if Pleroma.Config.get([:database, :rum_enabled]), do: :rum, else: :gin
limit = Enum.min([Keyword.get(options, :limit), 40])
@ -45,6 +47,12 @@ def search(user, search_query, options \\ []) do
end
end
@impl true
def add_to_index(_activity), do: nil
@impl true
def remove_from_index(_object), do: nil
def maybe_restrict_author(query, %User{} = author) do
Activity.Queries.by_author(query, author)
end
@ -57,7 +65,7 @@ def maybe_restrict_blocked(query, %User{} = user) do
def maybe_restrict_blocked(query, _), do: query
defp restrict_public(q) do
def restrict_public(q) do
from([a, o] in q,
where: fragment("?->>'type' = 'Create'", a.data),
where: ^Pleroma.Constants.as_public() in a.recipients
@ -124,7 +132,7 @@ defp query_with(q, :rum, search_query, :websearch) do
)
end
defp maybe_restrict_local(q, user) do
def maybe_restrict_local(q, user) do
limit = Pleroma.Config.get([:instance, :limit_to_local_content], :unauthenticated)
case {limit, user} do
@ -137,7 +145,7 @@ defp maybe_restrict_local(q, 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"]),

View file

@ -3,24 +3,22 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Search.Elasticsearch do
@behaviour Pleroma.Search
@behaviour Pleroma.Search.SearchBackend
alias Pleroma.Activity
alias Pleroma.Object.Fetcher
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Search.Elasticsearch.Parsers
alias Pleroma.Web.Endpoint
def es_query(:activity, query) do
def es_query(:activity, query, offset, limit) do
must = Parsers.Activity.parse(query)
if must == [] do
:skip
else
%{
size: 50,
size: limit,
from: offset,
terminate_after: 50,
timeout: "5s",
sort: [
@ -36,50 +34,6 @@ def es_query(:activity, query) do
end
end
def es_query(:user, query) do
must = Parsers.User.parse(query)
if must == [] do
:skip
else
%{
size: 50,
terminate_after: 50,
timeout: "5s",
sort: [
"_score"
],
query: %{
bool: %{
must: must
}
}
}
end
end
def es_query(:hashtag, query) do
must = Parsers.Hashtag.parse(query)
if must == [] do
:skip
else
%{
size: 50,
terminate_after: 50,
timeout: "5s",
sort: [
"_score"
],
query: %{
bool: %{
must: Parsers.Hashtag.parse(query)
}
}
}
end
end
defp maybe_fetch(:activity, search_query) do
with true <- Regex.match?(~r/https?:/, search_query),
{:ok, object} <- Fetcher.fetch_object_from_id(search_query),
@ -90,8 +44,10 @@ defp maybe_fetch(:activity, search_query) do
end
end
@impl Pleroma.Search
def search(%{assigns: %{user: user}} = _conn, %{q: query} = _params, _options) do
def search(user, query, options) do
limit = Enum.min([Keyword.get(options, :limit), 40])
offset = Keyword.get(options, :offset, 0)
parsed_query =
query
|> String.trim()
@ -104,30 +60,13 @@ def search(%{assigns: %{user: user}} = _conn, %{q: query} = _params, _options) d
activity_task =
Task.async(fn ->
q = es_query(:activity, parsed_query)
q = es_query(:activity, parsed_query, offset, limit)
Pleroma.Elasticsearch.search(:activities, q)
Pleroma.Search.Elasticsearch.Store.search(:activities, q)
|> Enum.filter(fn x -> Visibility.visible_for_user?(x, user) end)
end)
user_task =
Task.async(fn ->
q = es_query(:user, parsed_query)
Pleroma.Elasticsearch.search(:users, q)
|> Enum.filter(fn x -> Pleroma.User.visible_for(x, user) == :visible end)
end)
hashtag_task =
Task.async(fn ->
q = es_query(:hashtag, parsed_query)
Pleroma.Elasticsearch.search(:hashtags, q)
end)
activity_results = Task.await(activity_task)
user_results = Task.await(user_task)
hashtag_results = Task.await(hashtag_task)
direct_activity = Task.await(activity_fetch_task)
activity_results =
@ -137,25 +76,16 @@ def search(%{assigns: %{user: user}} = _conn, %{q: query} = _params, _options) d
[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

View file

@ -0,0 +1,4 @@
defmodule Pleroma.Search.Elasticsearch.Cluster do
@moduledoc false
use Elasticsearch.Cluster, otp_app: :pleroma
end

View file

@ -0,0 +1,61 @@
# Akkoma: A lightweight social networking server
# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
# SPDX-License-Identifier: AGPL-3.0-only
defimpl Elasticsearch.Document, for: Pleroma.Activity do
alias Pleroma.Object
require Pleroma.Constants
def id(obj), do: obj.id
def routing(_), do: false
def object_to_search_data(object) do
# Only index public or unlisted Notes
if not is_nil(object) and object.data["type"] == "Note" and
not is_nil(object.data["content"]) and
(Pleroma.Constants.as_public() in object.data["to"] or
Pleroma.Constants.as_public() in object.data["cc"]) and
String.length(object.data["content"]) > 1 do
data = object.data
content_str =
case data["content"] do
[nil | rest] -> to_string(rest)
str -> str
end
content =
with {:ok, scrubbed} <- FastSanitize.strip_tags(content_str),
trimmed <- String.trim(scrubbed) do
trimmed
end
if String.length(content) > 1 do
{:ok, published, _} = DateTime.from_iso8601(data["published"])
%{
_timestamp: published,
content: content,
instance: URI.parse(object.data["actor"]).host,
hashtags: Object.hashtags(object),
user: Pleroma.User.get_cached_by_ap_id(object.data["actor"]).nickname
}
else
%{}
end
else
%{}
end
end
def encode(activity) do
object = Pleroma.Object.normalize(activity)
object_to_search_data(object)
end
end
defimpl Elasticsearch.Document, for: Pleroma.Object do
def id(obj), do: obj.id
def routing(_), do: false
def encode(_), do: nil
end

View file

@ -1,34 +0,0 @@
# Akkoma: A lightweight social networking server
# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Search.Elasticsearch.Parsers.Hashtag do
defp to_es(term) when is_binary(term) do
%{
term: %{
hashtag: %{
value: String.downcase(term)
}
}
}
end
defp to_es({:quoted, term}), do: to_es(term)
defp to_es({:filter, ["hashtag", query]}) do
%{
term: %{
hashtag: %{
value: String.downcase(query)
}
}
}
end
defp to_es({:filter, _}), do: nil
def parse(q) do
Enum.map(q, &to_es/1)
|> Enum.filter(fn x -> x != nil end)
end
end

View file

@ -0,0 +1,52 @@
# Akkoma: A lightweight social networking server
# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Search.Elasticsearch.Store do
@behaviour Elasticsearch.Store
alias Pleroma.Search.Elasticsearch.Cluster
require Logger
alias Pleroma.Repo
@impl true
def stream(schema) do
Repo.stream(schema)
end
@impl true
def transaction(fun) do
{:ok, result} = Repo.transaction(fun, timeout: :infinity)
result
end
def search(_, _, _, :skip), do: []
def search(:raw, index, q) do
with {:ok, raw_results} <- Elasticsearch.post(Cluster, "/#{index}/_search", q) do
results =
raw_results
|> Map.get("hits", %{})
|> Map.get("hits", [])
{:ok, results}
else
{:error, e} ->
Logger.error(e)
{:error, e}
end
end
def search(:activities, q) do
with {:ok, results} <- search(:raw, "activities", q) do
results
|> Enum.map(fn result -> result["_id"] end)
|> Pleroma.Activity.all_by_ids_with_object()
|> Enum.sort(&(&1.inserted_at >= &2.inserted_at))
else
e ->
Logger.error(e)
[]
end
end
end

View file

@ -1,57 +0,0 @@
# Akkoma: A lightweight social networking server
# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Search.Elasticsearch.Parsers.User do
defp to_es(term) when is_binary(term) do
%{
bool: %{
minimum_should_match: 1,
should: [
%{
match: %{
bio: %{
query: term,
operator: "AND"
}
}
},
%{
term: %{
nickname: %{
value: term
}
}
},
%{
match: %{
display_name: %{
query: term,
operator: "AND"
}
}
}
]
}
}
end
defp to_es({:quoted, term}), do: to_es(term)
defp to_es({:filter, ["user", query]}) do
%{
term: %{
nickname: %{
value: query
}
}
}
end
defp to_es({:filter, _}), do: nil
def parse(q) do
Enum.map(q, &to_es/1)
|> Enum.filter(fn x -> x != nil end)
end
end

View file

@ -0,0 +1,169 @@
defmodule Pleroma.Search.Meilisearch do
require Logger
require Pleroma.Constants
alias Pleroma.Activity
import Pleroma.Search.DatabaseSearch
import Ecto.Query
@behaviour Pleroma.Search.SearchBackend
defp meili_headers do
private_key = Pleroma.Config.get([Pleroma.Search.Meilisearch, :private_key])
[{"Content-Type", "application/json"}] ++
if is_nil(private_key), do: [], else: [{"Authorization", "Bearer #{private_key}"}]
end
def meili_get(path) do
endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url])
result =
Pleroma.HTTP.get(
Path.join(endpoint, path),
meili_headers()
)
with {:ok, res} <- result do
{:ok, Jason.decode!(res.body)}
end
end
def meili_post(path, params) do
endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url])
result =
Pleroma.HTTP.post(
Path.join(endpoint, path),
Jason.encode!(params),
meili_headers()
)
with {:ok, res} <- result do
{:ok, Jason.decode!(res.body)}
end
end
def meili_put(path, params) do
endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url])
result =
Pleroma.HTTP.request(
:put,
Path.join(endpoint, path),
Jason.encode!(params),
meili_headers(),
[]
)
with {:ok, res} <- result do
{:ok, Jason.decode!(res.body)}
end
end
def meili_delete!(path) do
endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url])
{:ok, _} =
Pleroma.HTTP.request(
:delete,
Path.join(endpoint, path),
"",
meili_headers(),
[]
)
end
def search(user, query, options \\ []) do
limit = Enum.min([Keyword.get(options, :limit), 40])
offset = Keyword.get(options, :offset, 0)
author = Keyword.get(options, :author)
res =
meili_post(
"/indexes/objects/search",
%{q: query, offset: offset, limit: limit}
)
with {:ok, result} <- res do
hits = result["hits"] |> Enum.map(& &1["ap"])
try do
hits
|> Activity.create_by_object_ap_id()
|> Activity.with_preloaded_object()
|> Activity.with_preloaded_object()
|> Activity.restrict_deactivated_users()
|> maybe_restrict_local(user)
|> maybe_restrict_author(author)
|> maybe_restrict_blocked(user)
|> maybe_fetch(user, query)
|> order_by([object: obj], desc: obj.data["published"])
|> Pleroma.Repo.all()
rescue
_ -> maybe_fetch([], user, query)
end
end
end
def object_to_search_data(object) do
# Only index public or unlisted Notes
if not is_nil(object) and object.data["type"] == "Note" and
not is_nil(object.data["content"]) and
(Pleroma.Constants.as_public() in object.data["to"] or
Pleroma.Constants.as_public() in object.data["cc"]) and
String.length(object.data["content"]) > 1 do
data = object.data
content_str =
case data["content"] do
[nil | rest] -> to_string(rest)
str -> str
end
content =
with {:ok, scrubbed} <- FastSanitize.strip_tags(content_str),
trimmed <- String.trim(scrubbed) do
trimmed
end
if String.length(content) > 1 do
{:ok, published, _} = DateTime.from_iso8601(data["published"])
%{
id: object.id,
content: content,
ap: data["id"],
published: published |> DateTime.to_unix()
}
end
end
end
@impl true
def add_to_index(activity) do
maybe_search_data = object_to_search_data(activity.object)
if activity.data["type"] == "Create" and maybe_search_data do
result =
meili_put(
"/indexes/objects/documents",
[maybe_search_data]
)
with {:ok, res} <- result,
true <- Map.has_key?(res, "uid") do
# Do nothing
else
_ ->
Logger.error("Failed to add activity #{activity.id} to index: #{inspect(result)}")
end
end
end
@impl true
def remove_from_index(object) do
meili_delete!("/indexes/objects/documents/#{object.id}")
end
end

View file

@ -0,0 +1,17 @@
defmodule Pleroma.Search.SearchBackend do
@doc """
Add the object associated with the activity to the search index.
The whole activity is passed, to allow filtering on things such as scope.
"""
@callback add_to_index(activity :: Pleroma.Activity.t()) :: nil
@doc """
Remove the object from the index.
Just the object, as opposed to the whole activity, is passed, since the object
is what contains the actual content and there is no need for fitlering when removing
from index.
"""
@callback remove_from_index(object :: Pleroma.Object.t()) :: nil
end

View file

@ -12,8 +12,7 @@ defmodule Pleroma.Telemetry.Logger do
[:pleroma, :connection_pool, :reclaim, :stop],
[:pleroma, :connection_pool, :provision_failure],
[:pleroma, :connection_pool, :client, :dead],
[:pleroma, :connection_pool, :client, :add],
[:pleroma, :repo, :query]
[:pleroma, :connection_pool, :client, :add]
]
def attach do
:telemetry.attach_many(
@ -93,64 +92,4 @@ def handle_event(
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

View file

@ -151,6 +151,7 @@ defmodule Pleroma.User do
field(:pinned_objects, :map, default: %{})
field(:is_suggested, :boolean, default: false)
field(:last_status_at, :naive_datetime)
field(:language, :string)
embeds_one(
:notification_settings,
@ -734,7 +735,8 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do
:password_confirmation,
:emoji,
:accepts_chat_messages,
:registration_reason
:registration_reason,
:language
])
|> validate_required([:name, :nickname, :password, :password_confirmation])
|> validate_confirmation(:password)
@ -1089,11 +1091,24 @@ def update_and_set_cache(struct, params) do
|> update_and_set_cache()
end
def update_and_set_cache(changeset) do
def update_and_set_cache(%{data: %Pleroma.User{} = user} = changeset) do
was_superuser_before_update = User.superuser?(user)
with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
Pleroma.Elasticsearch.maybe_put_into_elasticsearch(user)
set_cache(user)
end
|> maybe_remove_report_notifications(was_superuser_before_update)
end
defp maybe_remove_report_notifications({:ok, %Pleroma.User{} = user} = result, true) do
if not User.superuser?(user),
do: user |> Notification.destroy_multiple_from_types(["pleroma:report"])
result
end
defp maybe_remove_report_notifications(result, _) do
result
end
def get_user_friends_ap_ids(user) do

View file

@ -140,6 +140,9 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when
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 ->

View file

@ -24,7 +24,7 @@ defp score_displayname("federationbot"), do: 1.0
defp score_displayname("fedibot"), do: 1.0
defp score_displayname(_), do: 0.0
defp determine_if_followbot(%User{nickname: nickname, name: displayname}) do
defp determine_if_followbot(%User{nickname: nickname, name: displayname, actor_type: actor_type}) do
# nickname will be a binary string except when following a relay
nick_score =
if is_binary(nickname) do
@ -45,19 +45,32 @@ defp determine_if_followbot(%User{nickname: nickname, name: displayname}) 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}"}

View file

@ -12,6 +12,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
defp accept_host?(host), do: host in Config.get([:mrf_steal_emoji, :hosts], [])
defp shortcode_matches?(shortcode, pattern) when is_binary(pattern) do
shortcode == pattern
end
defp shortcode_matches?(shortcode, pattern) do
String.match?(shortcode, pattern)
end
defp steal_emoji({shortcode, url}, emoji_dir_path) do
url = Pleroma.Web.MediaProxy.url(url)
@ -72,7 +80,7 @@ def filter(%{"object" => %{"emoji" => foreign_emojis, "actor" => actor}} = messa
reject_emoji? =
[:mrf_steal_emoji, :rejected_shortcodes]
|> Config.get([])
|> Enum.find(false, fn regex -> String.match?(shortcode, regex) end)
|> Enum.find(false, fn pattern -> shortcode_matches?(shortcode, pattern) end)
!reject_emoji?
end)
@ -122,8 +130,12 @@ def config_description do
%{
key: :rejected_shortcodes,
type: {:list, :string},
description: "Regex-list of shortcodes to reject",
suggestions: [""]
description: """
A list of patterns or matches to reject shortcodes with.
Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
""",
suggestions: ["foo", ~r/foo/]
},
%{
key: :size_limit,

View file

@ -108,6 +108,7 @@ defp fix(data) do
|> fix_replies()
|> fix_source()
|> fix_misskey_content()
|> Transmogrifier.fix_attachments()
|> Transmogrifier.fix_emoji()
|> Transmogrifier.fix_content_map()
end

View file

@ -28,7 +28,6 @@ def common_pipeline(object, meta) do
case Repo.transaction(fn -> do_common_pipeline(object, meta) end, Utils.query_timeout()) do
{:ok, {:ok, activity, meta}} ->
side_effects().handle_after_transaction(meta)
side_effects().handle_after_transaction(activity)
{:ok, activity, meta}
{:ok, value} ->

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.SideEffects do
@ -193,6 +193,7 @@ def handle(%{data: %{"type" => "Like"}} = object, meta) do
# - Increase replies count
# - Set up ActivityExpiration
# - Set up notifications
# - Index incoming posts for search (if needed)
@impl true
def handle(%{data: %{"type" => "Create"}} = activity, meta) do
with {:ok, object, meta} <- handle_object_creation(meta[:object_data], activity, meta),
@ -222,6 +223,8 @@ def handle(%{data: %{"type" => "Create"}} = activity, meta) do
Task.start(fn -> Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) end)
end)
Pleroma.Search.add_to_index(Map.put(activity, :object, object))
meta =
meta
|> add_notifications(notifications)
@ -269,6 +272,7 @@ def handle(%{data: %{"type" => "Undo", "object" => undone_object}} = object, met
def handle(%{data: %{"type" => "EmojiReact"}} = object, meta) do
reacted_object = Object.get_by_ap_id(object.data["object"])
Utils.add_emoji_reaction_to_object(object, reacted_object)
Notification.create_notifications(object)
{:ok, object, meta}
@ -281,6 +285,7 @@ def handle(%{data: %{"type" => "EmojiReact"}} = object, meta) do
# - Reduce the user note count
# - Reduce the reply count
# - Stream out the activity
# - Removes posts from search index (if needed)
@impl true
def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, meta) do
deleted_object =
@ -320,6 +325,12 @@ def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = 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}
@ -537,24 +548,6 @@ defp add_notifications(meta, notifications) do
end
@impl true
def handle_after_transaction(%Pleroma.Activity{data: %{"type" => "Create"}} = activity) do
Pleroma.Elasticsearch.put_by_id(:activity, activity.id)
end
def handle_after_transaction(%Pleroma.Activity{
data: %{"type" => "Delete", "deleted_activity_id" => id}
}) do
Pleroma.Elasticsearch.delete_by_id(:activity, id)
end
def handle_after_transaction(%Pleroma.Activity{}) do
:ok
end
def handle_after_transaction(%Pleroma.Object{}) do
:ok
end
def handle_after_transaction(meta) do
meta
|> send_notifications()

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.SideEffects.Handling do

View file

@ -507,6 +507,11 @@ defp create_request 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: %{

View file

@ -396,13 +396,7 @@ def listen(user, data) do
def post(user, %{status: _} = data) do
with {:ok, draft} <- ActivityDraft.create(user, data) do
activity = ActivityPub.create(draft.changes, draft.preview?)
unless draft.preview? do
Pleroma.Elasticsearch.maybe_put_into_elasticsearch(activity)
end
activity
ActivityPub.create(draft.changes, draft.preview?)
end
end

View file

@ -9,6 +9,7 @@ defmodule Pleroma.Web.Feed.FeedView do
alias Pleroma.Formatter
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.Gettext
alias Pleroma.Web.MediaProxy
require Pleroma.Constants

View file

@ -25,4 +25,196 @@ defmodule Pleroma.Web.Gettext do
See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
"""
use Gettext, otp_app: :pleroma
def language_tag do
# Naive implementation: HTML lang attribute uses BCP 47, which
# uses - as a separator.
# https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang
Gettext.get_locale()
|> String.replace("_", "-", global: true)
end
def normalize_locale(locale) do
if is_binary(locale) do
String.replace(locale, "-", "_", global: true)
else
nil
end
end
def supports_locale?(locale) do
Pleroma.Web.Gettext
|> Gettext.known_locales()
|> Enum.member?(locale)
end
def variant?(locale), do: String.contains?(locale, "_")
def language_for_variant(locale) do
Enum.at(String.split(locale, "_"), 0)
end
def ensure_fallbacks(locales) do
locales
|> Enum.flat_map(fn locale ->
others =
other_supported_variants_of_locale(locale)
|> Enum.filter(fn l -> not Enum.member?(locales, l) end)
[locale] ++ others
end)
end
def other_supported_variants_of_locale(locale) do
cond do
supports_locale?(locale) ->
[]
variant?(locale) ->
lang = language_for_variant(locale)
if supports_locale?(lang), do: [lang], else: []
true ->
Gettext.known_locales(Pleroma.Web.Gettext)
|> Enum.filter(fn l -> String.starts_with?(l, locale <> "_") end)
end
end
def get_locales do
Process.get({Pleroma.Web.Gettext, :locales}, [])
end
def is_locale_list(locales) do
Enum.all?(locales, &is_binary/1)
end
def put_locales(locales) do
if is_locale_list(locales) do
Process.put({Pleroma.Web.Gettext, :locales}, Enum.uniq(locales))
Gettext.put_locale(Enum.at(locales, 0, Gettext.get_locale()))
:ok
else
{:error, :not_locale_list}
end
end
def locale_or_default(locale) do
if supports_locale?(locale) do
locale
else
Gettext.get_locale()
end
end
def with_locales_func(locales, fun) do
prev_locales = Process.get({Pleroma.Web.Gettext, :locales})
put_locales(locales)
try do
fun.()
after
if prev_locales do
put_locales(prev_locales)
else
Process.delete({Pleroma.Web.Gettext, :locales})
Process.delete(Gettext)
end
end
end
defmacro with_locales(locales, do: fun) do
quote do
Pleroma.Web.Gettext.with_locales_func(unquote(locales), fn ->
unquote(fun)
end)
end
end
def to_locale_list(locale) when is_binary(locale) do
locale
|> String.split(",")
|> Enum.filter(&supports_locale?/1)
end
def to_locale_list(_), do: []
defmacro with_locale_or_default(locale, do: fun) do
quote do
Pleroma.Web.Gettext.with_locales_func(
Pleroma.Web.Gettext.to_locale_list(unquote(locale))
|> Enum.concat(Pleroma.Web.Gettext.get_locales()),
fn ->
unquote(fun)
end
)
end
end
defp next_locale(locale, list) do
index = Enum.find_index(list, fn item -> item == locale end)
if not is_nil(index) do
Enum.at(list, index + 1)
else
nil
end
end
# We do not yet have a proper English translation. The "English"
# version is currently but the fallback msgid. However, this
# will not work if the user puts English as the first language,
# and at the same time specifies other languages, as gettext will
# think the English translation is missing, and call
# handle_missing_translation functions. This may result in
# text in other languages being shown even if English is preferred
# by the user.
#
# To prevent this, we do not allow fallbacking when the current
# locale missing a translation is English.
defp should_fallback?(locale) do
locale != "en"
end
def handle_missing_translation(locale, domain, msgctxt, msgid, bindings) do
next = next_locale(locale, get_locales())
if is_nil(next) or not should_fallback?(locale) do
super(locale, domain, msgctxt, msgid, bindings)
else
{:ok,
Gettext.with_locale(next, fn ->
Gettext.dpgettext(Pleroma.Web.Gettext, domain, msgctxt, msgid, bindings)
end)}
end
end
def handle_missing_plural_translation(
locale,
domain,
msgctxt,
msgid,
msgid_plural,
n,
bindings
) do
next = next_locale(locale, get_locales())
if is_nil(next) or not should_fallback?(locale) do
super(locale, domain, msgctxt, msgid, msgid_plural, n, bindings)
else
{:ok,
Gettext.with_locale(next, fn ->
Gettext.dpngettext(
Pleroma.Web.Gettext,
domain,
msgctxt,
msgid,
msgid_plural,
n,
bindings
)
end)}
end
end
end

View file

@ -217,6 +217,7 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p
|> 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:
#

View file

@ -1,13 +1,16 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.SearchController do
use Pleroma.Web, :controller
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ControllerHelper
alias Pleroma.Web.Endpoint
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.Plugs.RateLimiter
@ -41,13 +44,34 @@ def account_search(%{assigns: %{user: user}} = conn, %{q: query} = params) do
def search2(conn, params), do: do_search(:v2, conn, params)
def search(conn, params), do: do_search(:v1, conn, params)
defp do_search(version, %{assigns: %{user: user}} = conn, params) do
options =
search_options(params, user)
|> Keyword.put(:version, version)
defp do_search(version, %{assigns: %{user: user}} = conn, %{q: query} = params) do
query = String.trim(query)
options = search_options(params, user)
timeout = Keyword.get(Repo.config(), :timeout, 15_000)
default_values = %{"statuses" => [], "accounts" => [], "hashtags" => []}
search_provider = Pleroma.Config.get([:search, :provider])
json(conn, search_provider.search(conn, params, options))
result =
default_values
|> Enum.map(fn {resource, default_value} ->
if params[:type] in [nil, resource] do
{resource, fn -> resource_search(version, resource, query, options) end}
else
{resource, fn -> default_value end}
end
end)
|> Task.async_stream(fn {resource, f} -> {resource, with_fallback(f)} end,
timeout: timeout,
on_timeout: :kill_task
)
|> Enum.reduce(default_values, fn
{:ok, {resource, result}}, acc ->
Map.put(acc, resource, result)
_error, acc ->
acc
end)
json(conn, result)
end
defp search_options(params, user) do
@ -64,6 +88,104 @@ 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)

View file

@ -384,11 +384,13 @@ def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
def context(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Activity{} = activity <- Activity.get_by_id(id) do
activities =
ActivityPub.fetch_activities_for_context(activity.data["context"], %{
activity.data["context"]
|> ActivityPub.fetch_activities_for_context(%{
blocking_user: user,
user: user,
exclude_id: activity.id
})
|> Enum.filter(fn activity -> Visibility.visible_for_user?(activity, user) end)
render(conn, "context.json", activity: activity, activities: activities, user: user)
end

View file

@ -6,6 +6,7 @@ defmodule Pleroma.Web.OAuth.MFAView do
use Pleroma.Web, :view
import Phoenix.HTML.Form
alias Pleroma.MFA
alias Pleroma.Web.Gettext
def render("mfa_response.json", %{token: token, user: user}) do
%{

View file

@ -5,6 +5,8 @@
defmodule Pleroma.Web.OAuth.OAuthView do
use Pleroma.Web, :view
import Phoenix.HTML.Form
import Phoenix.HTML
alias Pleroma.Web.Gettext
alias Pleroma.Web.OAuth.Token.Utils

View file

@ -6,18 +6,56 @@
defmodule Pleroma.Web.Plugs.SetLocalePlug do
import Plug.Conn, only: [get_req_header: 2, assign: 3]
def frontend_language_cookie_name, do: "userLanguage"
def init(_), do: nil
def call(conn, _) do
locale = get_locale_from_header(conn) || Gettext.get_locale()
Gettext.put_locale(locale)
assign(conn, :locale, locale)
locales = get_locales_from_header(conn)
first_locale = Enum.at(locales, 0, Gettext.get_locale())
Pleroma.Web.Gettext.put_locales(locales)
conn
|> assign(:locale, first_locale)
|> assign(:locales, locales)
end
defp get_locale_from_header(conn) do
defp get_locales_from_header(conn) do
conn
|> extract_accept_language()
|> Enum.find(&supported_locale?/1)
|> extract_preferred_language()
|> normalize_language_codes()
|> all_supported()
|> Enum.uniq()
end
defp all_supported(locales) do
locales
|> Pleroma.Web.Gettext.ensure_fallbacks()
|> Enum.filter(&supported_locale?/1)
end
defp normalize_language_codes(codes) do
codes
|> Enum.map(fn code -> Pleroma.Web.Gettext.normalize_locale(code) end)
end
defp extract_preferred_language(conn) do
extract_frontend_language(conn) ++ extract_accept_language(conn)
end
defp extract_frontend_language(conn) do
%{req_cookies: cookies} =
conn
|> Plug.Conn.fetch_cookies()
case cookies[frontend_language_cookie_name()] do
nil ->
[]
fe_lang ->
String.split(fe_lang, ",")
end
end
defp extract_accept_language(conn) do
@ -29,7 +67,6 @@ defp extract_accept_language(conn) do
|> Enum.sort(&(&1.quality > &2.quality))
|> Enum.map(& &1.tag)
|> Enum.reject(&is_nil/1)
|> ensure_language_fallbacks()
_ ->
[]
@ -37,9 +74,7 @@ defp extract_accept_language(conn) do
end
defp supported_locale?(locale) do
Pleroma.Web.Gettext
|> Gettext.known_locales()
|> Enum.member?(locale)
Pleroma.Web.Gettext.supports_locale?(locale)
end
defp parse_language_option(string) do
@ -53,11 +88,4 @@ 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

View file

@ -160,7 +160,7 @@
<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]-->
@ -382,7 +382,7 @@
<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>
@ -535,16 +535,16 @@
style="color:<%= @styling.text_color %>;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<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)]><!-->

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom"
<feed xml:lang="<%= Gettext.language_tag() %>" xmlns="http://www.w3.org/2005/Atom"
xmlns:thr="http://purl.org/syndication/thread/1.0"
xmlns:georss="http://www.georss.org/georss"
xmlns:activity="http://activitystrea.ms/spec/1.0/"
@ -12,7 +12,7 @@
<id><%= '#{Routes.tag_feed_url(@conn, :feed, @tag)}.rss' %></id>
<title>#<%= @tag %></title>
<subtitle>These are public toots tagged with #<%= @tag %>. You can interact with them if you have an account anywhere in the fediverse.</subtitle>
<subtitle><%= Gettext.dpgettext("static_pages", "tag feed description", "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse.", tag: @tag) %></subtitle>
<logo><%= feed_logo() %></logo>
<updated><%= most_recent_update(@activities) %></updated>
<link rel="self" href="<%= '#{Routes.tag_feed_url(@conn, :feed, @tag)}.atom' %>" type="application/atom+xml"/>

View file

@ -4,7 +4,7 @@
<title>#<%= @tag %></title>
<description>These are public toots tagged with #<%= @tag %>. You can interact with them if you have an account anywhere in the fediverse.</description>
<description><%= Gettext.dpgettext("static_pages", "tag feed description", "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse.", tag: @tag) %></description>
<link><%= '#{Routes.tag_feed_url(@conn, :feed, @tag)}.rss' %></link>
<webfeeds:logo><%= feed_logo() %></webfeeds:logo>
<webfeeds:accentColor>2b90d9</webfeeds:accentColor>

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="<%= Pleroma.Web.Gettext.language_tag() %>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui">

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="<%= Pleroma.Web.Gettext.language_tag() %>">
<head>
<meta charset="utf-8">
<title><%= @email.subject %></title>

View file

@ -1 +1 @@
<h1>UNSUBSCRIBE FAILURE</h1>
<h1><%= Gettext.dpgettext("static_pages", "mailer unsubscribe failed message", "UNSUBSCRIBE FAILURE") %></h1>

View file

@ -1 +1 @@
<h1>UNSUBSCRIBE SUCCESSFUL</h1>
<h1><%= Gettext.dpgettext("static_pages", "mailer unsubscribe successful message", "UNSUBSCRIBE SUCCESSFUL") %></h1>

View file

@ -5,11 +5,11 @@
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
<% end %>
<h2>Two-factor recovery</h2>
<h2><%= Gettext.dpgettext("static_pages", "mfa recover page title", "Two-factor recovery") %></h2>
<%= form_for @conn, Routes.mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %>
<div class="input">
<%= label f, :code, "Recovery code" %>
<%= label f, :code, Gettext.dpgettext("static_pages", "mfa recover recovery code prompt", "Recovery code") %>
<%= text_input f, :code, [autocomplete: false, autocorrect: "off", autocapitalize: "off", autofocus: true, spellcheck: false] %>
<%= hidden_input f, :mfa_token, value: @mfa_token %>
<%= hidden_input f, :state, value: @state %>
@ -17,8 +17,8 @@
<%= hidden_input f, :challenge_type, value: "recovery" %>
</div>
<%= submit "Verify" %>
<%= submit Gettext.dpgettext("static_pages", "mfa recover verify recovery code button", "Verify") %>
<% end %>
<a href="<%= Routes.mfa_path(@conn, :show, %{challenge_type: "totp", mfa_token: @mfa_token, state: @state, redirect_uri: @redirect_uri}) %>">
Enter a two-factor code
<%= Gettext.dpgettext("static_pages", "mfa recover use 2fa code link", "Enter a two-factor code") %>
</a>

View file

@ -5,20 +5,20 @@
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
<% end %>
<h2>Two-factor authentication</h2>
<h2><%= Gettext.dpgettext("static_pages", "mfa auth page title", "Two-factor authentication") %></h2>
<%= form_for @conn, Routes.mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %>
<div class="input">
<%= label f, :code, "Authentication code" %>
<%= text_input f, :code, [autocomplete: false, autocorrect: "off", autocapitalize: "off", autofocus: true, pattern: "[0-9]*", spellcheck: false] %>
<%= label f, :code, Gettext.dpgettext("static_pages", "mfa auth code prompt", "Authentication code") %>
<%= text_input f, :code, [autocomplete: "one-time-code", autocorrect: "off", autocapitalize: "off", autofocus: true, pattern: "[0-9]*", spellcheck: false] %>
<%= hidden_input f, :mfa_token, value: @mfa_token %>
<%= hidden_input f, :state, value: @state %>
<%= hidden_input f, :redirect_uri, value: @redirect_uri %>
<%= hidden_input f, :challenge_type, value: "totp" %>
</div>
<%= submit "Verify" %>
<%= submit Gettext.dpgettext("static_pages", "mfa auth verify code button", "Verify") %>
<% end %>
<a href="<%= Routes.mfa_path(@conn, :show, %{challenge_type: "recovery", mfa_token: @mfa_token, state: @state, redirect_uri: @redirect_uri}) %>">
Enter a two-factor recovery code
<%= Gettext.dpgettext("static_pages", "mfa auth page use recovery code link", "Enter a two-factor recovery code") %>
</a>

View file

@ -1,5 +1,5 @@
<div class="scopes-input">
<%= label @form, :scope, "The following permissions will be granted" %>
<%= label @form, :scope, Gettext.dpgettext("static_pages", "oauth scopes message", "The following permissions will be granted") %>
<div class="scopes">
<%= for scope <- @available_scopes do %>
<%# Note: using hidden input with `unchecked_value` in order to distinguish user's empty selection from `scope` param being omitted %>

View file

@ -1,4 +1,4 @@
<h2>Sign in with external provider</h2>
<h2><%= Gettext.dpgettext("static_pages", "oauth external provider page title", "Sign in with external provider") %></h2>
<%= form_for @conn, Routes.o_auth_path(@conn, :prepare_request), [as: "authorization", method: "get"], fn f -> %>
<div style="display: none">
@ -10,6 +10,6 @@
<%= hidden_input f, :state, value: @state %>
<%= for strategy <- Pleroma.Config.oauth_consumer_strategies() do %>
<%= submit "Sign in with #{String.capitalize(strategy)}", name: "provider", value: strategy %>
<%= submit Gettext.dpgettext("static_pages", "oauth external provider sign in button", "Sign in with %{strategy}", strategy: String.capitalize(strategy)), name: "provider", value: strategy %>
<% end %>
<% end %>

View file

@ -1,2 +1,2 @@
<h1>Successfully authorized</h1>
<h2>Token code is <br><%= @auth.token %></h2>
<h1><%= Gettext.dpgettext("static_pages", "oauth authorized page title", "Successfully authorized") %></h1>
<h2><%= raw Gettext.dpgettext("static_pages", "oauth token code message", "Token code is <br>%{token}", token: safe_to_string(html_escape(@auth.token))) %></h2>

View file

@ -1,2 +1,2 @@
<h1>Authorization exists</h1>
<h2>Access token is <br><%= @token.token %></h2>
<h1><%= Gettext.dpgettext("static_pages", "oauth authorization exists page title", "Authorization exists") %></h1>
<h2><%= raw Gettext.dpgettext("static_pages", "oauth token code message", "Token code is <br>%{token}", token: safe_to_string(html_escape(@token.token))) %></h2>

View file

@ -5,34 +5,34 @@
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
<% end %>
<h2>Registration Details</h2>
<h2><%= Gettext.dpgettext("static_pages", "oauth register page title", "Registration Details") %></h2>
<p>If you'd like to register a new account, please provide the details below.</p>
<p><%= Gettext.dpgettext("static_pages", "oauth register page fill form prompt", "If you'd like to register a new account, please provide the details below.") %></p>
<%= form_for @conn, Routes.o_auth_path(@conn, :register), [as: "authorization"], fn f -> %>
<div class="input">
<%= label f, :nickname, "Nickname" %>
<%= text_input f, :nickname, value: @nickname %>
<%= label f, :nickname, Gettext.dpgettext("static_pages", "oauth register page nickname prompt", "Nickname") %>
<%= text_input f, :nickname, value: @nickname, autocomplete: "username" %>
</div>
<div class="input">
<%= label f, :email, "Email" %>
<%= text_input f, :email, value: @email %>
<%= label f, :email, Gettext.dpgettext("static_pages", "oauth register page email prompt", "Email") %>
<%= text_input f, :email, value: @email, autocomplete: "email" %>
</div>
<%= submit "Proceed as new user", name: "op", value: "register" %>
<%= submit Gettext.dpgettext("static_pages", "oauth register page register button", "Proceed as new user"), name: "op", value: "register" %>
<p>Alternatively, sign in to connect to existing account.</p>
<p><%= Gettext.dpgettext("static_pages", "oauth register page login prompt", "Alternatively, sign in to connect to existing account.") %></p>
<div class="input">
<%= label f, :name, "Name or email" %>
<%= text_input f, :name %>
<%= label f, :name, Gettext.dpgettext("static_pages", "oauth register page login username prompt", "Name or email") %>
<%= text_input f, :name, autocomplete: "username" %>
</div>
<div class="input">
<%= label f, :password, "Password" %>
<%= password_input f, :password %>
<%= label f, :password, Gettext.dpgettext("static_pages", "oauth register page login password prompt", "Password") %>
<%= password_input f, :password, autocomplete: "password" %>
</div>
<%= submit "Proceed as existing user", name: "op", value: "connect" %>
<%= submit Gettext.dpgettext("static_pages", "oauth register page login button", "Proceed as existing user"), name: "op", value: "connect" %>
<%= hidden_input f, :client_id, value: @client_id %>
<%= hidden_input f, :redirect_uri, value: @redirect_uri %>

View file

@ -20,36 +20,38 @@
<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>

View file

@ -5,7 +5,7 @@
<form class="pull-right collapse" method="POST" action="<%= Helpers.util_path(@conn, :remote_subscribe) %>">
<input type="hidden" name="nickname" value="<%= @user.nickname %>">
<input type="hidden" name="profile" value="">
<button type="submit" class="collapse">Remote follow</button>
<button type="submit" class="collapse"><%= Gettext.dpgettext("static_pages", "static fe profile page remote follow button", "Remote follow") %></button>
</form>
<%= raw Formatter.emojify(@user.name, @user.emoji) %> |
<%= link "@#{@user.nickname}@#{Endpoint.host()}", to: (@user.uri || @user.ap_id) %>

View file

@ -1 +1 @@
<h2>Invalid Token</h2>
<h2><%= Gettext.dpgettext("static_pages", "password reset invalid token message", "Invalid Token") %></h2>

View file

@ -1,13 +1,13 @@
<h2>Password Reset for <%= @user.nickname %></h2>
<%= form_for @conn, Routes.reset_password_path(@conn, :do_reset), [as: "data"], fn f -> %>
<div class="form-row">
<%= label f, :password, "Password" %>
<%= label f, :password, Gettext.dpgettext("static_pages", "password reset form password prompt", "Password") %>
<%= password_input f, :password %>
</div>
<div class="form-row">
<%= label f, :password_confirmation, "Confirmation" %>
<%= label f, :password_confirmation, Gettext.dpgettext("static_pages", "password reset form confirm password prompt", "Confirmation") %>
<%= password_input f, :password_confirmation %>
</div>
<%= hidden_input f, :token, value: @token.token %>
<%= submit "Reset" %>
<%= submit Gettext.dpgettext("static_pages", "password reset button", "Reset") %>
<% end %>

View file

@ -1,2 +1,6 @@
<h2>Password reset failed</h2>
<h3><a href="<%= Pleroma.Web.Endpoint.url() %>">Homepage</a></h3>
<h2><%= Gettext.dpgettext("static_pages", "password reset failed message", "Password reset failed") %></h2>
<h3>
<a href="<%= Pleroma.Web.Endpoint.url() %>">
<%= Gettext.dpgettext("static_pages", "password reset failed homepage link", "Homepage") %>
</a>
</h3>

View file

@ -1,2 +1,2 @@
<h2>Password changed!</h2>
<h3><a href="<%= Pleroma.Web.Endpoint.url() %>">Homepage</a></h3>
<h2><%= Gettext.dpgettext("static_pages", "password reset successful message", "Password changed!") %></h2>
<h3><a href="<%= Pleroma.Web.Endpoint.url() %>"><%= Gettext.dpgettext("static_pages", "password reset successful homepage link", "Homepage") %></a></h3>

View file

@ -1,11 +1,11 @@
<%= if @error == :error do %>
<h2>Error fetching user</h2>
<h2><%= Gettext.dpgettext("static_pages", "remote follow error", "Error fetching user") %></h2>
<% else %>
<h2>Remote follow</h2>
<h2><%= Gettext.dpgettext("static_pages", "remote follow header", "Remote follow") %></h2>
<img height="128" width="128" src="<%= avatar_url(@followee) %>">
<p><%= @followee.nickname %></p>
<%= form_for @conn, Routes.remote_follow_path(@conn, :do_follow), [as: "user"], fn f -> %>
<%= hidden_input f, :id, value: @followee.id %>
<%= submit "Authorize" %>
<%= submit Gettext.dpgettext("static_pages", "remote follow authorization button", "Authorize") %>
<% end %>
<% end %>

View file

@ -1,14 +1,14 @@
<%= if @error do %>
<h2><%= @error %></h2>
<% end %>
<h2>Log in to follow</h2>
<h2><%= Gettext.dpgettext("static_pages", "remote follow header, need login", "Log in to follow") %></h2>
<p><%= @followee.nickname %></p>
<img height="128" width="128" src="<%= avatar_url(@followee) %>">
<%= form_for @conn, Routes.remote_follow_path(@conn, :do_follow), [as: "authorization"], fn f -> %>
<%= text_input f, :name, placeholder: "Username", required: true %>
<%= text_input f, :name, placeholder: Gettext.dpgettext("static_pages", "placeholder text for username entry", "Username"), required: true, autocomplete: "username" %>
<br>
<%= password_input f, :password, placeholder: "Password", required: true %>
<%= password_input f, :password, placeholder: Gettext.dpgettext("static_pages", "placeholder text for password entry", "Password"), required: true, autocomplete: "password" %>
<br>
<%= hidden_input f, :id, value: @followee.id %>
<%= submit "Authorize" %>
<%= submit Gettext.dpgettext("static_pages", "remote follow authorization button for login", "Authorize") %>
<% end %>

View file

@ -1,13 +1,13 @@
<%= if @error do %>
<h2><%= @error %></h2>
<% end %>
<h2>Two-factor authentication</h2>
<h2><%= Gettext.dpgettext("static_pages", "remote follow mfa header", "Two-factor authentication") %></h2>
<p><%= @followee.nickname %></p>
<img height="128" width="128" src="<%= avatar_url(@followee) %>">
<%= form_for @conn, Routes.remote_follow_path(@conn, :do_follow), [as: "mfa"], fn f -> %>
<%= text_input f, :code, placeholder: "Authentication code", required: true %>
<%= text_input f, :code, placeholder: Gettext.dpgettext("static_pages", "placeholder text for auth code entry", "Authentication code"), required: true %>
<br>
<%= hidden_input f, :id, value: @followee.id %>
<%= hidden_input f, :token, value: @mfa_token %>
<%= submit "Authorize" %>
<%= submit Gettext.dpgettext("static_pages", "remote follow authorization button for mfa", "Authorize") %>
<% end %>

View file

@ -1,6 +1,5 @@
<%= if @error do %>
<p>Error following account</p>
<p><%= Gettext.dpgettext("static_pages", "remote follow error", "Error following account") %></p>
<% else %>
<h2>Account followed!</h2>
<h2><%= Gettext.dpgettext("static_pages", "remote follow success", "Account followed!") %></h2>
<% end %>

View file

@ -1,10 +1,10 @@
<%= if @error do %>
<h2>Error: <%= @error %></h2>
<h2><%= Gettext.dpgettext("static_pages", "remote follow error", "Error: %{error}", error: @error) %></h2>
<% else %>
<h2>Remotely follow <%= @nickname %></h2>
<h2><%= Gettext.dpgettext("static_pages", "remote follow header", "Remotely follow %{nickname}", nickname: @nickname) %></h2>
<%= form_for @conn, Routes.util_path(@conn, :remote_subscribe), [as: "user"], fn f -> %>
<%= hidden_input f, :nickname, value: @nickname %>
<%= text_input f, :profile, placeholder: "Your account ID, e.g. lain@quitter.se" %>
<%= submit "Follow" %>
<%= text_input f, :profile, placeholder: Gettext.dpgettext("static_pages", "placeholder text for account id", "Your account ID, e.g. lain@quitter.se") %>
<%= submit Gettext.dpgettext("static_pages", "remote follow authorization button for following with a remote account", "Follow") %>
<% end %>
<% end %>

View file

@ -12,6 +12,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
alias Pleroma.UserInviteToken
def register_user(params, opts \\ []) do
fallback_language = Gettext.get_locale()
params =
params
|> Map.take([:email, :token, :password])
@ -20,6 +22,10 @@ def register_user(params, opts \\ []) do
|> Map.put(:name, Map.get(params, :fullname, params[:username]))
|> Map.put(:password_confirmation, params[:password])
|> Map.put(:registration_reason, params[:reason])
|> Map.put(
:language,
Pleroma.Web.Gettext.normalize_locale(params[:language]) || fallback_language
)
if Pleroma.Config.get([:instance, :registrations_open]) do
create_user(params, opts)

View file

@ -5,4 +5,5 @@
defmodule Pleroma.Web.TwitterAPI.PasswordView do
use Pleroma.Web, :view
import Phoenix.HTML.Form
alias Pleroma.Web.Gettext
end

View file

@ -5,6 +5,7 @@
defmodule Pleroma.Web.TwitterAPI.RemoteFollowView do
use Pleroma.Web, :view
import Phoenix.HTML.Form
alias Pleroma.Web.Gettext
defdelegate avatar_url(user), to: Pleroma.User
end

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilView do
import Phoenix.HTML.Form
alias Pleroma.Config
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Gettext
def status_net_config(instance) do
"""

View file

@ -6,6 +6,7 @@ defmodule Pleroma.Web.EmailView do
use Pleroma.Web, :view
import Phoenix.HTML
import Phoenix.HTML.Link
alias Pleroma.Web.Gettext
def avatar_url(user) do
Pleroma.User.avatar_url(user)

View file

@ -4,4 +4,5 @@
defmodule Pleroma.Web.Mailer.SubscriptionView do
use Pleroma.Web, :view
alias Pleroma.Web.Gettext
end

View file

@ -0,0 +1,25 @@
defmodule Pleroma.Workers.SearchIndexingWorker do
use Pleroma.Workers.WorkerHelper, queue: "search_indexing"
@impl Oban.Worker
def perform(%Job{args: %{"op" => "add_to_index", "activity" => activity_id}}) do
activity = Pleroma.Activity.get_by_id_with_object(activity_id)
search_module = Pleroma.Config.get([Pleroma.Search, :module])
search_module.add_to_index(activity)
:ok
end
def perform(%Job{args: %{"op" => "remove_from_index", "object" => object_id}}) do
object = Pleroma.Object.get_by_id(object_id)
search_module = Pleroma.Config.get([Pleroma.Search, :module])
search_module.remove_from_index(object)
:ok
end
end

10
mix.exs
View file

@ -123,7 +123,10 @@ defp deps do
{:ecto_sql, "~> 3.6.2"},
{:postgrex, ">= 0.15.5"},
{:oban, "~> 2.3.4"},
{:gettext, "~> 0.18"},
{:gettext,
git: "https://github.com/tusooa/gettext.git",
ref: "72fb2496b6c5280ed911bdc3756890e7f38a4808",
override: true},
{:bcrypt_elixir, "~> 2.2"},
{:trailing_format_plug, "~> 0.0.7"},
{:fast_sanitize, "~> 0.2.0"},
@ -200,6 +203,7 @@ defp deps do
{:nimble_parsec, "~> 1.0", override: true},
{:phoenix_live_dashboard, "~> 0.6.2"},
{:ecto_psql_extras, "~> 0.6"},
{:elasticsearch, "~> 1.0.0"},
# indirect dependency version override
{:plug, "~> 1.10.4", override: true},
@ -248,9 +252,10 @@ defp version(version) do
identifier_filter = ~r/[^0-9a-z\-]+/i
git_available? = match?({_output, 0}, System.cmd("sh", ["-c", "command -v git"]))
dotgit_present? = File.exists?(".git")
git_pre_release =
if git_available? do
if git_available? and dotgit_present? do
{tag, tag_err} =
System.cmd("git", ["describe", "--tags", "--abbrev=0"], stderr_to_stdout: true)
@ -277,6 +282,7 @@ defp version(version) do
# Branch name as pre-release version component, denoted with a dot
branch_name =
with true <- git_available?,
true <- dotgit_present?,
{branch_name, 0} <- System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"]),
branch_name <- String.trim(branch_name),
branch_name <- System.get_env("PLEROMA_BUILD_BRANCH") || branch_name,

View file

@ -57,7 +57,7 @@
"gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"},
"gen_stage": {:hex, :gen_stage, "0.14.3", "d0c66f1c87faa301c1a85a809a3ee9097a4264b2edf7644bf5c123237ef732bf", [:mix], [], "hexpm"},
"gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm"},
"gettext": {:hex, :gettext, "0.18.2", "7df3ea191bb56c0309c00a783334b288d08a879f53a7014341284635850a6e55", [:mix], [], "hexpm", "f9f537b13d4fdd30f3039d33cb80144c3aa1f8d9698e47d7bcbcc8df93b1f5c5"},
"gettext": {:git, "https://github.com/tusooa/gettext.git", "72fb2496b6c5280ed911bdc3756890e7f38a4808", [ref: "72fb2496b6c5280ed911bdc3756890e7f38a4808"]},
"gun": {:hex, :gun, "2.0.0-rc.2", "7c489a32dedccb77b6e82d1f3c5a7dadfbfa004ec14e322cdb5e579c438632d2", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "6b9d1eae146410d727140dbf8b404b9631302ecc2066d1d12f22097ad7d254fc"},
"hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"},
"hpax": {:hex, :hpax, "0.1.1", "2396c313683ada39e98c20a75a82911592b47e5c24391363343bde74f82396ca", [:mix], [], "hexpm", "0ae7d5a0b04a8a60caf7a39fcf3ec476f35cc2cc16c05abea730d3ce6ac6c826"},

View file

@ -1,20 +1,22 @@
{
"properties": {
"_timestamp": {
"type": "date",
"index": true
},
"instance": {
"type": "keyword"
},
"content": {
"type": "text"
},
"hashtags": {
"type": "keyword"
},
"user": {
"type": "text"
"mappings": {
"properties": {
"_timestamp": {
"type": "date",
"index": true
},
"instance": {
"type": "keyword"
},
"content": {
"type": "text"
},
"hashtags": {
"type": "keyword"
},
"user": {
"type": "text"
}
}
}
}

185
priv/gettext/default.pot Normal file
View file

@ -0,0 +1,185 @@
## This file is a PO Template file.
##
## "msgid"s here are often extracted from source code.
## Add new translations manually only if they're dynamic
## translations that can't be statically extracted.
##
## Run "mix gettext.extract" to bring this file up to
## date. Leave "msgstr"s empty as changing them here as no
## effect: edit them in PO (.po) files instead.
msgid ""
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:122
msgid "%{name} - %{count} is not a multiple of %{multiple}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:131
msgid "%{name} - %{value} is larger than exclusive maximum %{max}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:140
msgid "%{name} - %{value} is larger than inclusive maximum %{max}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:149
msgid "%{name} - %{value} is smaller than exclusive minimum %{min}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:158
msgid "%{name} - %{value} is smaller than inclusive minimum %{min}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:102
msgid "%{name} - Array items must be unique."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:114
msgid "%{name} - Array length %{length} is larger than maxItems: %{}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:106
msgid "%{name} - Array length %{length} is smaller than minItems: %{min}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:166
msgid "%{name} - Invalid %{type}. Got: %{value}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:174
msgid "%{name} - Invalid format. Expected %{format}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:51
msgid "%{name} - Invalid schema.type. Got: %{type}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:178
msgid "%{name} - Invalid value for enum."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:95
msgid "%{name} - String length is larger than maxLength: %{length}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:88
msgid "%{name} - String length is smaller than minLength: %{length}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:63
msgid "%{name} - null value where %{type} expected."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:60
msgid "%{name} - null value."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:182
msgid "Failed to cast to any schema in %{polymorphic_type}"
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:71
msgid "Failed to cast value as %{invalid_schema}. Value must be castable using `allOf` schemas listed."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:84
msgid "Failed to cast value to one of: %{failed_schemas}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:78
msgid "Failed to cast value using any of: %{failed_schemas}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:212
msgid "Invalid value for header: %{name}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:204
msgid "Missing field: %{name}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:208
msgid "Missing header: %{name}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:196
msgid "No value provided for required discriminator `%{field}`."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:216
msgid "Object property count %{property_count} is greater than maxProperties: %{max_properties}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:224
msgid "Object property count %{property_count} is less than minProperties: %{min_properties}"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/static_fe/static_fe/error.html.eex:2
msgid "Oops"
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:188
msgid "Unexpected field: %{name}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:200
msgid "Unknown schema: %{name}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:192
msgid "Value used as discriminator for `%{field}` matches no schemas."
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/embed/show.html.eex:43
#: lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex:37
msgid "announces"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/embed/show.html.eex:44
#: lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex:38
msgid "likes"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/embed/show.html.eex:42
#: lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex:36
msgid "replies"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/embed/show.html.eex:27
#: lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex:22
msgid "sensitive media"
msgstr ""

View file

@ -0,0 +1,186 @@
## "msgid"s in this file come from POT (.pot) files.
##
## Do not add, change, or remove "msgid"s manually here as
## they're tied to the ones in the corresponding POT file
## (with the same domain).
##
## Use "mix gettext.extract --merge" or "mix gettext.merge"
## to merge POT files into PO files.
msgid ""
msgstr ""
"Language: en_test\n"
"Plural-Forms: nplurals=2\n"
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:122
msgid "%{name} - %{count} is not a multiple of %{multiple}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:131
msgid "%{name} - %{value} is larger than exclusive maximum %{max}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:140
msgid "%{name} - %{value} is larger than inclusive maximum %{max}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:149
msgid "%{name} - %{value} is smaller than exclusive minimum %{min}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:158
msgid "%{name} - %{value} is smaller than inclusive minimum %{min}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:102
msgid "%{name} - Array items must be unique."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:114
msgid "%{name} - Array length %{length} is larger than maxItems: %{}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:106
msgid "%{name} - Array length %{length} is smaller than minItems: %{min}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:166
msgid "%{name} - Invalid %{type}. Got: %{value}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:174
msgid "%{name} - Invalid format. Expected %{format}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:51
msgid "%{name} - Invalid schema.type. Got: %{type}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:178
msgid "%{name} - Invalid value for enum."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:95
msgid "%{name} - String length is larger than maxLength: %{length}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:88
msgid "%{name} - String length is smaller than minLength: %{length}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:63
msgid "%{name} - null value where %{type} expected."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:60
msgid "%{name} - null value."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:182
msgid "Failed to cast to any schema in %{polymorphic_type}"
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:71
msgid "Failed to cast value as %{invalid_schema}. Value must be castable using `allOf` schemas listed."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:84
msgid "Failed to cast value to one of: %{failed_schemas}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:78
msgid "Failed to cast value using any of: %{failed_schemas}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:212
msgid "Invalid value for header: %{name}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:204
msgid "Missing field: %{name}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:208
msgid "Missing header: %{name}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:196
msgid "No value provided for required discriminator `%{field}`."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:216
msgid "Object property count %{property_count} is greater than maxProperties: %{max_properties}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:224
msgid "Object property count %{property_count} is less than minProperties: %{min_properties}"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/static_fe/static_fe/error.html.eex:2
msgid "Oops"
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:188
msgid "Unexpected field: %{name}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:200
msgid "Unknown schema: %{name}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:192
msgid "Value used as discriminator for `%{field}` matches no schemas."
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/embed/show.html.eex:43
#: lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex:37
msgid "announces"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/embed/show.html.eex:44
#: lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex:38
msgid "likes"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/embed/show.html.eex:42
#: lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex:36
msgid "replies"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/embed/show.html.eex:27
#: lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex:22
msgid "sensitive media"
msgstr ""

View file

@ -0,0 +1,557 @@
## "msgid"s in this file come from POT (.pot) files.
##
## Do not add, change, or remove "msgid"s manually here as
## they're tied to the ones in the corresponding POT file
## (with the same domain).
##
## Use "mix gettext.extract --merge" or "mix gettext.merge"
## to merge POT files into PO files.
msgid ""
msgstr ""
"Language: en_test\n"
"Plural-Forms: nplurals=2\n"
msgid "can't be blank"
msgstr ""
msgid "has already been taken"
msgstr ""
msgid "is invalid"
msgstr ""
msgid "has invalid format"
msgstr ""
msgid "has an invalid entry"
msgstr ""
msgid "is reserved"
msgstr ""
msgid "does not match confirmation"
msgstr ""
msgid "is still associated with this entry"
msgstr ""
msgid "are still associated with this entry"
msgstr ""
msgid "should be %{count} character(s)"
msgid_plural "should be %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should have %{count} item(s)"
msgid_plural "should have %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should be at least %{count} character(s)"
msgid_plural "should be at least %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should have at least %{count} item(s)"
msgid_plural "should have at least %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should be at most %{count} character(s)"
msgid_plural "should be at most %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should have at most %{count} item(s)"
msgid_plural "should have at most %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
msgid "must be less than %{number}"
msgstr ""
msgid "must be greater than %{number}"
msgstr ""
msgid "must be less than or equal to %{number}"
msgstr ""
msgid "must be greater than or equal to %{number}"
msgstr ""
msgid "must be equal to %{number}"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api.ex:523
msgid "Account not found"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api.ex:316
msgid "Already voted"
msgstr ""
#, elixir-format
#: lib/pleroma/web/o_auth/o_auth_controller.ex:402
msgid "Bad request"
msgstr ""
#, elixir-format
#: lib/pleroma/web/controller_helper.ex:97
#: lib/pleroma/web/controller_helper.ex:103
msgid "Can't display this activity"
msgstr ""
#, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:324
msgid "Can't find user"
msgstr ""
#, elixir-format
#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:80
msgid "Can't get favorites"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api/utils.ex:482
msgid "Cannot post an empty status without attachments"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api/utils.ex:441
msgid "Comment must be up to %{max_size} characters"
msgstr ""
#, elixir-format
#: lib/pleroma/config_db.ex:200
msgid "Config with params %{params} not found"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api.ex:167 lib/pleroma/web/common_api.ex:171
msgid "Could not delete"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api.ex:217
msgid "Could not favorite"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api.ex:254
msgid "Could not unfavorite"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api.ex:202
msgid "Could not unrepeat"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api.ex:530 lib/pleroma/web/common_api.ex:539
msgid "Could not update state"
msgstr ""
#, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:205
msgid "Error."
msgstr ""
#, elixir-format
#: lib/pleroma/web/twitter_api/twitter_api.ex:99
msgid "Invalid CAPTCHA"
msgstr ""
#, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:144
#: lib/pleroma/web/o_auth/o_auth_controller.ex:631
msgid "Invalid credentials"
msgstr ""
#, elixir-format
#: lib/pleroma/web/plugs/ensure_authenticated_plug.ex:42
msgid "Invalid credentials."
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api.ex:337
msgid "Invalid indices"
msgstr ""
#, elixir-format
#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:29
msgid "Invalid parameters"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api/utils.ex:349
msgid "Invalid password."
msgstr ""
#, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:254
msgid "Invalid request"
msgstr ""
#, elixir-format
#: lib/pleroma/web/twitter_api/twitter_api.ex:102
msgid "Kocaptcha service unavailable"
msgstr ""
#, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:140
msgid "Missing parameters"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api/utils.ex:477
msgid "No such conversation"
msgstr ""
#, elixir-format
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:171
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:197 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:239
msgid "No such permission_group"
msgstr ""
#, elixir-format
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:504
#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:11 lib/pleroma/web/feed/tag_controller.ex:16
#: lib/pleroma/web/feed/user_controller.ex:69 lib/pleroma/web/o_status/o_status_controller.ex:132
#: lib/pleroma/web/plugs/uploaded_media.ex:84
msgid "Not found"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api.ex:308
msgid "Poll's author can't vote"
msgstr ""
#, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:20
#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:39 lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:51
#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:52 lib/pleroma/web/mastodon_api/controllers/status_controller.ex:326
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:71
msgid "Record not found"
msgstr ""
#, elixir-format
#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:35
#: lib/pleroma/web/feed/user_controller.ex:78 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:42
#: lib/pleroma/web/o_status/o_status_controller.ex:138
msgid "Something went wrong"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api/activity_draft.ex:143
msgid "The message visibility must be direct"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api/utils.ex:492
msgid "The status is over the character limit"
msgstr ""
#, elixir-format
#: lib/pleroma/web/plugs/ensure_public_or_authenticated_plug.ex:36
msgid "This resource requires authentication."
msgstr ""
#, elixir-format
#: lib/pleroma/web/plugs/rate_limiter.ex:208
msgid "Throttled"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api.ex:338
msgid "Too many choices"
msgstr ""
#, elixir-format
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:268
msgid "You can't revoke your own admin status."
msgstr ""
#, elixir-format
#: lib/pleroma/web/o_auth/o_auth_controller.ex:243
#: lib/pleroma/web/o_auth/o_auth_controller.ex:333
msgid "Your account is currently disabled"
msgstr ""
#, elixir-format
#: lib/pleroma/web/o_auth/o_auth_controller.ex:205
#: lib/pleroma/web/o_auth/o_auth_controller.ex:356
msgid "Your login is missing a confirmed e-mail address"
msgstr ""
#, elixir-format
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:392
msgid "can't read inbox of %{nickname} as %{as_nickname}"
msgstr ""
#, elixir-format
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:491
msgid "can't update outbox of %{nickname} as %{as_nickname}"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api.ex:475
msgid "conversation is already muted"
msgstr ""
#, elixir-format
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:510
msgid "error"
msgstr ""
#, elixir-format
#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:34
msgid "mascots can only be images"
msgstr ""
#, elixir-format
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:63
msgid "not found"
msgstr ""
#, elixir-format
#: lib/pleroma/web/o_auth/o_auth_controller.ex:437
msgid "Bad OAuth request."
msgstr ""
#, elixir-format
#: lib/pleroma/web/twitter_api/twitter_api.ex:108
msgid "CAPTCHA already used"
msgstr ""
#, elixir-format
#: lib/pleroma/web/twitter_api/twitter_api.ex:105
msgid "CAPTCHA expired"
msgstr ""
#, elixir-format
#: lib/pleroma/web/plugs/uploaded_media.ex:57
msgid "Failed"
msgstr ""
#, elixir-format
#: lib/pleroma/web/o_auth/o_auth_controller.ex:453
msgid "Failed to authenticate: %{message}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/o_auth/o_auth_controller.ex:484
msgid "Failed to set up user account."
msgstr ""
#, elixir-format
#: lib/pleroma/web/plugs/o_auth_scopes_plug.ex:37
msgid "Insufficient permissions: %{permissions}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/plugs/uploaded_media.ex:111
msgid "Internal Error"
msgstr ""
#, elixir-format
#: lib/pleroma/web/o_auth/fallback_controller.ex:22
#: lib/pleroma/web/o_auth/fallback_controller.ex:29
msgid "Invalid Username/Password"
msgstr ""
#, elixir-format
#: lib/pleroma/web/twitter_api/twitter_api.ex:111
msgid "Invalid answer data"
msgstr ""
#, elixir-format
#: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:33
msgid "Nodeinfo schema version not handled"
msgstr ""
#, elixir-format
#: lib/pleroma/web/o_auth/o_auth_controller.ex:194
msgid "This action is outside the authorized scopes"
msgstr ""
#, elixir-format
#: lib/pleroma/web/o_auth/fallback_controller.ex:14
msgid "Unknown error, please check the details and try again."
msgstr ""
#, elixir-format
#: lib/pleroma/web/o_auth/o_auth_controller.ex:136
#: lib/pleroma/web/o_auth/o_auth_controller.ex:180
msgid "Unlisted redirect_uri."
msgstr ""
#, elixir-format
#: lib/pleroma/web/o_auth/o_auth_controller.ex:433
msgid "Unsupported OAuth provider: %{provider}."
msgstr ""
#, elixir-format
#: lib/pleroma/uploaders/uploader.ex:74
msgid "Uploader callback timeout"
msgstr ""
#, elixir-format
#: lib/pleroma/web/uploader_controller.ex:23
msgid "bad request"
msgstr ""
#, elixir-format
#: lib/pleroma/web/twitter_api/twitter_api.ex:96
msgid "CAPTCHA Error"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api.ex:266
msgid "Could not add reaction emoji"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api.ex:277
msgid "Could not remove reaction emoji"
msgstr ""
#, elixir-format
#: lib/pleroma/web/twitter_api/twitter_api.ex:122
msgid "Invalid CAPTCHA (Missing parameter: %{name})"
msgstr ""
#, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:96
msgid "List not found"
msgstr ""
#, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:151
msgid "Missing parameter: %{name}"
msgstr ""
#, elixir-format
#: lib/pleroma/web/o_auth/o_auth_controller.ex:232
#: lib/pleroma/web/o_auth/o_auth_controller.ex:346
msgid "Password reset is required"
msgstr ""
#, elixir-format
#: lib/pleroma/tests/auth_test_controller.ex:9
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:6 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:6
#: lib/pleroma/web/admin_api/controllers/chat_controller.ex:6 lib/pleroma/web/admin_api/controllers/config_controller.ex:6
#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:6 lib/pleroma/web/admin_api/controllers/frontend_controller.ex:6
#: lib/pleroma/web/admin_api/controllers/instance_controller.ex:6 lib/pleroma/web/admin_api/controllers/instance_document_controller.ex:6
#: lib/pleroma/web/admin_api/controllers/invite_controller.ex:6 lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex:6
#: lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex:6 lib/pleroma/web/admin_api/controllers/relay_controller.ex:6
#: lib/pleroma/web/admin_api/controllers/report_controller.ex:6 lib/pleroma/web/admin_api/controllers/status_controller.ex:6
#: lib/pleroma/web/admin_api/controllers/user_controller.ex:6 lib/pleroma/web/controller_helper.ex:6 lib/pleroma/web/embed_controller.ex:6
#: lib/pleroma/web/fallback/redirect_controller.ex:6 lib/pleroma/web/feed/tag_controller.ex:6
#: lib/pleroma/web/feed/user_controller.ex:6 lib/pleroma/web/mailer/subscription_controller.ex:6
#: lib/pleroma/web/manifest_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/account_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/app_controller.ex:11 lib/pleroma/web/mastodon_api/controllers/auth_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/directory_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/filter_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/instance_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/marker_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex:14 lib/pleroma/web/mastodon_api/controllers/media_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/notification_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/report_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/search_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/status_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:7 lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:6 lib/pleroma/web/media_proxy/media_proxy_controller.ex:6
#: lib/pleroma/web/mongoose_im/mongoose_im_controller.ex:6 lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:6
#: lib/pleroma/web/o_auth/fallback_controller.ex:6 lib/pleroma/web/o_auth/mfa_controller.ex:10
#: lib/pleroma/web/o_auth/o_auth_controller.ex:6 lib/pleroma/web/o_status/o_status_controller.ex:6
#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/app_controller.ex:6
#: lib/pleroma/web/pleroma_api/controllers/backup_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/chat_controller.ex:5
#: lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex:6
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex:6
#: lib/pleroma/web/pleroma_api/controllers/instances_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:6
#: lib/pleroma/web/pleroma_api/controllers/notification_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/report_controller.ex:6
#: lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex:6
#: lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex:7 lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex:6
#: lib/pleroma/web/static_fe/static_fe_controller.ex:6 lib/pleroma/web/twitter_api/controller.ex:6
#: lib/pleroma/web/twitter_api/controllers/password_controller.ex:10 lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex:6
#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:6 lib/pleroma/web/uploader_controller.ex:6
#: lib/pleroma/web/web_finger/web_finger_controller.ex:6
msgid "Security violation: OAuth scopes check was neither handled nor explicitly skipped."
msgstr ""
#, elixir-format
#: lib/pleroma/web/plugs/ensure_authenticated_plug.ex:32
msgid "Two-factor authentication enabled, you must use a access token."
msgstr ""
#, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61
msgid "Web push subscription is disabled on this Pleroma instance"
msgstr ""
#, elixir-format
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:234
msgid "You can't revoke your own admin/moderator status."
msgstr ""
#, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:129
msgid "authorization required for timeline view"
msgstr ""
#, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:24
msgid "Access denied"
msgstr ""
#, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:321
msgid "This API requires an authenticated user"
msgstr ""
#, elixir-format
#: lib/pleroma/web/plugs/ensure_staff_privileged_plug.ex:26
#: lib/pleroma/web/plugs/user_is_admin_plug.ex:21
msgid "User is not an admin."
msgstr ""
#, elixir-format
#: lib/pleroma/user/backup.ex:75
msgid "Last export was less than a day ago"
msgid_plural "Last export was less than %{days} days ago"
msgstr[0] ""
msgstr[1] ""
#, elixir-format
#: lib/pleroma/user/backup.ex:93
msgid "Backups require enabled email"
msgstr ""
#, elixir-format
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:423
msgid "Character limit (%{limit} characters) exceeded, contains %{length} characters"
msgstr ""
#, elixir-format
#: lib/pleroma/user/backup.ex:98
msgid "Email is required"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api/utils.ex:507
msgid "Too many attachments"
msgstr ""
#, elixir-format
#: lib/pleroma/web/plugs/ensure_staff_privileged_plug.ex:33
#: lib/pleroma/web/plugs/user_is_staff_plug.ex:20
msgid "User is not a staff member."
msgstr ""
#, elixir-format
#: lib/pleroma/web/o_auth/o_auth_controller.ex:366
msgid "Your account is awaiting approval."
msgstr ""

View file

@ -0,0 +1,153 @@
## "msgid"s in this file come from POT (.pot) files.
##
## Do not add, change, or remove "msgid"s manually here as
## they're tied to the ones in the corresponding POT file
## (with the same domain).
##
## Use "mix gettext.extract --merge" or "mix gettext.merge"
## to merge POT files into PO files.
msgid ""
msgstr ""
"Language: en_test\n"
"Plural-Forms: nplurals=2\n"
msgid "eperm"
msgstr ""
msgid "eacces"
msgstr ""
msgid "eagain"
msgstr ""
msgid "ebadf"
msgstr ""
msgid "ebadmsg"
msgstr ""
msgid "ebusy"
msgstr ""
msgid "edeadlk"
msgstr ""
msgid "edeadlock"
msgstr ""
msgid "edquot"
msgstr ""
msgid "eexist"
msgstr ""
msgid "efault"
msgstr ""
msgid "efbig"
msgstr ""
msgid "eftype"
msgstr ""
msgid "eintr"
msgstr ""
msgid "einval"
msgstr ""
msgid "eio"
msgstr ""
msgid "eisdir"
msgstr ""
msgid "eloop"
msgstr ""
msgid "emfile"
msgstr ""
msgid "emlink"
msgstr ""
msgid "emultihop"
msgstr ""
msgid "enametoolong"
msgstr ""
msgid "enfile"
msgstr ""
msgid "enobufs"
msgstr ""
msgid "enodev"
msgstr ""
msgid "enolck"
msgstr ""
msgid "enolink"
msgstr ""
msgid "enoent"
msgstr ""
msgid "enomem"
msgstr ""
msgid "enospc"
msgstr ""
msgid "enosr"
msgstr ""
msgid "enostr"
msgstr ""
msgid "enosys"
msgstr ""
msgid "enotblk"
msgstr ""
msgid "enotdir"
msgstr ""
msgid "enotsup"
msgstr ""
msgid "enxio"
msgstr ""
msgid "eopnotsupp"
msgstr ""
msgid "eoverflow"
msgstr ""
msgid "epipe"
msgstr ""
msgid "erange"
msgstr ""
msgid "erofs"
msgstr ""
msgid "espipe"
msgstr ""
msgid "esrch"
msgstr ""
msgid "estale"
msgstr ""
msgid "etxtbsy"
msgstr ""
msgid "exdev"
msgstr ""

View file

@ -0,0 +1,529 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR Free Software Foundation, Inc.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"PO-Revision-Date: 2022-03-06 11:27-0500\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
#~ ## "msgid"s in this file come from POT (.pot) files.
#~ ##
#~ ## Do not add, change, or remove "msgid"s manually here as
#~ ## they're tied to the ones in the corresponding POT file
#~ ## (with the same domain).
#~ ##
#~ ## Use "mix gettext.extract --merge" or "mix gettext.merge"
#~ ## to merge POT files into PO files.
#~ msgid ""
#~ msgstr ""
#~ "Language: en_test\n"
#~ "Plural-Forms: nplurals=2\n"
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:9
msgctxt "remote follow authorization button"
msgid "Authorize"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:2
msgctxt "remote follow error"
msgid "Error fetching user"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:4
msgctxt "remote follow header"
msgid "Remote follow"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:8
msgctxt "placeholder text for auth code entry"
msgid "Authentication code"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:10
msgctxt "placeholder text for password entry"
msgid "Password"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:8
msgctxt "placeholder text for username entry"
msgid "Username"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:13
msgctxt "remote follow authorization button for login"
msgid "Authorize"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:12
msgctxt "remote follow authorization button for mfa"
msgid "Authorize"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:2
msgctxt "remote follow error"
msgid "Error following account"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:4
msgctxt "remote follow header, need login"
msgid "Log in to follow"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:4
msgctxt "remote follow mfa header"
msgid "Two-factor authentication"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:4
msgctxt "remote follow success"
msgid "Account followed!"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:7
msgctxt "placeholder text for account id"
msgid "Your account ID, e.g. lain@quitter.se"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:8
msgctxt "remote follow authorization button for following with a remote account"
msgid "Follow"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:2
msgctxt "remote follow error"
msgid "Error: %{error}"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:4
msgctxt "remote follow header"
msgid "Remotely follow %{nickname}"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:12
msgctxt "password reset button"
msgid "Reset"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:4
msgctxt "password reset failed homepage link"
msgid "Homepage"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:1
msgctxt "password reset failed message"
msgid "Password reset failed"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:8
msgctxt "password reset form confirm password prompt"
msgid "Confirmation"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:4
msgctxt "password reset form password prompt"
msgid "Password"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex:1
msgctxt "password reset invalid token message"
msgid "Invalid Token"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:2
msgctxt "password reset successful homepage link"
msgid "Homepage"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:1
msgctxt "password reset successful message"
msgid "Password changed!"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/feed/feed/tag.atom.eex:15
#: lib/pleroma/web/templates/feed/feed/tag.rss.eex:7
msgctxt "tag feed description"
msgid "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse."
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:1
msgctxt "oauth authorization exists page title"
msgid "Authorization exists"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:32
msgctxt "oauth authorize approve button"
msgid "Approve"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:30
msgctxt "oauth authorize cancel button"
msgid "Cancel"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:23
msgctxt "oauth authorize message"
msgid "Application <strong>%{client_name}</strong> is requesting access to your account."
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:1
msgctxt "oauth authorized page title"
msgid "Successfully authorized"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:1
msgctxt "oauth external provider page title"
msgid "Sign in with external provider"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:13
msgctxt "oauth external provider sign in button"
msgid "Sign in with %{strategy}"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:54
msgctxt "oauth login button"
msgid "Log In"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:51
msgctxt "oauth login password prompt"
msgid "Password"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:47
msgctxt "oauth login username prompt"
msgid "Username"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:39
msgctxt "oauth register nickname prompt"
msgid "Pleroma Handle"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:37
msgctxt "oauth register nickname unchangeable warning"
msgid "Choose carefully! You won't be able to change this later. You will be able to change your display name, though."
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:18
msgctxt "oauth register page email prompt"
msgid "Email"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:10
msgctxt "oauth register page fill form prompt"
msgid "If you'd like to register a new account, please provide the details below."
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:35
msgctxt "oauth register page login button"
msgid "Proceed as existing user"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:31
msgctxt "oauth register page login password prompt"
msgid "Password"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:24
msgctxt "oauth register page login prompt"
msgid "Alternatively, sign in to connect to existing account."
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:27
msgctxt "oauth register page login username prompt"
msgid "Name or email"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:14
msgctxt "oauth register page nickname prompt"
msgid "Nickname"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:22
msgctxt "oauth register page register button"
msgid "Proceed as new user"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:8
msgctxt "oauth register page title"
msgid "Registration Details"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:36
msgctxt "oauth register page title"
msgid "This is the first time you visit! Please enter your Pleroma handle."
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex:2
msgctxt "oauth scopes message"
msgid "The following permissions will be granted"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:2
#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:2
msgctxt "oauth token code message"
msgid "Token code is <br>%{token}"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:12
msgctxt "mfa auth code prompt"
msgid "Authentication code"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:8
msgctxt "mfa auth page title"
msgid "Two-factor authentication"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:23
msgctxt "mfa auth page use recovery code link"
msgid "Enter a two-factor recovery code"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:20
msgctxt "mfa auth verify code button"
msgid "Verify"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:8
msgctxt "mfa recover page title"
msgid "Two-factor recovery"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:12
msgctxt "mfa recover recovery code prompt"
msgid "Recovery code"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:23
msgctxt "mfa recover use 2fa code link"
msgid "Enter a two-factor code"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:20
msgctxt "mfa recover verify recovery code button"
msgid "Verify"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex:8
msgctxt "static fe profile page remote follow button"
msgid "Remote follow"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/email/digest.html.eex:163
msgctxt "digest email header line"
msgid "Hey %{nickname}, here is what you've missed!"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/email/digest.html.eex:544
msgctxt "digest email receiver address"
msgid "The email address you are subscribed as is <a href='mailto:%{@user.email}' style='color: %{color};text-decoration: none;'>%{email}</a>. "
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/email/digest.html.eex:538
msgctxt "digest email sending reason"
msgid "You have received this email because you have signed up to receive digest emails from <b>%{instance}</b> Pleroma instance."
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/email/digest.html.eex:547
msgctxt "digest email unsubscribe action"
msgid "To unsubscribe, please go %{here}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/email/digest.html.eex:547
msgctxt "digest email unsubscribe action link text"
msgid "here"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_failure.html.eex:1
msgctxt "mailer unsubscribe failed message"
msgid "UNSUBSCRIBE FAILURE"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_success.html.eex:1
msgctxt "mailer unsubscribe successful message"
msgid "UNSUBSCRIBE SUCCESSFUL"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/email/digest.html.eex:385
msgctxt "new followers count header"
msgid "%{count} New Follower"
msgid_plural "%{count} New Followers"
msgstr[0] "xx%{count} New Followerxx"
msgstr[1] "xx%{count} New Followersxx"
#, elixir-format
#: lib/pleroma/emails/user_email.ex:356
msgctxt "account archive email body - self-requested"
msgid "<p>You requested a full backup of your Pleroma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
msgstr ""
#, elixir-format
#: lib/pleroma/emails/user_email.ex:384
msgctxt "account archive email subject"
msgid "Your account archive is ready"
msgstr ""
#, elixir-format
#: lib/pleroma/emails/user_email.ex:188
msgctxt "approval pending email body"
msgid "<h3>Awaiting Approval</h3>\n<p>Your account at %{instance_name} is being reviewed by staff. You will receive another email once your account is approved.</p>\n"
msgstr ""
#, elixir-format
#: lib/pleroma/emails/user_email.ex:202
msgctxt "approval pending email subject"
msgid "Your account is awaiting approval"
msgstr "xxYour account is awaiting approvalxx"
#, elixir-format
#: lib/pleroma/emails/user_email.ex:158
msgctxt "confirmation email body"
msgid "<h3>Thank you for registering on %{instance_name}</h3>\n<p>Email confirmation is required to activate the account.</p>\n<p>Please click the following link to <a href=\"%{confirmation_url}\">activate your account</a>.</p>\n"
msgstr ""
#, elixir-format
#: lib/pleroma/emails/user_email.ex:174
msgctxt "confirmation email subject"
msgid "%{instance_name} account confirmation"
msgstr ""
#, elixir-format
#: lib/pleroma/emails/user_email.ex:310
msgctxt "digest email subject"
msgid "Your digest from %{instance_name}"
msgstr "xxYour digest from %{instance_name}xx"
#, elixir-format
#: lib/pleroma/emails/user_email.ex:81
msgctxt "password reset email body"
msgid "<h3>Reset your password at %{instance_name}</h3>\n<p>Someone has requested password change for your account at %{instance_name}.</p>\n<p>If it was you, visit the following link to proceed: <a href=\"%{password_reset_url}\">reset password</a>.</p>\n<p>If it was someone else, nothing to worry about: your data is secure and your password has not been changed.</p>\n"
msgstr ""
#, elixir-format
#: lib/pleroma/emails/user_email.ex:98
msgctxt "password reset email subject"
msgid "Password reset"
msgstr ""
#, elixir-format
#: lib/pleroma/emails/user_email.ex:215
msgctxt "successful registration email body"
msgid "<h3>Hello @%{nickname},</h3>\n<p>Your account at %{instance_name} has been registered successfully.</p>\n<p>No further action is required to activate your account.</p>\n"
msgstr ""
#, elixir-format
#: lib/pleroma/emails/user_email.ex:231
msgctxt "successful registration email subject"
msgid "Account registered on %{instance_name}"
msgstr ""
#, elixir-format
#: lib/pleroma/emails/user_email.ex:119
msgctxt "user invitation email body"
msgid "<h3>You are invited to %{instance_name}</h3>\n<p>%{inviter_name} invites you to join %{instance_name}, an instance of Pleroma federated social networking platform.</p>\n<p>Click the following link to register: <a href=\"%{registration_url}\">accept invitation</a>.</p>\n"
msgstr ""
#, elixir-format
#: lib/pleroma/emails/user_email.ex:136
msgctxt "user invitation email subject"
msgid "Invitation to %{instance_name}"
msgstr ""
#, elixir-format
#: lib/pleroma/emails/user_email.ex:53
msgctxt "welcome email html body"
msgid "Welcome to %{instance_name}!"
msgstr ""
#, elixir-format
#: lib/pleroma/emails/user_email.ex:41
msgctxt "welcome email subject"
msgid "Welcome to %{instance_name}!"
msgstr ""
#, elixir-format
#: lib/pleroma/emails/user_email.ex:65
msgctxt "welcome email text body"
msgid "Welcome to %{instance_name}!"
msgstr ""
#, elixir-format
#: lib/pleroma/emails/user_email.ex:368
msgctxt "account archive email body - admin requested"
msgid "<p>Admin @%{admin_nickname} requested a full backup of your Pleroma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
msgstr ""

View file

@ -90,121 +90,99 @@ msgid "must be equal to %{number}"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api/common_api.ex:505
#: lib/pleroma/web/common_api.ex:523
msgid "Account not found"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api/common_api.ex:339
#: lib/pleroma/web/common_api.ex:316
msgid "Already voted"
msgstr ""
#, elixir-format
#: lib/pleroma/web/oauth/oauth_controller.ex:359
#: lib/pleroma/web/o_auth/o_auth_controller.ex:402
msgid "Bad request"
msgstr ""
#, elixir-format
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:426
msgid "Can't delete object"
msgstr ""
#, elixir-format
#: lib/pleroma/web/controller_helper.ex:105
#: lib/pleroma/web/controller_helper.ex:111
#: lib/pleroma/web/controller_helper.ex:97
#: lib/pleroma/web/controller_helper.ex:103
msgid "Can't display this activity"
msgstr ""
#, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:285
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:324
msgid "Can't find user"
msgstr ""
#, elixir-format
#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:61
#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:80
msgid "Can't get favorites"
msgstr ""
#, elixir-format
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:438
msgid "Can't like object"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api/utils.ex:563
#: lib/pleroma/web/common_api/utils.ex:482
msgid "Cannot post an empty status without attachments"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api/utils.ex:511
#: lib/pleroma/web/common_api/utils.ex:441
msgid "Comment must be up to %{max_size} characters"
msgstr ""
#, elixir-format
#: lib/pleroma/config/config_db.ex:191
#: lib/pleroma/config_db.ex:200
msgid "Config with params %{params} not found"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api/common_api.ex:181
#: lib/pleroma/web/common_api/common_api.ex:185
#: lib/pleroma/web/common_api.ex:167 lib/pleroma/web/common_api.ex:171
msgid "Could not delete"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api/common_api.ex:231
#: lib/pleroma/web/common_api.ex:217
msgid "Could not favorite"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api/common_api.ex:453
msgid "Could not pin"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api/common_api.ex:278
#: lib/pleroma/web/common_api.ex:254
msgid "Could not unfavorite"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api/common_api.ex:463
msgid "Could not unpin"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api/common_api.ex:216
#: lib/pleroma/web/common_api.ex:202
msgid "Could not unrepeat"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api/common_api.ex:512
#: lib/pleroma/web/common_api/common_api.ex:521
#: lib/pleroma/web/common_api.ex:530 lib/pleroma/web/common_api.ex:539
msgid "Could not update state"
msgstr ""
#, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:207
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:205
msgid "Error."
msgstr ""
#, elixir-format
#: lib/pleroma/web/twitter_api/twitter_api.ex:106
#: lib/pleroma/web/twitter_api/twitter_api.ex:99
msgid "Invalid CAPTCHA"
msgstr ""
#, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:116
#: lib/pleroma/web/oauth/oauth_controller.ex:568
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:144
#: lib/pleroma/web/o_auth/o_auth_controller.ex:631
msgid "Invalid credentials"
msgstr ""
#, elixir-format
#: lib/pleroma/plugs/ensure_authenticated_plug.ex:38
#: lib/pleroma/web/plugs/ensure_authenticated_plug.ex:42
msgid "Invalid credentials."
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api/common_api.ex:355
#: lib/pleroma/web/common_api.ex:337
msgid "Invalid indices"
msgstr ""
@ -214,189 +192,184 @@ msgid "Invalid parameters"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api/utils.ex:414
#: lib/pleroma/web/common_api/utils.ex:349
msgid "Invalid password."
msgstr ""
#, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:220
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:254
msgid "Invalid request"
msgstr ""
#, elixir-format
#: lib/pleroma/web/twitter_api/twitter_api.ex:109
#: lib/pleroma/web/twitter_api/twitter_api.ex:102
msgid "Kocaptcha service unavailable"
msgstr ""
#, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:112
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:140
msgid "Missing parameters"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api/utils.ex:547
#: lib/pleroma/web/common_api/utils.ex:477
msgid "No such conversation"
msgstr ""
#, elixir-format
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:388
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:414 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:456
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:171
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:197 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:239
msgid "No such permission_group"
msgstr ""
#, elixir-format
#: lib/pleroma/plugs/uploaded_media.ex:84
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:486 lib/pleroma/web/admin_api/controllers/fallback_controller.ex:11
#: lib/pleroma/web/feed/user_controller.ex:71 lib/pleroma/web/ostatus/ostatus_controller.ex:143
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:504
#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:11 lib/pleroma/web/feed/tag_controller.ex:16
#: lib/pleroma/web/feed/user_controller.ex:69 lib/pleroma/web/o_status/o_status_controller.ex:132
#: lib/pleroma/web/plugs/uploaded_media.ex:84
msgid "Not found"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api/common_api.ex:331
#: lib/pleroma/web/common_api.ex:308
msgid "Poll's author can't vote"
msgstr ""
#, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:20
#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:37 lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:49
#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:50 lib/pleroma/web/mastodon_api/controllers/status_controller.ex:306
#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:39 lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:51
#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:52 lib/pleroma/web/mastodon_api/controllers/status_controller.ex:326
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:71
msgid "Record not found"
msgstr ""
#, elixir-format
#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:35
#: lib/pleroma/web/feed/user_controller.ex:77 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:36
#: lib/pleroma/web/ostatus/ostatus_controller.ex:149
#: lib/pleroma/web/feed/user_controller.ex:78 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:42
#: lib/pleroma/web/o_status/o_status_controller.ex:138
msgid "Something went wrong"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api/activity_draft.ex:107
#: lib/pleroma/web/common_api/activity_draft.ex:143
msgid "The message visibility must be direct"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api/utils.ex:573
#: lib/pleroma/web/common_api/utils.ex:492
msgid "The status is over the character limit"
msgstr ""
#, elixir-format
#: lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex:31
#: lib/pleroma/web/plugs/ensure_public_or_authenticated_plug.ex:36
msgid "This resource requires authentication."
msgstr ""
#, elixir-format
#: lib/pleroma/plugs/rate_limiter/rate_limiter.ex:206
#: lib/pleroma/web/plugs/rate_limiter.ex:208
msgid "Throttled"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api/common_api.ex:356
#: lib/pleroma/web/common_api.ex:338
msgid "Too many choices"
msgstr ""
#, elixir-format
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:443
msgid "Unhandled activity type"
msgstr ""
#, elixir-format
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:485
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:268
msgid "You can't revoke your own admin status."
msgstr ""
#, elixir-format
#: lib/pleroma/web/oauth/oauth_controller.ex:221
#: lib/pleroma/web/oauth/oauth_controller.ex:308
#: lib/pleroma/web/o_auth/o_auth_controller.ex:243
#: lib/pleroma/web/o_auth/o_auth_controller.ex:333
msgid "Your account is currently disabled"
msgstr ""
#, elixir-format
#: lib/pleroma/web/oauth/oauth_controller.ex:183
#: lib/pleroma/web/oauth/oauth_controller.ex:331
#: lib/pleroma/web/o_auth/o_auth_controller.ex:205
#: lib/pleroma/web/o_auth/o_auth_controller.ex:356
msgid "Your login is missing a confirmed e-mail address"
msgstr ""
#, elixir-format
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:390
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:392
msgid "can't read inbox of %{nickname} as %{as_nickname}"
msgstr ""
#, elixir-format
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:473
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:491
msgid "can't update outbox of %{nickname} as %{as_nickname}"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api/common_api.ex:471
#: lib/pleroma/web/common_api.ex:475
msgid "conversation is already muted"
msgstr ""
#, elixir-format
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:314
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:492
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:510
msgid "error"
msgstr ""
#, elixir-format
#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:32
#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:34
msgid "mascots can only be images"
msgstr ""
#, elixir-format
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:62
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:63
msgid "not found"
msgstr ""
#, elixir-format
#: lib/pleroma/web/oauth/oauth_controller.ex:394
#: lib/pleroma/web/o_auth/o_auth_controller.ex:437
msgid "Bad OAuth request."
msgstr ""
#, elixir-format
#: lib/pleroma/web/twitter_api/twitter_api.ex:115
#: lib/pleroma/web/twitter_api/twitter_api.ex:108
msgid "CAPTCHA already used"
msgstr ""
#, elixir-format
#: lib/pleroma/web/twitter_api/twitter_api.ex:112
#: lib/pleroma/web/twitter_api/twitter_api.ex:105
msgid "CAPTCHA expired"
msgstr ""
#, elixir-format
#: lib/pleroma/plugs/uploaded_media.ex:57
#: lib/pleroma/web/plugs/uploaded_media.ex:57
msgid "Failed"
msgstr ""
#, elixir-format
#: lib/pleroma/web/oauth/oauth_controller.ex:410
#: lib/pleroma/web/o_auth/o_auth_controller.ex:453
msgid "Failed to authenticate: %{message}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/oauth/oauth_controller.ex:441
#: lib/pleroma/web/o_auth/o_auth_controller.ex:484
msgid "Failed to set up user account."
msgstr ""
#, elixir-format
#: lib/pleroma/plugs/oauth_scopes_plug.ex:38
#: lib/pleroma/web/plugs/o_auth_scopes_plug.ex:37
msgid "Insufficient permissions: %{permissions}."
msgstr ""
#, elixir-format
#: lib/pleroma/plugs/uploaded_media.ex:104
#: lib/pleroma/web/plugs/uploaded_media.ex:111
msgid "Internal Error"
msgstr ""
#, elixir-format
#: lib/pleroma/web/oauth/fallback_controller.ex:22
#: lib/pleroma/web/oauth/fallback_controller.ex:29
#: lib/pleroma/web/o_auth/fallback_controller.ex:22
#: lib/pleroma/web/o_auth/fallback_controller.ex:29
msgid "Invalid Username/Password"
msgstr ""
#, elixir-format
#: lib/pleroma/web/twitter_api/twitter_api.ex:118
#: lib/pleroma/web/twitter_api/twitter_api.ex:111
msgid "Invalid answer data"
msgstr ""
@ -406,28 +379,28 @@ msgid "Nodeinfo schema version not handled"
msgstr ""
#, elixir-format
#: lib/pleroma/web/oauth/oauth_controller.ex:172
#: lib/pleroma/web/o_auth/o_auth_controller.ex:194
msgid "This action is outside the authorized scopes"
msgstr ""
#, elixir-format
#: lib/pleroma/web/oauth/fallback_controller.ex:14
#: lib/pleroma/web/o_auth/fallback_controller.ex:14
msgid "Unknown error, please check the details and try again."
msgstr ""
#, elixir-format
#: lib/pleroma/web/oauth/oauth_controller.ex:119
#: lib/pleroma/web/oauth/oauth_controller.ex:158
#: lib/pleroma/web/o_auth/o_auth_controller.ex:136
#: lib/pleroma/web/o_auth/o_auth_controller.ex:180
msgid "Unlisted redirect_uri."
msgstr ""
#, elixir-format
#: lib/pleroma/web/oauth/oauth_controller.ex:390
#: lib/pleroma/web/o_auth/o_auth_controller.ex:433
msgid "Unsupported OAuth provider: %{provider}."
msgstr ""
#, elixir-format
#: lib/pleroma/uploaders/uploader.ex:72
#: lib/pleroma/uploaders/uploader.ex:74
msgid "Uploader callback timeout"
msgstr ""
@ -437,120 +410,101 @@ msgid "bad request"
msgstr ""
#, elixir-format
#: lib/pleroma/web/twitter_api/twitter_api.ex:103
#: lib/pleroma/web/twitter_api/twitter_api.ex:96
msgid "CAPTCHA Error"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api/common_api.ex:290
#: lib/pleroma/web/common_api.ex:266
msgid "Could not add reaction emoji"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api/common_api.ex:301
#: lib/pleroma/web/common_api.ex:277
msgid "Could not remove reaction emoji"
msgstr ""
#, elixir-format
#: lib/pleroma/web/twitter_api/twitter_api.ex:129
#: lib/pleroma/web/twitter_api/twitter_api.ex:122
msgid "Invalid CAPTCHA (Missing parameter: %{name})"
msgstr ""
#, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:92
#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:96
msgid "List not found"
msgstr ""
#, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:123
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:151
msgid "Missing parameter: %{name}"
msgstr ""
#, elixir-format
#: lib/pleroma/web/oauth/oauth_controller.ex:210
#: lib/pleroma/web/oauth/oauth_controller.ex:321
#: lib/pleroma/web/o_auth/o_auth_controller.ex:232
#: lib/pleroma/web/o_auth/o_auth_controller.ex:346
msgid "Password reset is required"
msgstr ""
#, elixir-format
#: lib/pleroma/tests/auth_test_controller.ex:9
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:6 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:6
#: lib/pleroma/web/admin_api/controllers/config_controller.ex:6 lib/pleroma/web/admin_api/controllers/fallback_controller.ex:6
#: lib/pleroma/web/admin_api/controllers/chat_controller.ex:6 lib/pleroma/web/admin_api/controllers/config_controller.ex:6
#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:6 lib/pleroma/web/admin_api/controllers/frontend_controller.ex:6
#: lib/pleroma/web/admin_api/controllers/instance_controller.ex:6 lib/pleroma/web/admin_api/controllers/instance_document_controller.ex:6
#: lib/pleroma/web/admin_api/controllers/invite_controller.ex:6 lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex:6
#: lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex:6 lib/pleroma/web/admin_api/controllers/relay_controller.ex:6
#: lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex:6 lib/pleroma/web/admin_api/controllers/relay_controller.ex:6
#: lib/pleroma/web/admin_api/controllers/report_controller.ex:6 lib/pleroma/web/admin_api/controllers/status_controller.ex:6
#: lib/pleroma/web/controller_helper.ex:6 lib/pleroma/web/embed_controller.ex:6
#: lib/pleroma/web/fallback_redirect_controller.ex:6 lib/pleroma/web/feed/tag_controller.ex:6
#: lib/pleroma/web/feed/user_controller.ex:6 lib/pleroma/web/mailer/subscription_controller.ex:2
#: lib/pleroma/web/masto_fe_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/account_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/app_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/auth_controller.ex:6
#: lib/pleroma/web/admin_api/controllers/user_controller.ex:6 lib/pleroma/web/controller_helper.ex:6 lib/pleroma/web/embed_controller.ex:6
#: lib/pleroma/web/fallback/redirect_controller.ex:6 lib/pleroma/web/feed/tag_controller.ex:6
#: lib/pleroma/web/feed/user_controller.ex:6 lib/pleroma/web/mailer/subscription_controller.ex:6
#: lib/pleroma/web/manifest_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/account_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/app_controller.ex:11 lib/pleroma/web/mastodon_api/controllers/auth_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/filter_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/instance_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/list_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/marker_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex:14
#: lib/pleroma/web/mastodon_api/controllers/media_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/notification_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/report_controller.ex:8
#: lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/search_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/status_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:7
#: lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:6
#: lib/pleroma/web/media_proxy/media_proxy_controller.ex:6 lib/pleroma/web/mongooseim/mongoose_im_controller.ex:6
#: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:6 lib/pleroma/web/oauth/fallback_controller.ex:6
#: lib/pleroma/web/oauth/mfa_controller.ex:10 lib/pleroma/web/oauth/oauth_controller.ex:6
#: lib/pleroma/web/ostatus/ostatus_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/account_controller.ex:6
#: lib/pleroma/web/pleroma_api/controllers/chat_controller.ex:5 lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex:6
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:2 lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex:6
#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/notification_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/directory_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/filter_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/instance_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/marker_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex:14 lib/pleroma/web/mastodon_api/controllers/media_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/notification_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/report_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/search_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/status_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:7 lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:6 lib/pleroma/web/media_proxy/media_proxy_controller.ex:6
#: lib/pleroma/web/mongoose_im/mongoose_im_controller.ex:6 lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:6
#: lib/pleroma/web/o_auth/fallback_controller.ex:6 lib/pleroma/web/o_auth/mfa_controller.ex:10
#: lib/pleroma/web/o_auth/o_auth_controller.ex:6 lib/pleroma/web/o_status/o_status_controller.ex:6
#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/app_controller.ex:6
#: lib/pleroma/web/pleroma_api/controllers/backup_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/chat_controller.ex:5
#: lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex:6
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex:6
#: lib/pleroma/web/pleroma_api/controllers/instances_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:6
#: lib/pleroma/web/pleroma_api/controllers/notification_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/report_controller.ex:6
#: lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex:6
#: lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex:7 lib/pleroma/web/static_fe/static_fe_controller.ex:6
#: lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex:7 lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex:6
#: lib/pleroma/web/static_fe/static_fe_controller.ex:6 lib/pleroma/web/twitter_api/controller.ex:6
#: lib/pleroma/web/twitter_api/controllers/password_controller.ex:10 lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex:6
#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:6 lib/pleroma/web/twitter_api/twitter_api_controller.ex:6
#: lib/pleroma/web/uploader_controller.ex:6 lib/pleroma/web/web_finger/web_finger_controller.ex:6
#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:6 lib/pleroma/web/uploader_controller.ex:6
#: lib/pleroma/web/web_finger/web_finger_controller.ex:6
msgid "Security violation: OAuth scopes check was neither handled nor explicitly skipped."
msgstr ""
#, elixir-format
#: lib/pleroma/plugs/ensure_authenticated_plug.ex:28
#: lib/pleroma/web/plugs/ensure_authenticated_plug.ex:32
msgid "Two-factor authentication enabled, you must use a access token."
msgstr ""
#, elixir-format
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:210
msgid "Unexpected error occurred while adding file to pack."
msgstr ""
#, elixir-format
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:138
msgid "Unexpected error occurred while creating pack."
msgstr ""
#, elixir-format
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:278
msgid "Unexpected error occurred while removing file from pack."
msgstr ""
#, elixir-format
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:250
msgid "Unexpected error occurred while updating file in pack."
msgstr ""
#, elixir-format
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:179
msgid "Unexpected error occurred while updating pack metadata."
msgstr ""
#, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61
msgid "Web push subscription is disabled on this Pleroma instance"
msgstr ""
#, elixir-format
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:451
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:234
msgid "You can't revoke your own admin/moderator status."
msgstr ""
#, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:126
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:129
msgid "authorization required for timeline view"
msgstr ""
@ -560,11 +514,50 @@ msgid "Access denied"
msgstr ""
#, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:282
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:321
msgid "This API requires an authenticated user"
msgstr ""
#, elixir-format
#: lib/pleroma/plugs/user_is_admin_plug.ex:21
#: lib/pleroma/web/plugs/ensure_staff_privileged_plug.ex:26
#: lib/pleroma/web/plugs/user_is_admin_plug.ex:21
msgid "User is not an admin."
msgstr ""
#, elixir-format
#: lib/pleroma/user/backup.ex:75
msgid "Last export was less than a day ago"
msgid_plural "Last export was less than %{days} days ago"
msgstr[0] ""
msgstr[1] ""
#, elixir-format
#: lib/pleroma/user/backup.ex:93
msgid "Backups require enabled email"
msgstr ""
#, elixir-format
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:423
msgid "Character limit (%{limit} characters) exceeded, contains %{length} characters"
msgstr ""
#, elixir-format
#: lib/pleroma/user/backup.ex:98
msgid "Email is required"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api/utils.ex:507
msgid "Too many attachments"
msgstr ""
#, elixir-format
#: lib/pleroma/web/plugs/ensure_staff_privileged_plug.ex:33
#: lib/pleroma/web/plugs/user_is_staff_plug.ex:20
msgid "User is not a staff member."
msgstr ""
#, elixir-format
#: lib/pleroma/web/o_auth/o_auth_controller.ex:366
msgid "Your account is awaiting approval."
msgstr ""

View file

@ -0,0 +1,513 @@
## This file is a PO Template file.
##
## "msgid"s here are often extracted from source code.
## Add new translations manually only if they're dynamic
## translations that can't be statically extracted.
##
## Run "mix gettext.extract" to bring this file up to
## date. Leave "msgstr"s empty as changing them here as no
## effect: edit them in PO (.po) files instead.
msgid ""
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:9
msgctxt "remote follow authorization button"
msgid "Authorize"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:2
msgctxt "remote follow error"
msgid "Error fetching user"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:4
msgctxt "remote follow header"
msgid "Remote follow"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:8
msgctxt "placeholder text for auth code entry"
msgid "Authentication code"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:10
msgctxt "placeholder text for password entry"
msgid "Password"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:8
msgctxt "placeholder text for username entry"
msgid "Username"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:13
msgctxt "remote follow authorization button for login"
msgid "Authorize"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:12
msgctxt "remote follow authorization button for mfa"
msgid "Authorize"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:2
msgctxt "remote follow error"
msgid "Error following account"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:4
msgctxt "remote follow header, need login"
msgid "Log in to follow"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:4
msgctxt "remote follow mfa header"
msgid "Two-factor authentication"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:4
msgctxt "remote follow success"
msgid "Account followed!"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:7
msgctxt "placeholder text for account id"
msgid "Your account ID, e.g. lain@quitter.se"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:8
msgctxt "remote follow authorization button for following with a remote account"
msgid "Follow"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:2
msgctxt "remote follow error"
msgid "Error: %{error}"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:4
msgctxt "remote follow header"
msgid "Remotely follow %{nickname}"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:12
msgctxt "password reset button"
msgid "Reset"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:4
msgctxt "password reset failed homepage link"
msgid "Homepage"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:1
msgctxt "password reset failed message"
msgid "Password reset failed"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:8
msgctxt "password reset form confirm password prompt"
msgid "Confirmation"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:4
msgctxt "password reset form password prompt"
msgid "Password"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex:1
msgctxt "password reset invalid token message"
msgid "Invalid Token"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:2
msgctxt "password reset successful homepage link"
msgid "Homepage"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:1
msgctxt "password reset successful message"
msgid "Password changed!"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/feed/feed/tag.atom.eex:15
#: lib/pleroma/web/templates/feed/feed/tag.rss.eex:7
msgctxt "tag feed description"
msgid "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse."
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:1
msgctxt "oauth authorization exists page title"
msgid "Authorization exists"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:32
msgctxt "oauth authorize approve button"
msgid "Approve"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:30
msgctxt "oauth authorize cancel button"
msgid "Cancel"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:23
msgctxt "oauth authorize message"
msgid "Application <strong>%{client_name}</strong> is requesting access to your account."
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:1
msgctxt "oauth authorized page title"
msgid "Successfully authorized"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:1
msgctxt "oauth external provider page title"
msgid "Sign in with external provider"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:13
msgctxt "oauth external provider sign in button"
msgid "Sign in with %{strategy}"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:54
msgctxt "oauth login button"
msgid "Log In"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:51
msgctxt "oauth login password prompt"
msgid "Password"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:47
msgctxt "oauth login username prompt"
msgid "Username"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:39
msgctxt "oauth register nickname prompt"
msgid "Pleroma Handle"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:37
msgctxt "oauth register nickname unchangeable warning"
msgid "Choose carefully! You won't be able to change this later. You will be able to change your display name, though."
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:18
msgctxt "oauth register page email prompt"
msgid "Email"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:10
msgctxt "oauth register page fill form prompt"
msgid "If you'd like to register a new account, please provide the details below."
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:35
msgctxt "oauth register page login button"
msgid "Proceed as existing user"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:31
msgctxt "oauth register page login password prompt"
msgid "Password"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:24
msgctxt "oauth register page login prompt"
msgid "Alternatively, sign in to connect to existing account."
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:27
msgctxt "oauth register page login username prompt"
msgid "Name or email"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:14
msgctxt "oauth register page nickname prompt"
msgid "Nickname"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:22
msgctxt "oauth register page register button"
msgid "Proceed as new user"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:8
msgctxt "oauth register page title"
msgid "Registration Details"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:36
msgctxt "oauth register page title"
msgid "This is the first time you visit! Please enter your Pleroma handle."
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex:2
msgctxt "oauth scopes message"
msgid "The following permissions will be granted"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:2
#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:2
msgctxt "oauth token code message"
msgid "Token code is <br>%{token}"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:12
msgctxt "mfa auth code prompt"
msgid "Authentication code"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:8
msgctxt "mfa auth page title"
msgid "Two-factor authentication"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:23
msgctxt "mfa auth page use recovery code link"
msgid "Enter a two-factor recovery code"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:20
msgctxt "mfa auth verify code button"
msgid "Verify"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:8
msgctxt "mfa recover page title"
msgid "Two-factor recovery"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:12
msgctxt "mfa recover recovery code prompt"
msgid "Recovery code"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:23
msgctxt "mfa recover use 2fa code link"
msgid "Enter a two-factor code"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:20
msgctxt "mfa recover verify recovery code button"
msgid "Verify"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex:8
msgctxt "static fe profile page remote follow button"
msgid "Remote follow"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/email/digest.html.eex:163
msgctxt "digest email header line"
msgid "Hey %{nickname}, here is what you've missed!"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/email/digest.html.eex:544
msgctxt "digest email receiver address"
msgid "The email address you are subscribed as is <a href='mailto:%{@user.email}' style='color: %{color};text-decoration: none;'>%{email}</a>. "
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/email/digest.html.eex:538
msgctxt "digest email sending reason"
msgid "You have received this email because you have signed up to receive digest emails from <b>%{instance}</b> Pleroma instance."
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/email/digest.html.eex:547
msgctxt "digest email unsubscribe action"
msgid "To unsubscribe, please go %{here}."
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/email/digest.html.eex:547
msgctxt "digest email unsubscribe action link text"
msgid "here"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_failure.html.eex:1
msgctxt "mailer unsubscribe failed message"
msgid "UNSUBSCRIBE FAILURE"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_success.html.eex:1
msgctxt "mailer unsubscribe successful message"
msgid "UNSUBSCRIBE SUCCESSFUL"
msgstr ""
#, elixir-format
#: lib/pleroma/web/templates/email/digest.html.eex:385
msgctxt "new followers count header"
msgid "%{count} New Follower"
msgid_plural "%{count} New Followers"
msgstr[0] ""
msgstr[1] ""
#, elixir-format
#: lib/pleroma/emails/user_email.ex:356
msgctxt "account archive email body - self-requested"
msgid "<p>You requested a full backup of your Pleroma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
msgstr ""
#, elixir-format
#: lib/pleroma/emails/user_email.ex:384
msgctxt "account archive email subject"
msgid "Your account archive is ready"
msgstr ""
#, elixir-format
#: lib/pleroma/emails/user_email.ex:188
msgctxt "approval pending email body"
msgid "<h3>Awaiting Approval</h3>\n<p>Your account at %{instance_name} is being reviewed by staff. You will receive another email once your account is approved.</p>\n"
msgstr ""
#, elixir-format
#: lib/pleroma/emails/user_email.ex:202
msgctxt "approval pending email subject"
msgid "Your account is awaiting approval"
msgstr ""
#, elixir-format
#: lib/pleroma/emails/user_email.ex:158
msgctxt "confirmation email body"
msgid "<h3>Thank you for registering on %{instance_name}</h3>\n<p>Email confirmation is required to activate the account.</p>\n<p>Please click the following link to <a href=\"%{confirmation_url}\">activate your account</a>.</p>\n"
msgstr ""
#, elixir-format
#: lib/pleroma/emails/user_email.ex:174
msgctxt "confirmation email subject"
msgid "%{instance_name} account confirmation"
msgstr ""
#, elixir-format
#: lib/pleroma/emails/user_email.ex:310
msgctxt "digest email subject"
msgid "Your digest from %{instance_name}"
msgstr ""
#, elixir-format
#: lib/pleroma/emails/user_email.ex:81
msgctxt "password reset email body"
msgid "<h3>Reset your password at %{instance_name}</h3>\n<p>Someone has requested password change for your account at %{instance_name}.</p>\n<p>If it was you, visit the following link to proceed: <a href=\"%{password_reset_url}\">reset password</a>.</p>\n<p>If it was someone else, nothing to worry about: your data is secure and your password has not been changed.</p>\n"
msgstr ""
#, elixir-format
#: lib/pleroma/emails/user_email.ex:98
msgctxt "password reset email subject"
msgid "Password reset"
msgstr ""
#, elixir-format
#: lib/pleroma/emails/user_email.ex:215
msgctxt "successful registration email body"
msgid "<h3>Hello @%{nickname},</h3>\n<p>Your account at %{instance_name} has been registered successfully.</p>\n<p>No further action is required to activate your account.</p>\n"
msgstr ""
#, elixir-format
#: lib/pleroma/emails/user_email.ex:231
msgctxt "successful registration email subject"
msgid "Account registered on %{instance_name}"
msgstr ""
#, elixir-format
#: lib/pleroma/emails/user_email.ex:119
msgctxt "user invitation email body"
msgid "<h3>You are invited to %{instance_name}</h3>\n<p>%{inviter_name} invites you to join %{instance_name}, an instance of Pleroma federated social networking platform.</p>\n<p>Click the following link to register: <a href=\"%{registration_url}\">accept invitation</a>.</p>\n"
msgstr ""
#, elixir-format
#: lib/pleroma/emails/user_email.ex:136
msgctxt "user invitation email subject"
msgid "Invitation to %{instance_name}"
msgstr ""
#, elixir-format
#: lib/pleroma/emails/user_email.ex:53
msgctxt "welcome email html body"
msgid "Welcome to %{instance_name}!"
msgstr ""
#, elixir-format
#: lib/pleroma/emails/user_email.ex:41
msgctxt "welcome email subject"
msgid "Welcome to %{instance_name}!"
msgstr ""
#, elixir-format
#: lib/pleroma/emails/user_email.ex:65
msgctxt "welcome email text body"
msgid "Welcome to %{instance_name}!"
msgstr ""
#, elixir-format
#: lib/pleroma/emails/user_email.ex:368
msgctxt "account archive email body - admin requested"
msgid "<p>Admin @%{admin_nickname} requested a full backup of your Pleroma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
msgstr ""

View file

@ -0,0 +1,9 @@
defmodule Pleroma.Repo.Migrations.AddLanguageToUsers do
use Ecto.Migration
def change do
alter table(:users) do
add_if_not_exists(:language, :string)
end
end
end

View file

@ -0,0 +1,31 @@
{
"attachment": {
"content": "Live stream preview",
"type": "Image",
"url": "https://owncast.localhost.localdomain/preview.gif?us=KjfNX387gm"
},
"attributedTo": "https://owncast.localhost.localdomain/federation/user/streamer",
"audience": "https://www.w3.org/ns/activitystreams#Public",
"content": "<p>I've gone live!</p><p></p><p><a class=\"hashtag\" href=\"https://directory.owncast.online/tags/owncast\">#owncast</a> <a class=\"hashtag\" href=\"https://directory.owncast.online/tags/streaming\">#streaming</a></p><a href=\"https://owncast.localhost.localdomain\">https://owncast.localhost.localdomain</a>",
"id": "https://owncast.localhost.localdomain/federation/KjBNuq8ng",
"published": "2022-04-17T15:42:03Z",
"tag": [
{
"href": "https://directory.owncast.online/tags/owncast",
"name": "#owncast",
"type": "Hashtag"
},
{
"href": "https://directory.owncast.online/tags/streaming",
"name": "#streaming",
"type": "Hashtag"
},
{
"href": "https://directory.owncast.online/tags/owncast",
"name": "#owncast",
"type": "Hashtag"
}
],
"to": "https://www.w3.org/ns/activitystreams#Public",
"type": "Note"
}

View file

@ -53,7 +53,13 @@ test "Sends digest to the given user" do
assert_email_sent(
to: {user2.name, user2.email},
html_body: ~r/here is what you've missed!/i
html_body:
Regex.compile!(
"here is what you've missed!"
|> Phoenix.HTML.html_escape()
|> Phoenix.HTML.safe_to_string(),
"i"
)
)
end
end

View file

@ -56,4 +56,16 @@ test "build approval pending email" do
assert email.subject == "Your account is awaiting approval"
assert email.html_body =~ "Awaiting Approval"
end
test "email i18n" do
user = insert(:user, language: "en_test")
email = UserEmail.approval_pending_email(user)
assert email.subject == "xxYour account is awaiting approvalxx"
end
test "email i18n should fallback to default locale if user language is unsupported" do
user = insert(:user, language: "unsupported")
email = UserEmail.approval_pending_email(user)
assert email.subject == "Your account is awaiting approval"
end
end

View file

@ -20,6 +20,7 @@ test "tells if a string is an unicode emoji" do
assert Emoji.is_unicode_emoji?("🤰")
assert Emoji.is_unicode_emoji?("❤️")
assert Emoji.is_unicode_emoji?("🏳️‍⚧️")
assert Emoji.is_unicode_emoji?("🫵")
# Additionally, we accept regional indicators.
assert Emoji.is_unicode_emoji?("🇵")

Some files were not shown because too many files have changed in this diff Show more