Merge remote-tracking branch 'upstream/develop' into registration-workflow

This commit is contained in:
Alex Gleason 2020-12-17 09:04:43 -06:00
commit 80891e83d8
No known key found for this signature in database
GPG key ID: 7211D1F99744FBB7
162 changed files with 8941 additions and 2668 deletions

8
.gitattributes vendored
View file

@ -1,8 +1,10 @@
*.ex diff=elixir
*.exs diff=elixir
# At the time of writing all js/css files included
# in the repo are minified bundles, and we don't want
# to search/diff those as text files.
priv/static/instance/static.css diff=css
# Most of js/css files included in the repo are minified bundles,
# and we don't want to search/diff those as text files.
*.js binary
*.js.map binary
*.css binary

View file

@ -57,7 +57,7 @@ unit-testing:
policy: pull
services:
- name: postgres:9.6
- name: postgres:13
alias: postgres
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
script:
@ -228,7 +228,7 @@ arm:
artifacts: *release-artifacts
only: *release-only
tags:
- arm32
- arm32-specified
image: arm32v7/elixir:1.10.3
cache: *release-cache
variables: *release-variables
@ -240,7 +240,7 @@ arm-musl:
artifacts: *release-artifacts
only: *release-only
tags:
- arm32
- arm32-specified
image: arm32v7/elixir:1.10.3-alpine
cache: *release-cache
variables: *release-variables

View file

@ -12,18 +12,23 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Admin Emails: The ap id is used as the user link in emails now.
- Improved registration workflow for email confirmation and account approval modes.
- **Breaking:** Changed `mix pleroma.user toggle_confirmed` to `mix pleroma.user confirm`
- Search: When using Postgres 11+, Pleroma will use the `websearch_to_tsvector` function to parse search queries.
- Emoji: Support the full Unicode 13.1 set of Emoji for reactions, plus regional indicators.
### Added
- Reports now generate notifications for admins and mods.
- Experimental websocket-based federation between Pleroma instances.
- Support for local-only statuses
- Support for local-only statuses.
- Support pagination of blocks and mutes.
- Account backup.
- Configuration: Add `:instance, autofollowing_nicknames` setting to provide a way to make accounts automatically follow new users that register on the local Pleroma instance.
- Ability to view remote timelines, with ex. `/api/v1/timelines/public?instance=lain.com` and streams `public:remote` and `public:remote:media`.
- The site title is now injected as a `title` tag like preloads or metadata.
- Password reset tokens now are not accepted after a certain age.
- Mix tasks to help with displaying and removing ConfigDB entries. See `mix pleroma.config`.
- OAuth form improvements: users are remembered by their cookie, the CSS is overridable by the admin, and the style has been improved.
- OAuth improvements and fixes: more secure session-based authentication (by token that could be revoked anytime), ability to revoke belonging OAuth token from any client etc.
<details>
<summary>API Changes</summary>
@ -31,13 +36,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Pleroma API: Add `idempotency_key` to the chat message entity that can be used for optimistic message sending.
- Pleroma API: (`GET /api/v1/pleroma/federation_status`) Add a way to get a list of unreachable instances.
- Mastodon API: User and conversation mutes can now auto-expire if `expires_in` parameter was given while adding the mute.
- Admin API: An endpoint to manage frontends
- Admin API: An endpoint to manage frontends.
- Streaming API: Add follow relationships updates.
</details>
### Fixed
- Users with `is_discoverable` field set to false (default value) will appear in in-service search results but be hidden from external services (search bots etc.).
- Streaming API: Posts and notifications are not dropped, when CLI task is executing.
<details>
<summary>API Changes</summary>
@ -58,6 +64,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mix task pleroma.user delete_activities for source installations.
- Fix ability to update Pleroma Chat push notifications with PUT /api/v1/push/subscription and alert type pleroma:chat_mention
- Forwarded reports duplication from Pleroma instances.
- Rich Media Previews sometimes showed the wrong preview due to a bug following redirects.
<details>
<summary>API</summary>
@ -101,7 +108,7 @@ switched to a new configuration mechanism, however it was not officially removed
- Media preview proxy (requires `ffmpeg` and `ImageMagick` to be installed and media proxy to be enabled; see `:media_preview_proxy` config for more details).
- Mix tasks for controlling user account confirmation status in bulk (`mix pleroma.user confirm_all` and `mix pleroma.user unconfirm_all`)
- Mix task for sending confirmation emails to all unconfirmed users (`mix pleroma.email send_confirmation_mails`)
- Mix task for sending confirmation emails to all unconfirmed users (`mix pleroma.email resend_confirmation_emails`)
- Mix task option for force-unfollowing relays
- App metrics: ability to restrict access to specified IP whitelist.

View file

@ -33,7 +33,7 @@ ARG DATA=/var/lib/pleroma
RUN echo "http://nl.alpinelinux.org/alpine/latest-stable/community" >> /etc/apk/repositories &&\
apk update &&\
apk add exiftool imagemagick ncurses postgresql-client &&\
apk add exiftool imagemagick libmagic ncurses postgresql-client &&\
adduser --system --shell /bin/false --home ${HOME} pleroma &&\
mkdir -p ${DATA}/uploads &&\
mkdir -p ${DATA}/static &&\

View file

@ -109,8 +109,8 @@ def make_friends(main_user, max) when is_integer(max) do
end
def make_friends(%User{} = main_user, %User{} = user) do
{:ok, _} = User.follow(main_user, user)
{:ok, _} = User.follow(user, main_user)
{:ok, _, _} = User.follow(main_user, user)
{:ok, _, _} = User.follow(user, main_user)
end
@spec get_users(User.t(), keyword()) :: [User.t()]

View file

@ -50,7 +50,7 @@ def run(_args) do
)
users
|> Enum.each(fn {:ok, follower} -> Pleroma.User.follow(follower, user) end)
|> Enum.each(fn {:ok, follower, user} -> Pleroma.User.follow(follower, user) end)
Benchee.run(
%{

View file

@ -147,16 +147,6 @@
"SameSite=Lax"
]
config :pleroma, :fed_sockets,
enabled: false,
connection_duration: :timer.hours(8),
rejection_duration: :timer.minutes(15),
fed_socket_fetches: [
default: 12_000,
interval: 3_000,
lazy: false
]
# Configures Elixir's Logger
config :logger, :console,
level: :debug,
@ -316,7 +306,7 @@
hideSitename: false,
hideUserStats: false,
loginMethod: "password",
logo: "/static/logo.png",
logo: "/static/logo.svg",
logoMargin: ".1em",
logoMask: true,
minimalScopesMode: false,
@ -353,8 +343,8 @@
config :pleroma, :manifest,
icons: [
%{
src: "/static/logo.png",
type: "image/png"
src: "/static/logo.svg",
type: "image/svg+xml"
}
],
theme_color: "#282c37",
@ -658,7 +648,7 @@
}
config :pleroma, :oauth2,
token_expires_in: 600,
token_expires_in: 3600 * 24 * 30,
issue_new_refresh_token: true,
clean_expired_tokens: false

View file

@ -1254,7 +1254,7 @@
hideSitename: false,
hideUserStats: false,
loginMethod: "password",
logo: "/static/logo.png",
logo: "/static/logo.svg",
logoMargin: ".1em",
logoMask: true,
minimalScopesMode: false,
@ -1340,7 +1340,7 @@
key: :logo,
type: {:string, :image},
description: "URL of the logo, defaults to Pleroma's logo",
suggestions: ["/static/logo.png"]
suggestions: ["/static/logo.svg"]
},
%{
key: :logoMargin,
@ -2540,7 +2540,7 @@
key: :token_expires_in,
type: :integer,
description: "The lifetime in seconds of the access token",
suggestions: [600]
suggestions: [2_592_000]
},
%{
key: :issue_new_refresh_token,

View file

@ -1,31 +0,0 @@
import Config
config :pleroma, :instance, static_dir: "/var/lib/pleroma/static"
config :pleroma, Pleroma.Uploaders.Local, uploads: "/var/lib/pleroma/uploads"
config :pleroma, :modules, runtime_dir: "/var/lib/pleroma/modules"
config_path = System.get_env("PLEROMA_CONFIG_PATH") || "/etc/pleroma/config.exs"
config :pleroma, release: true, config_path: config_path
if File.exists?(config_path) do
import_config config_path
else
warning = [
IO.ANSI.red(),
IO.ANSI.bright(),
"!!! #{config_path} not found! Please ensure it exists and that PLEROMA_CONFIG_PATH is unset or points to an existing file",
IO.ANSI.reset()
]
IO.puts(warning)
end
exported_config =
config_path
|> Path.dirname()
|> Path.join("prod.exported_from_db.secret.exs")
if File.exists?(exported_config) do
import_config exported_config
end

View file

@ -19,11 +19,6 @@
level: :warn,
format: "\n[$level] $message\n"
config :pleroma, :fed_sockets,
enabled: false,
connection_duration: 5,
rejection_duration: 5
config :pleroma, :auth, oauth_consumer_strategies: []
config :pleroma, Pleroma.Upload,

View file

@ -4,7 +4,7 @@ A Pleroma instance can be identified by "<Mastodon version> (compatible; Pleroma
## Flake IDs
Pleroma uses 128-bit ids as opposed to Mastodon's 64 bits. However just like Mastodon's ids they are lexically sortable strings
Pleroma uses 128-bit ids as opposed to Mastodon's 64 bits. However, just like Mastodon's ids, they are lexically sortable strings
## Timelines
@ -26,8 +26,8 @@ Has these additional fields under the `pleroma` object:
- `conversation_id`: the ID of the AP context the status is associated with (if any)
- `direct_conversation_id`: the ID of the Mastodon direct message conversation the status is associated with (if any)
- `in_reply_to_account_acct`: the `acct` property of User entity for replied user (if any)
- `content`: a map consisting of alternate representations of the `content` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`
- `spoiler_text`: a map consisting of alternate representations of the `spoiler_text` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`
- `content`: a map consisting of alternate representations of the `content` property with the key being its mimetype. Currently, the only alternate representation supported is `text/plain`
- `spoiler_text`: a map consisting of alternate representations of the `spoiler_text` property with the key being its mimetype. Currently, the only alternate representation supported is `text/plain`
- `expires_at`: a datetime (iso8601) that states when the post will expire (be deleted automatically), or empty if the post won't expire
- `thread_muted`: true if the thread the post belongs to is muted
- `emoji_reactions`: A list with emoji / reaction maps. The format is `{name: "☕", count: 1, me: true}`. Contains no information about the reacting users, for that use the `/statuses/:id/reactions` endpoint.
@ -170,9 +170,9 @@ Returns on success: 200 OK `{}`
Additional parameters can be added to the JSON body/Form data:
- `preview`: boolean, if set to `true` the post won't be actually posted, but the status entitiy would still be rendered back. This could be useful for previewing rich text/custom emoji, for example.
- `preview`: boolean, if set to `true` the post won't be actually posted, but the status entity would still be rendered back. This could be useful for previewing rich text/custom emoji, for example.
- `content_type`: string, contain the MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint.
- `to`: A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for for post visibility are not affected by this and will still apply.
- `to`: A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for post visibility are not affected by this and will still apply.
- `visibility`: string, besides standard MastoAPI values (`direct`, `private`, `unlisted`, `local` or `public`) it can be used to address a List by setting it to `list:LIST_ID`.
- `expires_in`: The number of seconds the posted activity should expire in. When a posted activity expires it will be deleted from the server, and a delete request for it will be federated. This needs to be longer than an hour.
- `in_reply_to_conversation_id`: Will reply to a given conversation, addressing only the people who are part of the recipient set of that conversation. Sets the visibility to `direct`.
@ -279,10 +279,27 @@ Has these additional fields under the `pleroma` object:
## Streaming
### Chats
There is an additional `user:pleroma_chat` stream. Incoming chat messages will make the current chat be sent to this `user` stream. The `event` of an incoming chat message is `pleroma:chat_update`. The payload is the updated chat with the incoming chat message in the `last_message` field.
### Remote timelines
For viewing remote server timelines, there are `public:remote` and `public:remote:media` streams. Each of these accept a parameter like `?instance=lain.com`.
### Follow relationships updates
Pleroma streams follow relationships updates as `pleroma:follow_relationships_update` events to the `user` stream.
The message payload consist of:
- `state`: a relationship state, one of `follow_pending`, `follow_accept` or `follow_reject`.
- `follower` and `following` maps with following fields:
- `id`: user ID
- `follower_count`: follower count
- `following_count`: following count
## User muting and thread muting
Both user muting and thread muting can be done for only a certain time by adding an `expires_in` parameter to the API calls and giving the expiration time in seconds.

View file

@ -579,14 +579,14 @@ Emoji reactions work a lot like favourites do. They make it possible to react to
### React to a post with a unicode emoji
* Method: `PUT`
* Authentication: required
* Params: `emoji`: A single character unicode emoji
* Params: `emoji`: A unicode RGI emoji or a regional indicator
* Response: JSON, the status.
## `DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji`
### Remove a reaction to a post with a unicode emoji
* Method: `DELETE`
* Authentication: required
* Params: `emoji`: A single character unicode emoji
* Params: `emoji`: A unicode RGI emoji or a regional indicator
* Response: JSON, the status.
## `GET /api/v1/pleroma/statuses/:id/reactions`

View file

@ -32,7 +32,7 @@
config :pleroma, configurable_from_database: false
```
To delete transfered settings from database optional flag `-d` can be used. `<env>` is `prod` by default.
To delete transferred settings from database optional flag `-d` can be used. `<env>` is `prod` by default.
=== "OTP"
```sh
@ -43,3 +43,111 @@ To delete transfered settings from database optional flag `-d` can be used. `<en
```sh
mix pleroma.config migrate_from_db [--env=<env>] [-d]
```
## Dump all of the config settings defined in the database
=== "OTP"
```sh
./bin/pleroma_ctl config dump
```
=== "From Source"
```sh
mix pleroma.config dump
```
## List individual configuration groups in the database
=== "OTP"
```sh
./bin/pleroma_ctl config groups
```
=== "From Source"
```sh
mix pleroma.config groups
```
## Dump the saved configuration values for a specific group or key
e.g., this shows all the settings under `config :pleroma`
=== "OTP"
```sh
./bin/pleroma_ctl config dump pleroma
```
=== "From Source"
```sh
mix pleroma.config dump pleroma
```
To get values under a specific key:
e.g., this shows all the settings under `config :pleroma, :instance`
=== "OTP"
```sh
./bin/pleroma_ctl config dump pleroma instance
```
=== "From Source"
```sh
mix pleroma.config dump pleroma instance
```
## Delete the saved configuration values for a specific group or key
e.g., this deletes all the settings under `config :tesla`
=== "OTP"
```sh
./bin/pleroma_ctl config delete [--force] tesla
```
=== "From Source"
```sh
mix pleroma.config delete [--force] tesla
```
To delete values under a specific key:
e.g., this deletes all the settings under `config :phoenix, :stacktrace_depth`
=== "OTP"
```sh
./bin/pleroma_ctl config delete [--force] phoenix stacktrace_depth
```
=== "From Source"
```sh
mix pleroma.config delete [--force] phoenix stacktrace_depth
```
## Remove all settings from the database
This forcibly removes all saved values in the database.
=== "OTP"
```sh
./bin/pleroma_ctl config [--force] reset
```
=== "From Source"
```sh
mix pleroma.config [--force] reset
```

View file

@ -16,8 +16,7 @@
mix pleroma.email test [--to <destination email address>]
```
Example:
Example:
=== "OTP"
@ -36,11 +35,11 @@ Example:
=== "OTP"
```sh
./bin/pleroma_ctl email send_confirmation_mails
./bin/pleroma_ctl email resend_confirmation_emails
```
=== "From Source"
```sh
mix pleroma.email send_confirmation_mails
mix pleroma.email resend_confirmation_emails
```

View file

@ -5,50 +5,37 @@ The configuration of Pleroma has traditionally been managed with a config file,
## Migration to database config
1. Run the mix task to migrate to the database. You'll receive some debugging output and a few messages informing you of what happened.
1. Run the mix task to migrate to the database.
**Source:**
```
$ mix pleroma.config migrate_to_db
```
or
**OTP:**
*Note: OTP users need Pleroma to be running for `pleroma_ctl` commands to work*
```
$ ./bin/pleroma_ctl config migrate_to_db
```
```
10:04:34.155 [debug] QUERY OK source="config" db=1.6ms decode=2.0ms queue=33.5ms idle=0.0ms
SELECT c0."id", c0."key", c0."group", c0."value", c0."inserted_at", c0."updated_at" FROM "config" AS c0 []
Migrating settings from file: /home/pleroma/config/dev.secret.exs
10:04:34.240 [debug] QUERY OK db=4.5ms queue=0.3ms idle=92.2ms
TRUNCATE config; []
10:04:34.244 [debug] QUERY OK db=2.8ms queue=0.3ms idle=97.2ms
ALTER SEQUENCE config_id_seq RESTART; []
10:04:34.256 [debug] QUERY OK source="config" db=0.8ms queue=1.4ms idle=109.8ms
SELECT c0."id", c0."key", c0."group", c0."value", c0."inserted_at", c0."updated_at" FROM "config" AS c0 WHERE ((c0."group" = $1) AND (c0."key" = $2)) [":pleroma", ":instance"]
10:04:34.292 [debug] QUERY OK db=2.6ms queue=1.7ms idle=137.7ms
INSERT INTO "config" ("group","key","value","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5) RETURNING "id" [":pleroma", ":instance", <<131, 108, 0, 0, 0, 1, 104, 2, 100, 0, 4, 110, 97, 109, 101, 109, 0, 0, 0, 7, 66, 108, 101, 114, 111, 109, 97, 106>>, ~N[2020-07-12 15:04:34], ~N[2020-07-12 15:04:34]]
Settings for key instance migrated.
Settings for group :pleroma migrated.
```
2. It is recommended to backup your config file now.
```
cp config/dev.secret.exs config/dev.secret.exs.orig
```
3. Edit your Pleroma config to enable database configuration:
```
@ -76,17 +63,17 @@ The configuration of Pleroma has traditionally been managed with a config file,
config :pleroma, Pleroma.Web.Endpoint,
url: [host: "cool.pleroma.site", scheme: "https", port: 443]
config :pleroma, Pleroma.Repo,
adapter: Ecto.Adapters.Postgres,
username: "pleroma",
password: "MySecretPassword",
database: "pleroma_prod",
hostname: "localhost"
config :pleroma, configurable_from_database: true
```
5. Restart your instance and you can now access the Settings tab in AdminFE.
@ -95,15 +82,15 @@ The configuration of Pleroma has traditionally been managed with a config file,
1. Run the mix task to migrate back from the database. You'll receive some debugging output and a few messages informing you of what happened.
**Source:**
```
$ mix pleroma.config migrate_from_db
```
or
**OTP:**
```
$ ./bin/pleroma_ctl config migrate_from_db
```
@ -111,7 +98,7 @@ The configuration of Pleroma has traditionally been managed with a config file,
```
10:26:30.593 [debug] QUERY OK source="config" db=9.8ms decode=1.2ms queue=26.0ms idle=0.0ms
SELECT c0."id", c0."key", c0."group", c0."value", c0."inserted_at", c0."updated_at" FROM "config" AS c0 []
10:26:30.659 [debug] QUERY OK source="config" db=1.1ms idle=80.7ms
SELECT c0."id", c0."key", c0."group", c0."value", c0."inserted_at", c0."updated_at" FROM "config" AS c0 []
Database configuration settings have been saved to config/dev.exported_from_db.secret.exs
@ -124,30 +111,45 @@ The configuration of Pleroma has traditionally been managed with a config file,
## Debugging
### Clearing database config
You can clear the database config by truncating the `config` table in the database. e.g.,
You can clear the database config with the following command:
```
psql -d pleroma_dev
pleroma_dev=# TRUNCATE config;
TRUNCATE TABLE
```
**Source:**
```
$ mix pleroma.config reset
```
or
**OTP:**
```
$ ./bin/pleroma_ctl config reset
```
Additionally, every time you migrate the configuration to the database the config table is automatically truncated to ensure a clean migration.
### Manually removing a setting
If you encounter a situation where the server cannot run properly because of an invalid setting in the database and this is preventing you from accessing AdminFE, you can manually remove the offending setting if you know which one it is.
e.g., here is an example showing a minimal configuration in the database. Only the `config :pleroma, :instance` settings are in the table:
e.g., here is an example showing a the removal of the `config :pleroma, :instance` settings:
```
psql -d pleroma_dev
pleroma_dev=# select * from config;
id | key | value | inserted_at | updated_at | group
----+-----------+------------------------------------------------------------+---------------------+---------------------+----------
1 | :instance | \x836c0000000168026400046e616d656d00000007426c65726f6d616a | 2020-07-12 15:33:29 | 2020-07-12 15:33:29 | :pleroma
(1 row)
pleroma_dev=# delete from config where key = ':instance' and group = ':pleroma';
DELETE 1
```
**Source:**
```
$ mix pleroma.config delete pleroma instance
Are you sure you want to continue? [n] y
config :pleroma, :instance deleted from the ConfigDB.
```
or
**OTP:**
```
$ ./bin/pleroma_ctl config delete pleroma instance
Are you sure you want to continue? [n] y
config :pleroma, :instance deleted from the ConfigDB.
```
Now the `config :pleroma, :instance` settings have been removed from the database.

View file

@ -88,3 +88,8 @@ config :pleroma, :frontend_configurations,
Note the extra `static` folder for the terms-of-service.html
Terms of Service will be shown to all users on the registration page. It's the best place where to write down the rules for your instance. You can modify the rules by adding and changing `$static_dir/static/terms-of-service.html`.
## Styling rendered pages
To overwrite the CSS stylesheet of the OAuth form and other static pages, you can upload your own CSS file to `instance/static/static.css`. This will completely replace the CSS used by those pages, so it might be a good idea to copy the one from `priv/static/instance/static.css` and make your changes.

View file

@ -14,9 +14,9 @@ This document contains notes and guidelines for Pleroma developers.
For `:api` pipeline routes, it'll be verified whether `OAuthScopesPlug` was called or explicitly skipped, and if it was not then auth information will be dropped for request. Then `EnsurePublicOrAuthenticatedPlug` will be called to ensure that either the instance is not private or user is authenticated (unless explicitly skipped). Such automated checks help to prevent human errors and result in higher security / privacy for users.
## [HTTP Basic Authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization)
## Non-OAuth authentication
* With HTTP Basic Auth, OAuth scopes check is _not_ performed for any action (since password is provided during the auth, requester is able to obtain a token with full permissions anyways). `Pleroma.Web.Plugs.AuthenticationPlug` and `Pleroma.Web.Plugs.LegacyAuthenticationPlug` both call `Pleroma.Web.Plugs.OAuthScopesPlug.skip_plug(conn)` when password is provided.
* With non-OAuth authentication ([HTTP Basic Authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) or HTTP header- or params-provided auth), OAuth scopes check is _not_ performed for any action (since password is provided during the auth, requester is able to obtain a token with full permissions anyways); auth plugs invoke `Pleroma.Helpers.AuthHelper.skip_oauth(conn)` in this case.
## Auth-related configuration, OAuth consumer mode etc.

View file

@ -12,7 +12,8 @@ defmodule Mix.Pleroma do
:cachex,
:flake_id,
:swoosh,
:timex
:timex,
:fast_html
]
@cachex_children ["object", "user", "scrubber", "web_resp"]
@doc "Common functions to be reused in mix tasks"
@ -22,8 +23,8 @@ def start_pleroma do
Pleroma.Application.limiters_setup()
Application.put_env(:phoenix, :serve_endpoints, false, persistent: true)
if Pleroma.Config.get(:env) != :test do
Application.put_env(:logger, :console, level: :debug)
unless System.get_env("DEBUG") do
Logger.remove_backend(:console)
end
adapter = Application.get_env(:tesla, :adapter)
@ -37,12 +38,23 @@ def start_pleroma do
Enum.each(apps, &Application.ensure_all_started/1)
oban_config = [
crontab: [],
repo: Pleroma.Repo,
log: false,
queues: [],
plugins: []
]
children =
[
Pleroma.Repo,
Pleroma.Emoji,
{Pleroma.Config.TransferTask, false},
Pleroma.Web.Endpoint,
{Oban, Pleroma.Config.get(Oban)}
{Oban, oban_config},
{Majic.Pool,
[name: Pleroma.MajicPool, pool_size: Pleroma.Config.get([:majic_pool, :size], 2)]}
] ++
http_children(adapter)
@ -98,12 +110,6 @@ def shell_prompt(prompt, defval \\ nil, defname \\ nil) do
end
end
def shell_yes?(message) do
if mix_shell?(),
do: Mix.shell().yes?("Continue?"),
else: shell_prompt(message, "Continue?") in ~w(Yn Y y)
end
def shell_info(message) do
if mix_shell?(),
do: Mix.shell().info(message),

View file

@ -5,6 +5,7 @@
defmodule Mix.Tasks.Pleroma.Config do
use Mix.Task
import Ecto.Query
import Mix.Pleroma
alias Pleroma.ConfigDB
@ -14,26 +15,199 @@ defmodule Mix.Tasks.Pleroma.Config do
@moduledoc File.read!("docs/administration/CLI_tasks/config.md")
def run(["migrate_to_db"]) do
start_pleroma()
migrate_to_db()
check_configdb(fn ->
start_pleroma()
migrate_to_db()
end)
end
def run(["migrate_from_db" | options]) do
check_configdb(fn ->
start_pleroma()
{opts, _} =
OptionParser.parse!(options,
strict: [env: :string, delete: :boolean],
aliases: [d: :delete]
)
migrate_from_db(opts)
end)
end
def run(["dump"]) do
check_configdb(fn ->
start_pleroma()
header = config_header()
settings =
ConfigDB
|> Repo.all()
|> Enum.sort()
unless settings == [] do
shell_info("#{header}")
Enum.each(settings, &dump(&1))
else
shell_error("No settings in ConfigDB.")
end
end)
end
def run(["dump", group, key]) do
check_configdb(fn ->
start_pleroma()
group = maybe_atomize(group)
key = maybe_atomize(key)
group
|> ConfigDB.get_by_group_and_key(key)
|> dump()
end)
end
def run(["dump", group]) do
check_configdb(fn ->
start_pleroma()
group = maybe_atomize(group)
dump_group(group)
end)
end
def run(["groups"]) do
check_configdb(fn ->
start_pleroma()
groups =
ConfigDB
|> distinct([c], true)
|> select([c], c.group)
|> Repo.all()
if length(groups) > 0 do
shell_info("The following configuration groups are set in ConfigDB:\r\n")
groups |> Enum.each(fn x -> shell_info("- #{x}") end)
shell_info("\r\n")
end
end)
end
def run(["reset", "--force"]) do
check_configdb(fn ->
start_pleroma()
truncatedb()
shell_info("The ConfigDB settings have been removed from the database.")
end)
end
def run(["reset"]) do
check_configdb(fn ->
start_pleroma()
shell_info("The following settings will be permanently removed:")
ConfigDB
|> Repo.all()
|> Enum.sort()
|> Enum.each(&dump(&1))
shell_error("\nTHIS CANNOT BE UNDONE!")
if shell_prompt("Are you sure you want to continue?", "n") in ~w(Yn Y y) do
truncatedb()
shell_info("The ConfigDB settings have been removed from the database.")
else
shell_error("No changes made.")
end
end)
end
def run(["delete", "--force", group, key]) do
start_pleroma()
{opts, _} =
OptionParser.parse!(options,
strict: [env: :string, delete: :boolean],
aliases: [d: :delete]
)
group = maybe_atomize(group)
key = maybe_atomize(key)
migrate_from_db(opts)
with true <- key_exists?(group, key) do
shell_info("The following settings will be removed from ConfigDB:\n")
group
|> ConfigDB.get_by_group_and_key(key)
|> dump()
delete_key(group, key)
else
_ ->
shell_error("No settings in ConfigDB for #{inspect(group)}, #{inspect(key)}. Aborting.")
end
end
def run(["delete", "--force", group]) do
start_pleroma()
group = maybe_atomize(group)
with true <- group_exists?(group) do
shell_info("The following settings will be removed from ConfigDB:\n")
dump_group(group)
delete_group(group)
else
_ -> shell_error("No settings in ConfigDB for #{inspect(group)}. Aborting.")
end
end
def run(["delete", group, key]) do
start_pleroma()
group = maybe_atomize(group)
key = maybe_atomize(key)
with true <- key_exists?(group, key) do
shell_info("The following settings will be removed from ConfigDB:\n")
group
|> ConfigDB.get_by_group_and_key(key)
|> dump()
if shell_prompt("Are you sure you want to continue?", "n") in ~w(Yn Y y) do
delete_key(group, key)
else
shell_error("No changes made.")
end
else
_ ->
shell_error("No settings in ConfigDB for #{inspect(group)}, #{inspect(key)}. Aborting.")
end
end
def run(["delete", group]) do
start_pleroma()
group = maybe_atomize(group)
with true <- group_exists?(group) do
shell_info("The following settings will be removed from ConfigDB:\n")
dump_group(group)
if shell_prompt("Are you sure you want to continue?", "n") in ~w(Yn Y y) do
delete_group(group)
else
shell_error("No changes made.")
end
else
_ -> shell_error("No settings in ConfigDB for #{inspect(group)}. Aborting.")
end
end
@spec migrate_to_db(Path.t() | nil) :: any()
def migrate_to_db(file_path \\ nil) do
with true <- Pleroma.Config.get([:configurable_from_database]),
:ok <- Pleroma.Config.DeprecationWarnings.warn() do
with :ok <- Pleroma.Config.DeprecationWarnings.warn() do
config_file =
if file_path do
file_path
@ -47,16 +221,15 @@ def migrate_to_db(file_path \\ nil) do
do_migrate_to_db(config_file)
else
:error -> deprecation_error()
_ -> migration_error()
_ ->
shell_error("Migration is not allowed until all deprecation warnings have been resolved.")
end
end
defp do_migrate_to_db(config_file) do
if File.exists?(config_file) do
shell_info("Migrating settings from file: #{Path.expand(config_file)}")
Ecto.Adapters.SQL.query!(Repo, "TRUNCATE config;")
Ecto.Adapters.SQL.query!(Repo, "ALTER SEQUENCE config_id_seq RESTART;")
truncatedb()
custom_config =
config_file
@ -80,52 +253,38 @@ defp create(group, settings) do
shell_info("Settings for key #{key} migrated.")
end)
shell_info("Settings for group :#{group} migrated.")
shell_info("Settings for group #{inspect(group)} migrated.")
end
defp migrate_from_db(opts) do
if Pleroma.Config.get([:configurable_from_database]) do
env = opts[:env] || Pleroma.Config.get(:env)
env = opts[:env] || Pleroma.Config.get(:env)
config_path =
if Pleroma.Config.get(:release) do
:config_path
|> Pleroma.Config.get()
|> Path.dirname()
else
"config"
end
|> Path.join("#{env}.exported_from_db.secret.exs")
config_path =
if Pleroma.Config.get(:release) do
:config_path
|> Pleroma.Config.get()
|> Path.dirname()
else
"config"
end
|> Path.join("#{env}.exported_from_db.secret.exs")
file = File.open!(config_path, [:write, :utf8])
file = File.open!(config_path, [:write, :utf8])
IO.write(file, config_header())
IO.write(file, config_header())
ConfigDB
|> Repo.all()
|> Enum.each(&write_and_delete(&1, file, opts[:delete]))
ConfigDB
|> Repo.all()
|> Enum.each(&write_and_delete(&1, file, opts[:delete]))
:ok = File.close(file)
System.cmd("mix", ["format", config_path])
:ok = File.close(file)
System.cmd("mix", ["format", config_path])
shell_info(
"Database configuration settings have been exported to config/#{env}.exported_from_db.secret.exs"
)
else
migration_error()
end
end
defp migration_error do
shell_error(
"Migration is not allowed in config. You can change this behavior by setting `config :pleroma, configurable_from_database: true`"
shell_info(
"Database configuration settings have been exported to config/#{env}.exported_from_db.secret.exs"
)
end
defp deprecation_error do
shell_error("Migration is not allowed until all deprecation warnings have been resolved.")
end
if Code.ensure_loaded?(Config.Reader) do
defp config_header, do: "import Config\r\n\r\n"
defp read_file(config_file), do: Config.Reader.read_imports!(config_file)
@ -150,8 +309,80 @@ defp write(config, file) do
defp delete(config, true) do
{:ok, _} = Repo.delete(config)
shell_info("#{config.key} deleted from DB.")
shell_info(
"config #{inspect(config.group)}, #{inspect(config.key)} was deleted from the ConfigDB."
)
end
defp delete(_config, _), do: :ok
defp dump(%ConfigDB{} = config) do
value = inspect(config.value, limit: :infinity)
shell_info("config #{inspect(config.group)}, #{inspect(config.key)}, #{value}\r\n\r\n")
end
defp dump(_), do: :noop
defp dump_group(group) when is_atom(group) do
group
|> ConfigDB.get_all_by_group()
|> Enum.each(&dump/1)
end
defp group_exists?(group) do
group
|> ConfigDB.get_all_by_group()
|> Enum.any?()
end
defp key_exists?(group, key) do
group
|> ConfigDB.get_by_group_and_key(key)
|> is_nil
|> Kernel.!()
end
defp maybe_atomize(arg) when is_atom(arg), do: arg
defp maybe_atomize(":" <> arg), do: maybe_atomize(arg)
defp maybe_atomize(arg) when is_binary(arg) do
if ConfigDB.module_name?(arg) do
String.to_existing_atom("Elixir." <> arg)
else
String.to_atom(arg)
end
end
defp check_configdb(callback) do
with true <- Pleroma.Config.get([:configurable_from_database]) do
callback.()
else
_ ->
shell_error(
"ConfigDB not enabled. Please check the value of :configurable_from_database in your configuration."
)
end
end
defp delete_key(group, key) do
check_configdb(fn ->
ConfigDB.delete(%{group: group, key: key})
end)
end
defp delete_group(group) do
check_configdb(fn ->
group
|> ConfigDB.get_all_by_group()
|> Enum.each(&ConfigDB.delete/1)
end)
end
defp truncatedb do
Ecto.Adapters.SQL.query!(Repo, "TRUNCATE config;")
Ecto.Adapters.SQL.query!(Repo, "ALTER SEQUENCE config_id_seq RESTART;")
end
end

View file

@ -48,9 +48,15 @@ def run(["bump_all_conversations"]) do
def run(["update_users_following_followers_counts"]) do
start_pleroma()
User
|> Repo.all()
|> Enum.each(&User.update_follower_count/1)
Repo.transaction(
fn ->
from(u in User, select: u)
|> Repo.stream()
|> Stream.each(&User.update_follower_count/1)
|> Stream.run()
end,
timeout: :infinity
)
end
def run(["prune_objects" | args]) do

View file

@ -161,12 +161,21 @@ def run(["gen" | rest]) do
)
|> Path.expand()
{strip_uploads_message, strip_uploads_default} =
if Pleroma.Utils.command_available?("exiftool") do
{"Do you want to strip location (GPS) data from uploaded images? This requires exiftool, it was detected as installed. (y/n)",
"y"}
else
{"Do you want to strip location (GPS) data from uploaded images? This requires exiftool, it was detected as not installed, please install it if you answer yes. (y/n)",
"n"}
end
strip_uploads =
get_option(
options,
:strip_uploads,
"Do you want to strip location (GPS) data from uploaded images? (y/n)",
"y"
strip_uploads_message,
strip_uploads_default
) === "y"
anonymize_uploads =
@ -253,7 +262,7 @@ def run(["gen" | rest]) do
else
shell_error(
"The task would have overwritten the following files:\n" <>
(Enum.map(paths, &"- #{&1}\n") |> Enum.join("")) <>
(Enum.map(will_overwrite, &"- #{&1}\n") |> Enum.join("")) <>
"Rerun with `--force` to overwrite them."
)
end

View file

@ -60,7 +60,7 @@ def run(["new", nickname, email | rest]) do
- admin: #{if(admin?, do: "true", else: "false")}
""")
proceed? = assume_yes? or shell_yes?("Continue?")
proceed? = assume_yes? or shell_prompt("Continue?", "n") in ~w(Yn Y y)
if proceed? do
start_pleroma()

View file

@ -194,6 +194,19 @@ def get_by_id(id) do
end
end
def get_by_id_with_user_actor(id) do
case FlakeId.flake_id?(id) do
true ->
Activity
|> where([a], a.id == ^id)
|> with_preloaded_user_actor()
|> Repo.one()
_ ->
nil
end
end
def get_by_id_with_object(id) do
Activity
|> where(id: ^id)

View file

@ -19,11 +19,18 @@ def search(user, search_query, options \\ []) do
offset = Keyword.get(options, :offset, 0)
author = Keyword.get(options, :author)
search_function =
if :persistent_term.get({Pleroma.Repo, :postgres_version}) >= 11 do
:websearch
else
:plain
end
Activity
|> Activity.with_preloaded_object()
|> Activity.restrict_deactivated_users()
|> restrict_public()
|> query_with(index_type, search_query)
|> query_with(index_type, search_query, search_function)
|> maybe_restrict_local(user)
|> maybe_restrict_author(author)
|> maybe_restrict_blocked(user)
@ -53,7 +60,7 @@ defp restrict_public(q) do
)
end
defp query_with(q, :gin, search_query) do
defp query_with(q, :gin, search_query, :plain) do
from([a, o] in q,
where:
fragment(
@ -64,7 +71,18 @@ defp query_with(q, :gin, search_query) do
)
end
defp query_with(q, :rum, search_query) do
defp query_with(q, :gin, search_query, :websearch) do
from([a, o] in q,
where:
fragment(
"to_tsvector('english', ?->>'content') @@ websearch_to_tsquery('english', ?)",
o.data,
^search_query
)
)
end
defp query_with(q, :rum, search_query, :plain) do
from([a, o] in q,
where:
fragment(
@ -76,6 +94,18 @@ defp query_with(q, :rum, search_query) do
)
end
defp query_with(q, :rum, search_query, :websearch) do
from([a, o] in q,
where:
fragment(
"? @@ websearch_to_tsquery('english', ?)",
o.fts_content,
^search_query
),
order_by: [fragment("? <=> now()::date", o.inserted_at)]
)
end
defp maybe_restrict_local(q, user) do
limit = Pleroma.Config.get([:instance, :limit_to_local_content], :unauthenticated)

View file

@ -110,7 +110,28 @@ def start(_type, _args) do
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Pleroma.Supervisor]
Supervisor.start_link(children, opts)
result = Supervisor.start_link(children, opts)
set_postgres_server_version()
result
end
defp set_postgres_server_version do
version =
with %{rows: [[version]]} <- Ecto.Adapters.SQL.query!(Pleroma.Repo, "show server_version"),
{num, _} <- Float.parse(version) do
num
else
e ->
Logger.warn(
"Could not get the postgres version: #{inspect(e)}.\nSetting the default value of 9.6"
)
9.6
end
:persistent_term.put({Pleroma.Repo, :postgres_version}, version)
end
def load_custom_modules do

View file

@ -24,6 +24,7 @@ def verify! do
|> check_migrations_applied!()
|> check_welcome_message_config!()
|> check_rum!()
|> check_repo_pool_size!()
|> handle_result()
end
@ -188,6 +189,30 @@ defp check_system_commands!(:ok) do
defp check_system_commands!(result), do: result
defp check_repo_pool_size!(:ok) do
if Pleroma.Config.get([Pleroma.Repo, :pool_size], 10) != 10 and
not Pleroma.Config.get([:dangerzone, :override_repo_pool_size], false) do
Logger.error("""
!!!CONFIG WARNING!!!
The database pool size has been altered from the recommended value of 10.
Please revert or ensure your database is tuned appropriately and then set
`config :pleroma, :dangerzone, override_repo_pool_size: true`.
If you are experiencing database timeouts, please check the "Optimizing
your PostgreSQL performance" section in the documentation. If you still
encounter issues after that, please open an issue on the tracker.
""")
{:error, "Repo.pool_size different than recommended value."}
else
:ok
end
end
defp check_repo_pool_size!(result), do: result
defp check_filter(filter, command_required) do
filters = Config.get([Pleroma.Upload, :filters])

View file

@ -9,12 +9,7 @@ defmodule Pleroma.Config.Holder do
def save_default do
default_config =
if System.get_env("RELEASE_NAME") do
release_config =
[:code.root_dir(), "releases", System.get_env("RELEASE_VSN"), "releases.exs"]
|> Path.join()
|> Pleroma.Config.Loader.read()
Pleroma.Config.Loader.merge(@config, release_config)
Pleroma.Config.Loader.merge(@config, release_defaults())
else
@config
end
@ -32,4 +27,16 @@ def default_config(group), do: Keyword.get(get_default(), group)
def default_config(group, key), do: get_in(get_default(), [group, key])
defp get_default, do: Pleroma.Config.get(:default_config)
@spec release_defaults() :: keyword()
def release_defaults do
[
pleroma: [
{:instance, [static_dir: "/var/lib/pleroma/static"]},
{Pleroma.Uploaders.Local, [uploads: "/var/lib/pleroma/uploads"]},
{:modules, [runtime_dir: "/var/lib/pleroma/modules"]},
{:release, true}
]
]
end
end

View file

@ -0,0 +1,50 @@
defmodule Pleroma.Config.ReleaseRuntimeProvider do
@moduledoc """
Imports `runtime.exs` and `{env}.exported_from_db.secret.exs` for elixir releases.
"""
@behaviour Config.Provider
@impl true
def init(opts), do: opts
@impl true
def load(config, _opts) do
with_defaults = Config.Reader.merge(config, Pleroma.Config.Holder.release_defaults())
config_path = System.get_env("PLEROMA_CONFIG_PATH") || "/etc/pleroma/config.exs"
with_runtime_config =
if File.exists?(config_path) do
runtime_config = Config.Reader.read!(config_path)
with_defaults
|> Config.Reader.merge(pleroma: [config_path: config_path])
|> Config.Reader.merge(runtime_config)
else
warning = [
IO.ANSI.red(),
IO.ANSI.bright(),
"!!! #{config_path} not found! Please ensure it exists and that PLEROMA_CONFIG_PATH is unset or points to an existing file",
IO.ANSI.reset()
]
IO.puts(warning)
with_defaults
end
exported_config_path =
config_path
|> Path.dirname()
|> Path.join("prod.exported_from_db.secret.exs")
with_exported =
if File.exists?(exported_config_path) do
exported_config = Config.Reader.read!(with_runtime_config)
Config.Reader.merge(with_runtime_config, exported_config)
else
with_runtime_config
end
with_exported
end
end

View file

@ -6,7 +6,7 @@ defmodule Pleroma.ConfigDB do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query, only: [select: 3]
import Ecto.Query, only: [select: 3, from: 2]
import Pleroma.Web.Gettext
alias __MODULE__
@ -41,8 +41,18 @@ def get_all_as_keyword do
end)
end
@spec get_all_by_group(atom() | String.t()) :: [t()]
def get_all_by_group(group) do
from(c in ConfigDB, where: c.group == ^group) |> Repo.all()
end
@spec get_by_group_and_key(atom() | String.t(), atom() | String.t()) :: t() | nil
def get_by_group_and_key(group, key) do
get_by_params(%{group: group, key: key})
end
@spec get_by_params(map()) :: ConfigDB.t() | nil
def get_by_params(params), do: Repo.get_by(ConfigDB, params)
def get_by_params(%{group: _, key: _} = params), do: Repo.get_by(ConfigDB, params)
@spec changeset(ConfigDB.t(), map()) :: Changeset.t()
def changeset(config, params \\ %{}) do

View file

@ -164,7 +164,7 @@ def digest_email(user) do
logo_path =
if is_nil(logo) do
Path.join(:code.priv_dir(:pleroma), "static/static/logo.png")
Path.join(:code.priv_dir(:pleroma), "static/static/logo.svg")
else
Path.join(Config.get([:instance, :static_dir]), logo)
end
@ -175,7 +175,7 @@ def digest_email(user) do
|> subject("Your digest from #{instance_name()}")
|> put_layout(false)
|> render_body("digest.html", html_data)
|> attachment(Swoosh.Attachment.new(logo_path, filename: "logo.png", type: :inline))
|> attachment(Swoosh.Attachment.new(logo_path, filename: "logo.svg", type: :inline))
end
end

View file

@ -1,769 +0,0 @@
# emoji-data.txt
# Date: 2019-01-15, 12:10:05 GMT
# © 2019 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 Data for UTS #51
# Version: 12.0
#
# For documentation and usage, see http://www.unicode.org/reports/tr51
#
# Format:
# <codepoint(s)> ; <property> # <comments>
# Note: there is no guarantee as to the structure of whitespace or comments
#
# Characters and sequences are listed in code point order. Users should be shown a more natural order.
# See the CLDR collation order for Emoji.
# ================================================
# All omitted code points have Emoji=No
# @missing: 0000..10FFFF ; Emoji ; No
0023 ; Emoji # 1.1 [1] (#) number sign
002A ; Emoji # 1.1 [1] (*) asterisk
0030..0039 ; Emoji # 1.1 [10] (0..9) digit zero..digit nine
00A9 ; Emoji # 1.1 [1] (©️) copyright
00AE ; Emoji # 1.1 [1] (®️) registered
203C ; Emoji # 1.1 [1] (‼️) double exclamation mark
2049 ; Emoji # 3.0 [1] (⁉️) exclamation question mark
2122 ; Emoji # 1.1 [1] (™️) trade mark
2139 ; Emoji # 3.0 [1] () information
2194..2199 ; Emoji # 1.1 [6] (↔️..↙️) left-right arrow..down-left arrow
21A9..21AA ; Emoji # 1.1 [2] (↩️..↪️) right arrow curving left..left arrow curving right
231A..231B ; Emoji # 1.1 [2] (⌚..⌛) watch..hourglass done
2328 ; Emoji # 1.1 [1] (⌨️) keyboard
23CF ; Emoji # 4.0 [1] (⏏️) eject button
23E9..23F3 ; Emoji # 6.0 [11] (⏩..⏳) fast-forward button..hourglass not done
23F8..23FA ; Emoji # 7.0 [3] (⏸️..⏺️) pause button..record button
24C2 ; Emoji # 1.1 [1] (Ⓜ️) circled M
25AA..25AB ; Emoji # 1.1 [2] (▪️..▫️) black small square..white small square
25B6 ; Emoji # 1.1 [1] (▶️) play button
25C0 ; Emoji # 1.1 [1] (◀️) reverse button
25FB..25FE ; Emoji # 3.2 [4] (◻️..◾) white medium square..black medium-small square
2600..2604 ; Emoji # 1.1 [5] (☀️..☄️) sun..comet
260E ; Emoji # 1.1 [1] (☎️) telephone
2611 ; Emoji # 1.1 [1] (☑️) check box with check
2614..2615 ; Emoji # 4.0 [2] (☔..☕) umbrella with rain drops..hot beverage
2618 ; Emoji # 4.1 [1] (☘️) shamrock
261D ; Emoji # 1.1 [1] (☝️) index pointing up
2620 ; Emoji # 1.1 [1] (☠️) skull and crossbones
2622..2623 ; Emoji # 1.1 [2] (☢️..☣️) radioactive..biohazard
2626 ; Emoji # 1.1 [1] (☦️) orthodox cross
262A ; Emoji # 1.1 [1] (☪️) star and crescent
262E..262F ; Emoji # 1.1 [2] (☮️..☯️) peace symbol..yin yang
2638..263A ; Emoji # 1.1 [3] (☸️..☺️) wheel of dharma..smiling face
2640 ; Emoji # 1.1 [1] (♀️) female sign
2642 ; Emoji # 1.1 [1] (♂️) male sign
2648..2653 ; Emoji # 1.1 [12] (♈..♓) Aries..Pisces
265F..2660 ; Emoji # 1.1 [2] (♟️..♠️) chess pawn..spade suit
2663 ; Emoji # 1.1 [1] (♣️) club suit
2665..2666 ; Emoji # 1.1 [2] (♥️..♦️) heart suit..diamond suit
2668 ; Emoji # 1.1 [1] (♨️) hot springs
267B ; Emoji # 3.2 [1] (♻️) recycling symbol
267E..267F ; Emoji # 4.1 [2] (♾️..♿) infinity..wheelchair symbol
2692..2697 ; Emoji # 4.1 [6] (⚒️..⚗️) hammer and pick..alembic
2699 ; Emoji # 4.1 [1] (⚙️) gear
269B..269C ; Emoji # 4.1 [2] (⚛️..⚜️) atom symbol..fleur-de-lis
26A0..26A1 ; Emoji # 4.0 [2] (⚠️..⚡) warning..high voltage
26AA..26AB ; Emoji # 4.1 [2] (⚪..⚫) white circle..black circle
26B0..26B1 ; Emoji # 4.1 [2] (⚰️..⚱️) coffin..funeral urn
26BD..26BE ; Emoji # 5.2 [2] (⚽..⚾) soccer ball..baseball
26C4..26C5 ; Emoji # 5.2 [2] (⛄..⛅) snowman without snow..sun behind cloud
26C8 ; Emoji # 5.2 [1] (⛈️) cloud with lightning and rain
26CE ; Emoji # 6.0 [1] (⛎) Ophiuchus
26CF ; Emoji # 5.2 [1] (⛏️) pick
26D1 ; Emoji # 5.2 [1] (⛑️) rescue workers helmet
26D3..26D4 ; Emoji # 5.2 [2] (⛓️..⛔) chains..no entry
26E9..26EA ; Emoji # 5.2 [2] (⛩️..⛪) shinto shrine..church
26F0..26F5 ; Emoji # 5.2 [6] (⛰️..⛵) mountain..sailboat
26F7..26FA ; Emoji # 5.2 [4] (⛷️..⛺) skier..tent
26FD ; Emoji # 5.2 [1] (⛽) fuel pump
2702 ; Emoji # 1.1 [1] (✂️) scissors
2705 ; Emoji # 6.0 [1] (✅) check mark button
2708..2709 ; Emoji # 1.1 [2] (✈️..✉️) airplane..envelope
270A..270B ; Emoji # 6.0 [2] (✊..✋) raised fist..raised hand
270C..270D ; Emoji # 1.1 [2] (✌️..✍️) victory hand..writing hand
270F ; Emoji # 1.1 [1] (✏️) pencil
2712 ; Emoji # 1.1 [1] (✒️) black nib
2714 ; Emoji # 1.1 [1] (✔️) check mark
2716 ; Emoji # 1.1 [1] (✖️) multiplication sign
271D ; Emoji # 1.1 [1] (✝️) latin cross
2721 ; Emoji # 1.1 [1] (✡️) star of David
2728 ; Emoji # 6.0 [1] (✨) sparkles
2733..2734 ; Emoji # 1.1 [2] (✳️..✴️) eight-spoked asterisk..eight-pointed star
2744 ; Emoji # 1.1 [1] (❄️) snowflake
2747 ; Emoji # 1.1 [1] (❇️) sparkle
274C ; Emoji # 6.0 [1] (❌) cross mark
274E ; Emoji # 6.0 [1] (❎) cross mark button
2753..2755 ; Emoji # 6.0 [3] (❓..❕) question mark..white exclamation mark
2757 ; Emoji # 5.2 [1] (❗) exclamation mark
2763..2764 ; Emoji # 1.1 [2] (❣️..❤️) heart exclamation..red heart
2795..2797 ; Emoji # 6.0 [3] (..➗) plus sign..division sign
27A1 ; Emoji # 1.1 [1] (➡️) right arrow
27B0 ; Emoji # 6.0 [1] (➰) curly loop
27BF ; Emoji # 6.0 [1] (➿) double curly loop
2934..2935 ; Emoji # 3.2 [2] (⤴️..⤵️) right arrow curving up..right arrow curving down
2B05..2B07 ; Emoji # 4.0 [3] (⬅️..⬇️) left arrow..down arrow
2B1B..2B1C ; Emoji # 5.1 [2] (⬛..⬜) black large square..white large square
2B50 ; Emoji # 5.1 [1] (⭐) star
2B55 ; Emoji # 5.2 [1] (⭕) hollow red circle
3030 ; Emoji # 1.1 [1] (〰️) wavy dash
303D ; Emoji # 3.2 [1] (〽️) part alternation mark
3297 ; Emoji # 1.1 [1] (㊗️) Japanese “congratulations” button
3299 ; Emoji # 1.1 [1] (㊙️) Japanese “secret” button
1F004 ; Emoji # 5.1 [1] (🀄) mahjong red dragon
1F0CF ; Emoji # 6.0 [1] (🃏) joker
1F170..1F171 ; Emoji # 6.0 [2] (🅰️..🅱️) A button (blood type)..B button (blood type)
1F17E ; Emoji # 6.0 [1] (🅾️) O button (blood type)
1F17F ; Emoji # 5.2 [1] (🅿️) P button
1F18E ; Emoji # 6.0 [1] (🆎) AB button (blood type)
1F191..1F19A ; Emoji # 6.0 [10] (🆑..🆚) CL button..VS button
1F1E6..1F1FF ; Emoji # 6.0 [26] (🇦..🇿) regional indicator symbol letter a..regional indicator symbol letter z
1F201..1F202 ; Emoji # 6.0 [2] (🈁..🈂️) Japanese “here” button..Japanese “service charge” button
1F21A ; Emoji # 5.2 [1] (🈚) Japanese “free of charge” button
1F22F ; Emoji # 5.2 [1] (🈯) Japanese “reserved” button
1F232..1F23A ; Emoji # 6.0 [9] (🈲..🈺) Japanese “prohibited” button..Japanese “open for business” button
1F250..1F251 ; Emoji # 6.0 [2] (🉐..🉑) Japanese “bargain” button..Japanese “acceptable” button
1F300..1F320 ; Emoji # 6.0 [33] (🌀..🌠) cyclone..shooting star
1F321 ; Emoji # 7.0 [1] (🌡️) thermometer
1F324..1F32C ; Emoji # 7.0 [9] (🌤️..🌬️) sun behind small cloud..wind face
1F32D..1F32F ; Emoji # 8.0 [3] (🌭..🌯) hot dog..burrito
1F330..1F335 ; Emoji # 6.0 [6] (🌰..🌵) chestnut..cactus
1F336 ; Emoji # 7.0 [1] (🌶️) hot pepper
1F337..1F37C ; Emoji # 6.0 [70] (🌷..🍼) tulip..baby bottle
1F37D ; Emoji # 7.0 [1] (🍽️) fork and knife with plate
1F37E..1F37F ; Emoji # 8.0 [2] (🍾..🍿) bottle with popping cork..popcorn
1F380..1F393 ; Emoji # 6.0 [20] (🎀..🎓) ribbon..graduation cap
1F396..1F397 ; Emoji # 7.0 [2] (🎖️..🎗️) military medal..reminder ribbon
1F399..1F39B ; Emoji # 7.0 [3] (🎙️..🎛️) studio microphone..control knobs
1F39E..1F39F ; Emoji # 7.0 [2] (🎞️..🎟️) film frames..admission tickets
1F3A0..1F3C4 ; Emoji # 6.0 [37] (🎠..🏄) carousel horse..person surfing
1F3C5 ; Emoji # 7.0 [1] (🏅) sports medal
1F3C6..1F3CA ; Emoji # 6.0 [5] (🏆..🏊) trophy..person swimming
1F3CB..1F3CE ; Emoji # 7.0 [4] (🏋️..🏎️) person lifting weights..racing car
1F3CF..1F3D3 ; Emoji # 8.0 [5] (🏏..🏓) cricket game..ping pong
1F3D4..1F3DF ; Emoji # 7.0 [12] (🏔️..🏟️) snow-capped mountain..stadium
1F3E0..1F3F0 ; Emoji # 6.0 [17] (🏠..🏰) house..castle
1F3F3..1F3F5 ; Emoji # 7.0 [3] (🏳️..🏵️) white flag..rosette
1F3F7 ; Emoji # 7.0 [1] (🏷️) label
1F3F8..1F3FF ; Emoji # 8.0 [8] (🏸..🏿) badminton..dark skin tone
1F400..1F43E ; Emoji # 6.0 [63] (🐀..🐾) rat..paw prints
1F43F ; Emoji # 7.0 [1] (🐿️) chipmunk
1F440 ; Emoji # 6.0 [1] (👀) eyes
1F441 ; Emoji # 7.0 [1] (👁️) eye
1F442..1F4F7 ; Emoji # 6.0[182] (👂..📷) ear..camera
1F4F8 ; Emoji # 7.0 [1] (📸) camera with flash
1F4F9..1F4FC ; Emoji # 6.0 [4] (📹..📼) video camera..videocassette
1F4FD ; Emoji # 7.0 [1] (📽️) film projector
1F4FF ; Emoji # 8.0 [1] (📿) prayer beads
1F500..1F53D ; Emoji # 6.0 [62] (🔀..🔽) shuffle tracks button..downwards button
1F549..1F54A ; Emoji # 7.0 [2] (🕉️..🕊️) om..dove
1F54B..1F54E ; Emoji # 8.0 [4] (🕋..🕎) kaaba..menorah
1F550..1F567 ; Emoji # 6.0 [24] (🕐..🕧) one oclock..twelve-thirty
1F56F..1F570 ; Emoji # 7.0 [2] (🕯️..🕰️) candle..mantelpiece clock
1F573..1F579 ; Emoji # 7.0 [7] (🕳️..🕹️) hole..joystick
1F57A ; Emoji # 9.0 [1] (🕺) man dancing
1F587 ; Emoji # 7.0 [1] (🖇️) linked paperclips
1F58A..1F58D ; Emoji # 7.0 [4] (🖊️..🖍️) pen..crayon
1F590 ; Emoji # 7.0 [1] (🖐️) hand with fingers splayed
1F595..1F596 ; Emoji # 7.0 [2] (🖕..🖖) middle finger..vulcan salute
1F5A4 ; Emoji # 9.0 [1] (🖤) black heart
1F5A5 ; Emoji # 7.0 [1] (🖥️) desktop computer
1F5A8 ; Emoji # 7.0 [1] (🖨️) printer
1F5B1..1F5B2 ; Emoji # 7.0 [2] (🖱️..🖲️) computer mouse..trackball
1F5BC ; Emoji # 7.0 [1] (🖼️) framed picture
1F5C2..1F5C4 ; Emoji # 7.0 [3] (🗂️..🗄️) card index dividers..file cabinet
1F5D1..1F5D3 ; Emoji # 7.0 [3] (🗑️..🗓️) wastebasket..spiral calendar
1F5DC..1F5DE ; Emoji # 7.0 [3] (🗜️..🗞️) clamp..rolled-up newspaper
1F5E1 ; Emoji # 7.0 [1] (🗡️) dagger
1F5E3 ; Emoji # 7.0 [1] (🗣️) speaking head
1F5E8 ; Emoji # 7.0 [1] (🗨️) left speech bubble
1F5EF ; Emoji # 7.0 [1] (🗯️) right anger bubble
1F5F3 ; Emoji # 7.0 [1] (🗳️) ballot box with ballot
1F5FA ; Emoji # 7.0 [1] (🗺️) world map
1F5FB..1F5FF ; Emoji # 6.0 [5] (🗻..🗿) mount fuji..moai
1F600 ; Emoji # 6.1 [1] (😀) grinning face
1F601..1F610 ; Emoji # 6.0 [16] (😁..😐) beaming face with smiling eyes..neutral face
1F611 ; Emoji # 6.1 [1] (😑) expressionless face
1F612..1F614 ; Emoji # 6.0 [3] (😒..😔) unamused face..pensive face
1F615 ; Emoji # 6.1 [1] (😕) confused face
1F616 ; Emoji # 6.0 [1] (😖) confounded face
1F617 ; Emoji # 6.1 [1] (😗) kissing face
1F618 ; Emoji # 6.0 [1] (😘) face blowing a kiss
1F619 ; Emoji # 6.1 [1] (😙) kissing face with smiling eyes
1F61A ; Emoji # 6.0 [1] (😚) kissing face with closed eyes
1F61B ; Emoji # 6.1 [1] (😛) face with tongue
1F61C..1F61E ; Emoji # 6.0 [3] (😜..😞) winking face with tongue..disappointed face
1F61F ; Emoji # 6.1 [1] (😟) worried face
1F620..1F625 ; Emoji # 6.0 [6] (😠..😥) angry face..sad but relieved face
1F626..1F627 ; Emoji # 6.1 [2] (😦..😧) frowning face with open mouth..anguished face
1F628..1F62B ; Emoji # 6.0 [4] (😨..😫) fearful face..tired face
1F62C ; Emoji # 6.1 [1] (😬) grimacing face
1F62D ; Emoji # 6.0 [1] (😭) loudly crying face
1F62E..1F62F ; Emoji # 6.1 [2] (😮..😯) face with open mouth..hushed face
1F630..1F633 ; Emoji # 6.0 [4] (😰..😳) anxious face with sweat..flushed face
1F634 ; Emoji # 6.1 [1] (😴) sleeping face
1F635..1F640 ; Emoji # 6.0 [12] (😵..🙀) dizzy face..weary cat
1F641..1F642 ; Emoji # 7.0 [2] (🙁..🙂) slightly frowning face..slightly smiling face
1F643..1F644 ; Emoji # 8.0 [2] (🙃..🙄) upside-down face..face with rolling eyes
1F645..1F64F ; Emoji # 6.0 [11] (🙅..🙏) person gesturing NO..folded hands
1F680..1F6C5 ; Emoji # 6.0 [70] (🚀..🛅) rocket..left luggage
1F6CB..1F6CF ; Emoji # 7.0 [5] (🛋️..🛏️) couch and lamp..bed
1F6D0 ; Emoji # 8.0 [1] (🛐) place of worship
1F6D1..1F6D2 ; Emoji # 9.0 [2] (🛑..🛒) stop sign..shopping cart
1F6D5 ; Emoji # 12.0 [1] (🛕) hindu temple
1F6E0..1F6E5 ; Emoji # 7.0 [6] (🛠️..🛥️) hammer and wrench..motor boat
1F6E9 ; Emoji # 7.0 [1] (🛩️) small airplane
1F6EB..1F6EC ; Emoji # 7.0 [2] (🛫..🛬) airplane departure..airplane arrival
1F6F0 ; Emoji # 7.0 [1] (🛰️) satellite
1F6F3 ; Emoji # 7.0 [1] (🛳️) passenger ship
1F6F4..1F6F6 ; Emoji # 9.0 [3] (🛴..🛶) kick scooter..canoe
1F6F7..1F6F8 ; Emoji # 10.0 [2] (🛷..🛸) sled..flying saucer
1F6F9 ; Emoji # 11.0 [1] (🛹) skateboard
1F6FA ; Emoji # 12.0 [1] (🛺) auto rickshaw
1F7E0..1F7EB ; Emoji # 12.0 [12] (🟠..🟫) orange circle..brown square
1F90D..1F90F ; Emoji # 12.0 [3] (🤍..🤏) white heart..pinching hand
1F910..1F918 ; Emoji # 8.0 [9] (🤐..🤘) zipper-mouth face..sign of the horns
1F919..1F91E ; Emoji # 9.0 [6] (🤙..🤞) call me hand..crossed fingers
1F91F ; Emoji # 10.0 [1] (🤟) love-you gesture
1F920..1F927 ; Emoji # 9.0 [8] (🤠..🤧) cowboy hat face..sneezing face
1F928..1F92F ; Emoji # 10.0 [8] (🤨..🤯) face with raised eyebrow..exploding head
1F930 ; Emoji # 9.0 [1] (🤰) pregnant woman
1F931..1F932 ; Emoji # 10.0 [2] (🤱..🤲) breast-feeding..palms up together
1F933..1F93A ; Emoji # 9.0 [8] (🤳..🤺) selfie..person fencing
1F93C..1F93E ; Emoji # 9.0 [3] (🤼..🤾) people wrestling..person playing handball
1F93F ; Emoji # 12.0 [1] (🤿) diving mask
1F940..1F945 ; Emoji # 9.0 [6] (🥀..🥅) wilted flower..goal net
1F947..1F94B ; Emoji # 9.0 [5] (🥇..🥋) 1st place medal..martial arts uniform
1F94C ; Emoji # 10.0 [1] (🥌) curling stone
1F94D..1F94F ; Emoji # 11.0 [3] (🥍..🥏) lacrosse..flying disc
1F950..1F95E ; Emoji # 9.0 [15] (🥐..🥞) croissant..pancakes
1F95F..1F96B ; Emoji # 10.0 [13] (🥟..🥫) dumpling..canned food
1F96C..1F970 ; Emoji # 11.0 [5] (🥬..🥰) leafy green..smiling face with hearts
1F971 ; Emoji # 12.0 [1] (🥱) yawning face
1F973..1F976 ; Emoji # 11.0 [4] (🥳..🥶) partying face..cold face
1F97A ; Emoji # 11.0 [1] (🥺) pleading face
1F97B ; Emoji # 12.0 [1] (🥻) sari
1F97C..1F97F ; Emoji # 11.0 [4] (🥼..🥿) lab coat..flat shoe
1F980..1F984 ; Emoji # 8.0 [5] (🦀..🦄) crab..unicorn
1F985..1F991 ; Emoji # 9.0 [13] (🦅..🦑) eagle..squid
1F992..1F997 ; Emoji # 10.0 [6] (🦒..🦗) giraffe..cricket
1F998..1F9A2 ; Emoji # 11.0 [11] (🦘..🦢) kangaroo..swan
1F9A5..1F9AA ; Emoji # 12.0 [6] (🦥..🦪) sloth..oyster
1F9AE..1F9AF ; Emoji # 12.0 [2] (🦮..🦯) guide dog..probing cane
1F9B0..1F9B9 ; Emoji # 11.0 [10] (🦰..🦹) red hair..supervillain
1F9BA..1F9BF ; Emoji # 12.0 [6] (🦺..🦿) safety vest..mechanical leg
1F9C0 ; Emoji # 8.0 [1] (🧀) cheese wedge
1F9C1..1F9C2 ; Emoji # 11.0 [2] (🧁..🧂) cupcake..salt
1F9C3..1F9CA ; Emoji # 12.0 [8] (🧃..🧊) beverage box..ice cube
1F9CD..1F9CF ; Emoji # 12.0 [3] (🧍..🧏) person standing..deaf person
1F9D0..1F9E6 ; Emoji # 10.0 [23] (🧐..🧦) face with monocle..socks
1F9E7..1F9FF ; Emoji # 11.0 [25] (🧧..🧿) red envelope..nazar amulet
1FA70..1FA73 ; Emoji # 12.0 [4] (🩰..🩳) ballet shoes..shorts
1FA78..1FA7A ; Emoji # 12.0 [3] (🩸..🩺) drop of blood..stethoscope
1FA80..1FA82 ; Emoji # 12.0 [3] (🪀..🪂) yo-yo..parachute
1FA90..1FA95 ; Emoji # 12.0 [6] (🪐..🪕) ringed planet..banjo
# Total elements: 1311
# ================================================
# All omitted code points have Emoji_Presentation=No
# @missing: 0000..10FFFF ; Emoji_Presentation ; No
231A..231B ; Emoji_Presentation # 1.1 [2] (⌚..⌛) watch..hourglass done
23E9..23EC ; Emoji_Presentation # 6.0 [4] (⏩..⏬) fast-forward button..fast down button
23F0 ; Emoji_Presentation # 6.0 [1] (⏰) alarm clock
23F3 ; Emoji_Presentation # 6.0 [1] (⏳) hourglass not done
25FD..25FE ; Emoji_Presentation # 3.2 [2] (◽..◾) white medium-small square..black medium-small square
2614..2615 ; Emoji_Presentation # 4.0 [2] (☔..☕) umbrella with rain drops..hot beverage
2648..2653 ; Emoji_Presentation # 1.1 [12] (♈..♓) Aries..Pisces
267F ; Emoji_Presentation # 4.1 [1] (♿) wheelchair symbol
2693 ; Emoji_Presentation # 4.1 [1] (⚓) anchor
26A1 ; Emoji_Presentation # 4.0 [1] (⚡) high voltage
26AA..26AB ; Emoji_Presentation # 4.1 [2] (⚪..⚫) white circle..black circle
26BD..26BE ; Emoji_Presentation # 5.2 [2] (⚽..⚾) soccer ball..baseball
26C4..26C5 ; Emoji_Presentation # 5.2 [2] (⛄..⛅) snowman without snow..sun behind cloud
26CE ; Emoji_Presentation # 6.0 [1] (⛎) Ophiuchus
26D4 ; Emoji_Presentation # 5.2 [1] (⛔) no entry
26EA ; Emoji_Presentation # 5.2 [1] (⛪) church
26F2..26F3 ; Emoji_Presentation # 5.2 [2] (⛲..⛳) fountain..flag in hole
26F5 ; Emoji_Presentation # 5.2 [1] (⛵) sailboat
26FA ; Emoji_Presentation # 5.2 [1] (⛺) tent
26FD ; Emoji_Presentation # 5.2 [1] (⛽) fuel pump
2705 ; Emoji_Presentation # 6.0 [1] (✅) check mark button
270A..270B ; Emoji_Presentation # 6.0 [2] (✊..✋) raised fist..raised hand
2728 ; Emoji_Presentation # 6.0 [1] (✨) sparkles
274C ; Emoji_Presentation # 6.0 [1] (❌) cross mark
274E ; Emoji_Presentation # 6.0 [1] (❎) cross mark button
2753..2755 ; Emoji_Presentation # 6.0 [3] (❓..❕) question mark..white exclamation mark
2757 ; Emoji_Presentation # 5.2 [1] (❗) exclamation mark
2795..2797 ; Emoji_Presentation # 6.0 [3] (..➗) plus sign..division sign
27B0 ; Emoji_Presentation # 6.0 [1] (➰) curly loop
27BF ; Emoji_Presentation # 6.0 [1] (➿) double curly loop
2B1B..2B1C ; Emoji_Presentation # 5.1 [2] (⬛..⬜) black large square..white large square
2B50 ; Emoji_Presentation # 5.1 [1] (⭐) star
2B55 ; Emoji_Presentation # 5.2 [1] (⭕) hollow red circle
1F004 ; Emoji_Presentation # 5.1 [1] (🀄) mahjong red dragon
1F0CF ; Emoji_Presentation # 6.0 [1] (🃏) joker
1F18E ; Emoji_Presentation # 6.0 [1] (🆎) AB button (blood type)
1F191..1F19A ; Emoji_Presentation # 6.0 [10] (🆑..🆚) CL button..VS button
1F1E6..1F1FF ; Emoji_Presentation # 6.0 [26] (🇦..🇿) regional indicator symbol letter a..regional indicator symbol letter z
1F201 ; Emoji_Presentation # 6.0 [1] (🈁) Japanese “here” button
1F21A ; Emoji_Presentation # 5.2 [1] (🈚) Japanese “free of charge” button
1F22F ; Emoji_Presentation # 5.2 [1] (🈯) Japanese “reserved” button
1F232..1F236 ; Emoji_Presentation # 6.0 [5] (🈲..🈶) Japanese “prohibited” button..Japanese “not free of charge” button
1F238..1F23A ; Emoji_Presentation # 6.0 [3] (🈸..🈺) Japanese “application” button..Japanese “open for business” button
1F250..1F251 ; Emoji_Presentation # 6.0 [2] (🉐..🉑) Japanese “bargain” button..Japanese “acceptable” button
1F300..1F320 ; Emoji_Presentation # 6.0 [33] (🌀..🌠) cyclone..shooting star
1F32D..1F32F ; Emoji_Presentation # 8.0 [3] (🌭..🌯) hot dog..burrito
1F330..1F335 ; Emoji_Presentation # 6.0 [6] (🌰..🌵) chestnut..cactus
1F337..1F37C ; Emoji_Presentation # 6.0 [70] (🌷..🍼) tulip..baby bottle
1F37E..1F37F ; Emoji_Presentation # 8.0 [2] (🍾..🍿) bottle with popping cork..popcorn
1F380..1F393 ; Emoji_Presentation # 6.0 [20] (🎀..🎓) ribbon..graduation cap
1F3A0..1F3C4 ; Emoji_Presentation # 6.0 [37] (🎠..🏄) carousel horse..person surfing
1F3C5 ; Emoji_Presentation # 7.0 [1] (🏅) sports medal
1F3C6..1F3CA ; Emoji_Presentation # 6.0 [5] (🏆..🏊) trophy..person swimming
1F3CF..1F3D3 ; Emoji_Presentation # 8.0 [5] (🏏..🏓) cricket game..ping pong
1F3E0..1F3F0 ; Emoji_Presentation # 6.0 [17] (🏠..🏰) house..castle
1F3F4 ; Emoji_Presentation # 7.0 [1] (🏴) black flag
1F3F8..1F3FF ; Emoji_Presentation # 8.0 [8] (🏸..🏿) badminton..dark skin tone
1F400..1F43E ; Emoji_Presentation # 6.0 [63] (🐀..🐾) rat..paw prints
1F440 ; Emoji_Presentation # 6.0 [1] (👀) eyes
1F442..1F4F7 ; Emoji_Presentation # 6.0[182] (👂..📷) ear..camera
1F4F8 ; Emoji_Presentation # 7.0 [1] (📸) camera with flash
1F4F9..1F4FC ; Emoji_Presentation # 6.0 [4] (📹..📼) video camera..videocassette
1F4FF ; Emoji_Presentation # 8.0 [1] (📿) prayer beads
1F500..1F53D ; Emoji_Presentation # 6.0 [62] (🔀..🔽) shuffle tracks button..downwards button
1F54B..1F54E ; Emoji_Presentation # 8.0 [4] (🕋..🕎) kaaba..menorah
1F550..1F567 ; Emoji_Presentation # 6.0 [24] (🕐..🕧) one oclock..twelve-thirty
1F57A ; Emoji_Presentation # 9.0 [1] (🕺) man dancing
1F595..1F596 ; Emoji_Presentation # 7.0 [2] (🖕..🖖) middle finger..vulcan salute
1F5A4 ; Emoji_Presentation # 9.0 [1] (🖤) black heart
1F5FB..1F5FF ; Emoji_Presentation # 6.0 [5] (🗻..🗿) mount fuji..moai
1F600 ; Emoji_Presentation # 6.1 [1] (😀) grinning face
1F601..1F610 ; Emoji_Presentation # 6.0 [16] (😁..😐) beaming face with smiling eyes..neutral face
1F611 ; Emoji_Presentation # 6.1 [1] (😑) expressionless face
1F612..1F614 ; Emoji_Presentation # 6.0 [3] (😒..😔) unamused face..pensive face
1F615 ; Emoji_Presentation # 6.1 [1] (😕) confused face
1F616 ; Emoji_Presentation # 6.0 [1] (😖) confounded face
1F617 ; Emoji_Presentation # 6.1 [1] (😗) kissing face
1F618 ; Emoji_Presentation # 6.0 [1] (😘) face blowing a kiss
1F619 ; Emoji_Presentation # 6.1 [1] (😙) kissing face with smiling eyes
1F61A ; Emoji_Presentation # 6.0 [1] (😚) kissing face with closed eyes
1F61B ; Emoji_Presentation # 6.1 [1] (😛) face with tongue
1F61C..1F61E ; Emoji_Presentation # 6.0 [3] (😜..😞) winking face with tongue..disappointed face
1F61F ; Emoji_Presentation # 6.1 [1] (😟) worried face
1F620..1F625 ; Emoji_Presentation # 6.0 [6] (😠..😥) angry face..sad but relieved face
1F626..1F627 ; Emoji_Presentation # 6.1 [2] (😦..😧) frowning face with open mouth..anguished face
1F628..1F62B ; Emoji_Presentation # 6.0 [4] (😨..😫) fearful face..tired face
1F62C ; Emoji_Presentation # 6.1 [1] (😬) grimacing face
1F62D ; Emoji_Presentation # 6.0 [1] (😭) loudly crying face
1F62E..1F62F ; Emoji_Presentation # 6.1 [2] (😮..😯) face with open mouth..hushed face
1F630..1F633 ; Emoji_Presentation # 6.0 [4] (😰..😳) anxious face with sweat..flushed face
1F634 ; Emoji_Presentation # 6.1 [1] (😴) sleeping face
1F635..1F640 ; Emoji_Presentation # 6.0 [12] (😵..🙀) dizzy face..weary cat
1F641..1F642 ; Emoji_Presentation # 7.0 [2] (🙁..🙂) slightly frowning face..slightly smiling face
1F643..1F644 ; Emoji_Presentation # 8.0 [2] (🙃..🙄) upside-down face..face with rolling eyes
1F645..1F64F ; Emoji_Presentation # 6.0 [11] (🙅..🙏) person gesturing NO..folded hands
1F680..1F6C5 ; Emoji_Presentation # 6.0 [70] (🚀..🛅) rocket..left luggage
1F6CC ; Emoji_Presentation # 7.0 [1] (🛌) person in bed
1F6D0 ; Emoji_Presentation # 8.0 [1] (🛐) place of worship
1F6D1..1F6D2 ; Emoji_Presentation # 9.0 [2] (🛑..🛒) stop sign..shopping cart
1F6D5 ; Emoji_Presentation # 12.0 [1] (🛕) hindu temple
1F6EB..1F6EC ; Emoji_Presentation # 7.0 [2] (🛫..🛬) airplane departure..airplane arrival
1F6F4..1F6F6 ; Emoji_Presentation # 9.0 [3] (🛴..🛶) kick scooter..canoe
1F6F7..1F6F8 ; Emoji_Presentation # 10.0 [2] (🛷..🛸) sled..flying saucer
1F6F9 ; Emoji_Presentation # 11.0 [1] (🛹) skateboard
1F6FA ; Emoji_Presentation # 12.0 [1] (🛺) auto rickshaw
1F7E0..1F7EB ; Emoji_Presentation # 12.0 [12] (🟠..🟫) orange circle..brown square
1F90D..1F90F ; Emoji_Presentation # 12.0 [3] (🤍..🤏) white heart..pinching hand
1F910..1F918 ; Emoji_Presentation # 8.0 [9] (🤐..🤘) zipper-mouth face..sign of the horns
1F919..1F91E ; Emoji_Presentation # 9.0 [6] (🤙..🤞) call me hand..crossed fingers
1F91F ; Emoji_Presentation # 10.0 [1] (🤟) love-you gesture
1F920..1F927 ; Emoji_Presentation # 9.0 [8] (🤠..🤧) cowboy hat face..sneezing face
1F928..1F92F ; Emoji_Presentation # 10.0 [8] (🤨..🤯) face with raised eyebrow..exploding head
1F930 ; Emoji_Presentation # 9.0 [1] (🤰) pregnant woman
1F931..1F932 ; Emoji_Presentation # 10.0 [2] (🤱..🤲) breast-feeding..palms up together
1F933..1F93A ; Emoji_Presentation # 9.0 [8] (🤳..🤺) selfie..person fencing
1F93C..1F93E ; Emoji_Presentation # 9.0 [3] (🤼..🤾) people wrestling..person playing handball
1F93F ; Emoji_Presentation # 12.0 [1] (🤿) diving mask
1F940..1F945 ; Emoji_Presentation # 9.0 [6] (🥀..🥅) wilted flower..goal net
1F947..1F94B ; Emoji_Presentation # 9.0 [5] (🥇..🥋) 1st place medal..martial arts uniform
1F94C ; Emoji_Presentation # 10.0 [1] (🥌) curling stone
1F94D..1F94F ; Emoji_Presentation # 11.0 [3] (🥍..🥏) lacrosse..flying disc
1F950..1F95E ; Emoji_Presentation # 9.0 [15] (🥐..🥞) croissant..pancakes
1F95F..1F96B ; Emoji_Presentation # 10.0 [13] (🥟..🥫) dumpling..canned food
1F96C..1F970 ; Emoji_Presentation # 11.0 [5] (🥬..🥰) leafy green..smiling face with hearts
1F971 ; Emoji_Presentation # 12.0 [1] (🥱) yawning face
1F973..1F976 ; Emoji_Presentation # 11.0 [4] (🥳..🥶) partying face..cold face
1F97A ; Emoji_Presentation # 11.0 [1] (🥺) pleading face
1F97B ; Emoji_Presentation # 12.0 [1] (🥻) sari
1F97C..1F97F ; Emoji_Presentation # 11.0 [4] (🥼..🥿) lab coat..flat shoe
1F980..1F984 ; Emoji_Presentation # 8.0 [5] (🦀..🦄) crab..unicorn
1F985..1F991 ; Emoji_Presentation # 9.0 [13] (🦅..🦑) eagle..squid
1F992..1F997 ; Emoji_Presentation # 10.0 [6] (🦒..🦗) giraffe..cricket
1F998..1F9A2 ; Emoji_Presentation # 11.0 [11] (🦘..🦢) kangaroo..swan
1F9A5..1F9AA ; Emoji_Presentation # 12.0 [6] (🦥..🦪) sloth..oyster
1F9AE..1F9AF ; Emoji_Presentation # 12.0 [2] (🦮..🦯) guide dog..probing cane
1F9B0..1F9B9 ; Emoji_Presentation # 11.0 [10] (🦰..🦹) red hair..supervillain
1F9BA..1F9BF ; Emoji_Presentation # 12.0 [6] (🦺..🦿) safety vest..mechanical leg
1F9C0 ; Emoji_Presentation # 8.0 [1] (🧀) cheese wedge
1F9C1..1F9C2 ; Emoji_Presentation # 11.0 [2] (🧁..🧂) cupcake..salt
1F9C3..1F9CA ; Emoji_Presentation # 12.0 [8] (🧃..🧊) beverage box..ice cube
1F9CD..1F9CF ; Emoji_Presentation # 12.0 [3] (🧍..🧏) person standing..deaf person
1F9D0..1F9E6 ; Emoji_Presentation # 10.0 [23] (🧐..🧦) face with monocle..socks
1F9E7..1F9FF ; Emoji_Presentation # 11.0 [25] (🧧..🧿) red envelope..nazar amulet
1FA70..1FA73 ; Emoji_Presentation # 12.0 [4] (🩰..🩳) ballet shoes..shorts
1FA78..1FA7A ; Emoji_Presentation # 12.0 [3] (🩸..🩺) drop of blood..stethoscope
1FA80..1FA82 ; Emoji_Presentation # 12.0 [3] (🪀..🪂) yo-yo..parachute
1FA90..1FA95 ; Emoji_Presentation # 12.0 [6] (🪐..🪕) ringed planet..banjo
# Total elements: 1093
# ================================================
# All omitted code points have Emoji_Modifier=No
# @missing: 0000..10FFFF ; Emoji_Modifier ; No
1F3FB..1F3FF ; Emoji_Modifier # 8.0 [5] (🏻..🏿) light skin tone..dark skin tone
# Total elements: 5
# ================================================
# All omitted code points have Emoji_Modifier_Base=No
# @missing: 0000..10FFFF ; Emoji_Modifier_Base ; No
261D ; Emoji_Modifier_Base # 1.1 [1] (☝️) index pointing up
26F9 ; Emoji_Modifier_Base # 5.2 [1] (⛹️) person bouncing ball
270A..270B ; Emoji_Modifier_Base # 6.0 [2] (✊..✋) raised fist..raised hand
270C..270D ; Emoji_Modifier_Base # 1.1 [2] (✌️..✍️) victory hand..writing hand
1F385 ; Emoji_Modifier_Base # 6.0 [1] (🎅) Santa Claus
1F3C2..1F3C4 ; Emoji_Modifier_Base # 6.0 [3] (🏂..🏄) snowboarder..person surfing
1F3C7 ; Emoji_Modifier_Base # 6.0 [1] (🏇) horse racing
1F3CA ; Emoji_Modifier_Base # 6.0 [1] (🏊) person swimming
1F3CB..1F3CC ; Emoji_Modifier_Base # 7.0 [2] (🏋️..🏌️) person lifting weights..person golfing
1F442..1F443 ; Emoji_Modifier_Base # 6.0 [2] (👂..👃) ear..nose
1F446..1F450 ; Emoji_Modifier_Base # 6.0 [11] (👆..👐) backhand index pointing up..open hands
1F466..1F478 ; Emoji_Modifier_Base # 6.0 [19] (👦..👸) boy..princess
1F47C ; Emoji_Modifier_Base # 6.0 [1] (👼) baby angel
1F481..1F483 ; Emoji_Modifier_Base # 6.0 [3] (💁..💃) person tipping hand..woman dancing
1F485..1F487 ; Emoji_Modifier_Base # 6.0 [3] (💅..💇) nail polish..person getting haircut
1F48F ; Emoji_Modifier_Base # 6.0 [1] (💏) kiss
1F491 ; Emoji_Modifier_Base # 6.0 [1] (💑) couple with heart
1F4AA ; Emoji_Modifier_Base # 6.0 [1] (💪) flexed biceps
1F574..1F575 ; Emoji_Modifier_Base # 7.0 [2] (🕴️..🕵️) man in suit levitating..detective
1F57A ; Emoji_Modifier_Base # 9.0 [1] (🕺) man dancing
1F590 ; Emoji_Modifier_Base # 7.0 [1] (🖐️) hand with fingers splayed
1F595..1F596 ; Emoji_Modifier_Base # 7.0 [2] (🖕..🖖) middle finger..vulcan salute
1F645..1F647 ; Emoji_Modifier_Base # 6.0 [3] (🙅..🙇) person gesturing NO..person bowing
1F64B..1F64F ; Emoji_Modifier_Base # 6.0 [5] (🙋..🙏) person raising hand..folded hands
1F6A3 ; Emoji_Modifier_Base # 6.0 [1] (🚣) person rowing boat
1F6B4..1F6B6 ; Emoji_Modifier_Base # 6.0 [3] (🚴..🚶) person biking..person walking
1F6C0 ; Emoji_Modifier_Base # 6.0 [1] (🛀) person taking bath
1F6CC ; Emoji_Modifier_Base # 7.0 [1] (🛌) person in bed
1F90F ; Emoji_Modifier_Base # 12.0 [1] (🤏) pinching hand
1F918 ; Emoji_Modifier_Base # 8.0 [1] (🤘) sign of the horns
1F919..1F91E ; Emoji_Modifier_Base # 9.0 [6] (🤙..🤞) call me hand..crossed fingers
1F91F ; Emoji_Modifier_Base # 10.0 [1] (🤟) love-you gesture
1F926 ; Emoji_Modifier_Base # 9.0 [1] (🤦) person facepalming
1F930 ; Emoji_Modifier_Base # 9.0 [1] (🤰) pregnant woman
1F931..1F932 ; Emoji_Modifier_Base # 10.0 [2] (🤱..🤲) breast-feeding..palms up together
1F933..1F939 ; Emoji_Modifier_Base # 9.0 [7] (🤳..🤹) selfie..person juggling
1F93C..1F93E ; Emoji_Modifier_Base # 9.0 [3] (🤼..🤾) people wrestling..person playing handball
1F9B5..1F9B6 ; Emoji_Modifier_Base # 11.0 [2] (🦵..🦶) leg..foot
1F9B8..1F9B9 ; Emoji_Modifier_Base # 11.0 [2] (🦸..🦹) superhero..supervillain
1F9BB ; Emoji_Modifier_Base # 12.0 [1] (🦻) ear with hearing aid
1F9CD..1F9CF ; Emoji_Modifier_Base # 12.0 [3] (🧍..🧏) person standing..deaf person
1F9D1..1F9DD ; Emoji_Modifier_Base # 10.0 [13] (🧑..🧝) person..elf
# Total elements: 120
# ================================================
# All omitted code points have Emoji_Component=No
# @missing: 0000..10FFFF ; Emoji_Component ; No
0023 ; Emoji_Component # 1.1 [1] (#) number sign
002A ; Emoji_Component # 1.1 [1] (*) asterisk
0030..0039 ; Emoji_Component # 1.1 [10] (0..9) digit zero..digit nine
200D ; Emoji_Component # 1.1 [1] () zero width joiner
20E3 ; Emoji_Component # 3.0 [1] (⃣) combining enclosing keycap
FE0F ; Emoji_Component # 3.2 [1] () VARIATION SELECTOR-16
1F1E6..1F1FF ; Emoji_Component # 6.0 [26] (🇦..🇿) regional indicator symbol letter a..regional indicator symbol letter z
1F3FB..1F3FF ; Emoji_Component # 8.0 [5] (🏻..🏿) light skin tone..dark skin tone
1F9B0..1F9B3 ; Emoji_Component # 11.0 [4] (🦰..🦳) red hair..white hair
E0020..E007F ; Emoji_Component # 3.1 [96] (󠀠..󠁿) tag space..cancel tag
# Total elements: 146
# ================================================
# All omitted code points have Extended_Pictographic=No
# @missing: 0000..10FFFF ; Extended_Pictographic ; No
00A9 ; Extended_Pictographic# 1.1 [1] (©️) copyright
00AE ; Extended_Pictographic# 1.1 [1] (®️) registered
203C ; Extended_Pictographic# 1.1 [1] (‼️) double exclamation mark
2049 ; Extended_Pictographic# 3.0 [1] (⁉️) exclamation question mark
2122 ; Extended_Pictographic# 1.1 [1] (™️) trade mark
2139 ; Extended_Pictographic# 3.0 [1] () information
2194..2199 ; Extended_Pictographic# 1.1 [6] (↔️..↙️) left-right arrow..down-left arrow
21A9..21AA ; Extended_Pictographic# 1.1 [2] (↩️..↪️) right arrow curving left..left arrow curving right
231A..231B ; Extended_Pictographic# 1.1 [2] (⌚..⌛) watch..hourglass done
2328 ; Extended_Pictographic# 1.1 [1] (⌨️) keyboard
2388 ; Extended_Pictographic# 3.0 [1] (⎈) HELM SYMBOL
23CF ; Extended_Pictographic# 4.0 [1] (⏏️) eject button
23E9..23F3 ; Extended_Pictographic# 6.0 [11] (⏩..⏳) fast-forward button..hourglass not done
23F8..23FA ; Extended_Pictographic# 7.0 [3] (⏸️..⏺️) pause button..record button
24C2 ; Extended_Pictographic# 1.1 [1] (Ⓜ️) circled M
25AA..25AB ; Extended_Pictographic# 1.1 [2] (▪️..▫️) black small square..white small square
25B6 ; Extended_Pictographic# 1.1 [1] (▶️) play button
25C0 ; Extended_Pictographic# 1.1 [1] (◀️) reverse button
25FB..25FE ; Extended_Pictographic# 3.2 [4] (◻️..◾) white medium square..black medium-small square
2600..2605 ; Extended_Pictographic# 1.1 [6] (☀️..★) sun..BLACK STAR
2607..2612 ; Extended_Pictographic# 1.1 [12] (☇..☒) LIGHTNING..BALLOT BOX WITH X
2614..2615 ; Extended_Pictographic# 4.0 [2] (☔..☕) umbrella with rain drops..hot beverage
2616..2617 ; Extended_Pictographic# 3.2 [2] (☖..☗) WHITE SHOGI PIECE..BLACK SHOGI PIECE
2618 ; Extended_Pictographic# 4.1 [1] (☘️) shamrock
2619 ; Extended_Pictographic# 3.0 [1] (☙) REVERSED ROTATED FLORAL HEART BULLET
261A..266F ; Extended_Pictographic# 1.1 [86] (☚..♯) BLACK LEFT POINTING INDEX..MUSIC SHARP SIGN
2670..2671 ; Extended_Pictographic# 3.0 [2] (♰..♱) WEST SYRIAC CROSS..EAST SYRIAC CROSS
2672..267D ; Extended_Pictographic# 3.2 [12] (♲..♽) UNIVERSAL RECYCLING SYMBOL..PARTIALLY-RECYCLED PAPER SYMBOL
267E..267F ; Extended_Pictographic# 4.1 [2] (♾️..♿) infinity..wheelchair symbol
2680..2685 ; Extended_Pictographic# 3.2 [6] (⚀..⚅) DIE FACE-1..DIE FACE-6
2690..2691 ; Extended_Pictographic# 4.0 [2] (⚐..⚑) WHITE FLAG..BLACK FLAG
2692..269C ; Extended_Pictographic# 4.1 [11] (⚒️..⚜️) hammer and pick..fleur-de-lis
269D ; Extended_Pictographic# 5.1 [1] (⚝) OUTLINED WHITE STAR
269E..269F ; Extended_Pictographic# 5.2 [2] (⚞..⚟) THREE LINES CONVERGING RIGHT..THREE LINES CONVERGING LEFT
26A0..26A1 ; Extended_Pictographic# 4.0 [2] (⚠️..⚡) warning..high voltage
26A2..26B1 ; Extended_Pictographic# 4.1 [16] (⚢..⚱️) DOUBLED FEMALE SIGN..funeral urn
26B2 ; Extended_Pictographic# 5.0 [1] (⚲) NEUTER
26B3..26BC ; Extended_Pictographic# 5.1 [10] (⚳..⚼) CERES..SESQUIQUADRATE
26BD..26BF ; Extended_Pictographic# 5.2 [3] (⚽..⚿) soccer ball..SQUARED KEY
26C0..26C3 ; Extended_Pictographic# 5.1 [4] (⛀..⛃) WHITE DRAUGHTS MAN..BLACK DRAUGHTS KING
26C4..26CD ; Extended_Pictographic# 5.2 [10] (⛄..⛍) snowman without snow..DISABLED CAR
26CE ; Extended_Pictographic# 6.0 [1] (⛎) Ophiuchus
26CF..26E1 ; Extended_Pictographic# 5.2 [19] (⛏️..⛡) pick..RESTRICTED LEFT ENTRY-2
26E2 ; Extended_Pictographic# 6.0 [1] (⛢) ASTRONOMICAL SYMBOL FOR URANUS
26E3 ; Extended_Pictographic# 5.2 [1] (⛣) HEAVY CIRCLE WITH STROKE AND TWO DOTS ABOVE
26E4..26E7 ; Extended_Pictographic# 6.0 [4] (⛤..⛧) PENTAGRAM..INVERTED PENTAGRAM
26E8..26FF ; Extended_Pictographic# 5.2 [24] (⛨..⛿) BLACK CROSS ON SHIELD..WHITE FLAG WITH HORIZONTAL MIDDLE BLACK STRIPE
2700 ; Extended_Pictographic# 7.0 [1] (✀) BLACK SAFETY SCISSORS
2701..2704 ; Extended_Pictographic# 1.1 [4] (✁..✄) UPPER BLADE SCISSORS..WHITE SCISSORS
2705 ; Extended_Pictographic# 6.0 [1] (✅) check mark button
2708..2709 ; Extended_Pictographic# 1.1 [2] (✈️..✉️) airplane..envelope
270A..270B ; Extended_Pictographic# 6.0 [2] (✊..✋) raised fist..raised hand
270C..2712 ; Extended_Pictographic# 1.1 [7] (✌️..✒️) victory hand..black nib
2714 ; Extended_Pictographic# 1.1 [1] (✔️) check mark
2716 ; Extended_Pictographic# 1.1 [1] (✖️) multiplication sign
271D ; Extended_Pictographic# 1.1 [1] (✝️) latin cross
2721 ; Extended_Pictographic# 1.1 [1] (✡️) star of David
2728 ; Extended_Pictographic# 6.0 [1] (✨) sparkles
2733..2734 ; Extended_Pictographic# 1.1 [2] (✳️..✴️) eight-spoked asterisk..eight-pointed star
2744 ; Extended_Pictographic# 1.1 [1] (❄️) snowflake
2747 ; Extended_Pictographic# 1.1 [1] (❇️) sparkle
274C ; Extended_Pictographic# 6.0 [1] (❌) cross mark
274E ; Extended_Pictographic# 6.0 [1] (❎) cross mark button
2753..2755 ; Extended_Pictographic# 6.0 [3] (❓..❕) question mark..white exclamation mark
2757 ; Extended_Pictographic# 5.2 [1] (❗) exclamation mark
2763..2767 ; Extended_Pictographic# 1.1 [5] (❣️..❧) heart exclamation..ROTATED FLORAL HEART BULLET
2795..2797 ; Extended_Pictographic# 6.0 [3] (..➗) plus sign..division sign
27A1 ; Extended_Pictographic# 1.1 [1] (➡️) right arrow
27B0 ; Extended_Pictographic# 6.0 [1] (➰) curly loop
27BF ; Extended_Pictographic# 6.0 [1] (➿) double curly loop
2934..2935 ; Extended_Pictographic# 3.2 [2] (⤴️..⤵️) right arrow curving up..right arrow curving down
2B05..2B07 ; Extended_Pictographic# 4.0 [3] (⬅️..⬇️) left arrow..down arrow
2B1B..2B1C ; Extended_Pictographic# 5.1 [2] (⬛..⬜) black large square..white large square
2B50 ; Extended_Pictographic# 5.1 [1] (⭐) star
2B55 ; Extended_Pictographic# 5.2 [1] (⭕) hollow red circle
3030 ; Extended_Pictographic# 1.1 [1] (〰️) wavy dash
303D ; Extended_Pictographic# 3.2 [1] (〽️) part alternation mark
3297 ; Extended_Pictographic# 1.1 [1] (㊗️) Japanese “congratulations” button
3299 ; Extended_Pictographic# 1.1 [1] (㊙️) Japanese “secret” button
1F000..1F02B ; Extended_Pictographic# 5.1 [44] (🀀..🀫) MAHJONG TILE EAST WIND..MAHJONG TILE BACK
1F02C..1F02F ; Extended_Pictographic# NA [4] (🀬..🀯) <reserved-1F02C>..<reserved-1F02F>
1F030..1F093 ; Extended_Pictographic# 5.1[100] (🀰..🂓) DOMINO TILE HORIZONTAL BACK..DOMINO TILE VERTICAL-06-06
1F094..1F09F ; Extended_Pictographic# NA [12] (🂔..🂟) <reserved-1F094>..<reserved-1F09F>
1F0A0..1F0AE ; Extended_Pictographic# 6.0 [15] (🂠..🂮) PLAYING CARD BACK..PLAYING CARD KING OF SPADES
1F0AF..1F0B0 ; Extended_Pictographic# NA [2] (🂯..🂰) <reserved-1F0AF>..<reserved-1F0B0>
1F0B1..1F0BE ; Extended_Pictographic# 6.0 [14] (🂱..🂾) PLAYING CARD ACE OF HEARTS..PLAYING CARD KING OF HEARTS
1F0BF ; Extended_Pictographic# 7.0 [1] (🂿) PLAYING CARD RED JOKER
1F0C0 ; Extended_Pictographic# NA [1] (🃀) <reserved-1F0C0>
1F0C1..1F0CF ; Extended_Pictographic# 6.0 [15] (🃁..🃏) PLAYING CARD ACE OF DIAMONDS..joker
1F0D0 ; Extended_Pictographic# NA [1] (🃐) <reserved-1F0D0>
1F0D1..1F0DF ; Extended_Pictographic# 6.0 [15] (🃑..🃟) PLAYING CARD ACE OF CLUBS..PLAYING CARD WHITE JOKER
1F0E0..1F0F5 ; Extended_Pictographic# 7.0 [22] (🃠..🃵) PLAYING CARD FOOL..PLAYING CARD TRUMP-21
1F0F6..1F0FF ; Extended_Pictographic# NA [10] (🃶..🃿) <reserved-1F0F6>..<reserved-1F0FF>
1F10D..1F10F ; Extended_Pictographic# NA [3] (🄍..🄏) <reserved-1F10D>..<reserved-1F10F>
1F12F ; Extended_Pictographic# 11.0 [1] (🄯) COPYLEFT SYMBOL
1F16C ; Extended_Pictographic# 12.0 [1] (🅬) RAISED MR SIGN
1F16D..1F16F ; Extended_Pictographic# NA [3] (🅭..🅯) <reserved-1F16D>..<reserved-1F16F>
1F170..1F171 ; Extended_Pictographic# 6.0 [2] (🅰️..🅱️) A button (blood type)..B button (blood type)
1F17E ; Extended_Pictographic# 6.0 [1] (🅾️) O button (blood type)
1F17F ; Extended_Pictographic# 5.2 [1] (🅿️) P button
1F18E ; Extended_Pictographic# 6.0 [1] (🆎) AB button (blood type)
1F191..1F19A ; Extended_Pictographic# 6.0 [10] (🆑..🆚) CL button..VS button
1F1AD..1F1E5 ; Extended_Pictographic# NA [57] (🆭..🇥) <reserved-1F1AD>..<reserved-1F1E5>
1F201..1F202 ; Extended_Pictographic# 6.0 [2] (🈁..🈂️) Japanese “here” button..Japanese “service charge” button
1F203..1F20F ; Extended_Pictographic# NA [13] (🈃..🈏) <reserved-1F203>..<reserved-1F20F>
1F21A ; Extended_Pictographic# 5.2 [1] (🈚) Japanese “free of charge” button
1F22F ; Extended_Pictographic# 5.2 [1] (🈯) Japanese “reserved” button
1F232..1F23A ; Extended_Pictographic# 6.0 [9] (🈲..🈺) Japanese “prohibited” button..Japanese “open for business” button
1F23C..1F23F ; Extended_Pictographic# NA [4] (🈼..🈿) <reserved-1F23C>..<reserved-1F23F>
1F249..1F24F ; Extended_Pictographic# NA [7] (🉉..🉏) <reserved-1F249>..<reserved-1F24F>
1F250..1F251 ; Extended_Pictographic# 6.0 [2] (🉐..🉑) Japanese “bargain” button..Japanese “acceptable” button
1F252..1F25F ; Extended_Pictographic# NA [14] (🉒..🉟) <reserved-1F252>..<reserved-1F25F>
1F260..1F265 ; Extended_Pictographic# 10.0 [6] (🉠..🉥) ROUNDED SYMBOL FOR FU..ROUNDED SYMBOL FOR CAI
1F266..1F2FF ; Extended_Pictographic# NA[154] (🉦..🋿) <reserved-1F266>..<reserved-1F2FF>
1F300..1F320 ; Extended_Pictographic# 6.0 [33] (🌀..🌠) cyclone..shooting star
1F321..1F32C ; Extended_Pictographic# 7.0 [12] (🌡️..🌬️) thermometer..wind face
1F32D..1F32F ; Extended_Pictographic# 8.0 [3] (🌭..🌯) hot dog..burrito
1F330..1F335 ; Extended_Pictographic# 6.0 [6] (🌰..🌵) chestnut..cactus
1F336 ; Extended_Pictographic# 7.0 [1] (🌶️) hot pepper
1F337..1F37C ; Extended_Pictographic# 6.0 [70] (🌷..🍼) tulip..baby bottle
1F37D ; Extended_Pictographic# 7.0 [1] (🍽️) fork and knife with plate
1F37E..1F37F ; Extended_Pictographic# 8.0 [2] (🍾..🍿) bottle with popping cork..popcorn
1F380..1F393 ; Extended_Pictographic# 6.0 [20] (🎀..🎓) ribbon..graduation cap
1F394..1F39F ; Extended_Pictographic# 7.0 [12] (🎔..🎟️) HEART WITH TIP ON THE LEFT..admission tickets
1F3A0..1F3C4 ; Extended_Pictographic# 6.0 [37] (🎠..🏄) carousel horse..person surfing
1F3C5 ; Extended_Pictographic# 7.0 [1] (🏅) sports medal
1F3C6..1F3CA ; Extended_Pictographic# 6.0 [5] (🏆..🏊) trophy..person swimming
1F3CB..1F3CE ; Extended_Pictographic# 7.0 [4] (🏋️..🏎️) person lifting weights..racing car
1F3CF..1F3D3 ; Extended_Pictographic# 8.0 [5] (🏏..🏓) cricket game..ping pong
1F3D4..1F3DF ; Extended_Pictographic# 7.0 [12] (🏔️..🏟️) snow-capped mountain..stadium
1F3E0..1F3F0 ; Extended_Pictographic# 6.0 [17] (🏠..🏰) house..castle
1F3F1..1F3F7 ; Extended_Pictographic# 7.0 [7] (🏱..🏷️) WHITE PENNANT..label
1F3F8..1F3FA ; Extended_Pictographic# 8.0 [3] (🏸..🏺) badminton..amphora
1F400..1F43E ; Extended_Pictographic# 6.0 [63] (🐀..🐾) rat..paw prints
1F43F ; Extended_Pictographic# 7.0 [1] (🐿️) chipmunk
1F440 ; Extended_Pictographic# 6.0 [1] (👀) eyes
1F441 ; Extended_Pictographic# 7.0 [1] (👁️) eye
1F442..1F4F7 ; Extended_Pictographic# 6.0[182] (👂..📷) ear..camera
1F4F8 ; Extended_Pictographic# 7.0 [1] (📸) camera with flash
1F4F9..1F4FC ; Extended_Pictographic# 6.0 [4] (📹..📼) video camera..videocassette
1F4FD..1F4FE ; Extended_Pictographic# 7.0 [2] (📽️..📾) film projector..PORTABLE STEREO
1F4FF ; Extended_Pictographic# 8.0 [1] (📿) prayer beads
1F500..1F53D ; Extended_Pictographic# 6.0 [62] (🔀..🔽) shuffle tracks button..downwards button
1F546..1F54A ; Extended_Pictographic# 7.0 [5] (🕆..🕊️) WHITE LATIN CROSS..dove
1F54B..1F54F ; Extended_Pictographic# 8.0 [5] (🕋..🕏) kaaba..BOWL OF HYGIEIA
1F550..1F567 ; Extended_Pictographic# 6.0 [24] (🕐..🕧) one oclock..twelve-thirty
1F568..1F579 ; Extended_Pictographic# 7.0 [18] (🕨..🕹️) RIGHT SPEAKER..joystick
1F57A ; Extended_Pictographic# 9.0 [1] (🕺) man dancing
1F57B..1F5A3 ; Extended_Pictographic# 7.0 [41] (🕻..🖣) LEFT HAND TELEPHONE RECEIVER..BLACK DOWN POINTING BACKHAND INDEX
1F5A4 ; Extended_Pictographic# 9.0 [1] (🖤) black heart
1F5A5..1F5FA ; Extended_Pictographic# 7.0 [86] (🖥️..🗺️) desktop computer..world map
1F5FB..1F5FF ; Extended_Pictographic# 6.0 [5] (🗻..🗿) mount fuji..moai
1F600 ; Extended_Pictographic# 6.1 [1] (😀) grinning face
1F601..1F610 ; Extended_Pictographic# 6.0 [16] (😁..😐) beaming face with smiling eyes..neutral face
1F611 ; Extended_Pictographic# 6.1 [1] (😑) expressionless face
1F612..1F614 ; Extended_Pictographic# 6.0 [3] (😒..😔) unamused face..pensive face
1F615 ; Extended_Pictographic# 6.1 [1] (😕) confused face
1F616 ; Extended_Pictographic# 6.0 [1] (😖) confounded face
1F617 ; Extended_Pictographic# 6.1 [1] (😗) kissing face
1F618 ; Extended_Pictographic# 6.0 [1] (😘) face blowing a kiss
1F619 ; Extended_Pictographic# 6.1 [1] (😙) kissing face with smiling eyes
1F61A ; Extended_Pictographic# 6.0 [1] (😚) kissing face with closed eyes
1F61B ; Extended_Pictographic# 6.1 [1] (😛) face with tongue
1F61C..1F61E ; Extended_Pictographic# 6.0 [3] (😜..😞) winking face with tongue..disappointed face
1F61F ; Extended_Pictographic# 6.1 [1] (😟) worried face
1F620..1F625 ; Extended_Pictographic# 6.0 [6] (😠..😥) angry face..sad but relieved face
1F626..1F627 ; Extended_Pictographic# 6.1 [2] (😦..😧) frowning face with open mouth..anguished face
1F628..1F62B ; Extended_Pictographic# 6.0 [4] (😨..😫) fearful face..tired face
1F62C ; Extended_Pictographic# 6.1 [1] (😬) grimacing face
1F62D ; Extended_Pictographic# 6.0 [1] (😭) loudly crying face
1F62E..1F62F ; Extended_Pictographic# 6.1 [2] (😮..😯) face with open mouth..hushed face
1F630..1F633 ; Extended_Pictographic# 6.0 [4] (😰..😳) anxious face with sweat..flushed face
1F634 ; Extended_Pictographic# 6.1 [1] (😴) sleeping face
1F635..1F640 ; Extended_Pictographic# 6.0 [12] (😵..🙀) dizzy face..weary cat
1F641..1F642 ; Extended_Pictographic# 7.0 [2] (🙁..🙂) slightly frowning face..slightly smiling face
1F643..1F644 ; Extended_Pictographic# 8.0 [2] (🙃..🙄) upside-down face..face with rolling eyes
1F645..1F64F ; Extended_Pictographic# 6.0 [11] (🙅..🙏) person gesturing NO..folded hands
1F680..1F6C5 ; Extended_Pictographic# 6.0 [70] (🚀..🛅) rocket..left luggage
1F6C6..1F6CF ; Extended_Pictographic# 7.0 [10] (🛆..🛏️) TRIANGLE WITH ROUNDED CORNERS..bed
1F6D0 ; Extended_Pictographic# 8.0 [1] (🛐) place of worship
1F6D1..1F6D2 ; Extended_Pictographic# 9.0 [2] (🛑..🛒) stop sign..shopping cart
1F6D3..1F6D4 ; Extended_Pictographic# 10.0 [2] (🛓..🛔) STUPA..PAGODA
1F6D5 ; Extended_Pictographic# 12.0 [1] (🛕) hindu temple
1F6D6..1F6DF ; Extended_Pictographic# NA [10] (🛖..🛟) <reserved-1F6D6>..<reserved-1F6DF>
1F6E0..1F6EC ; Extended_Pictographic# 7.0 [13] (🛠️..🛬) hammer and wrench..airplane arrival
1F6ED..1F6EF ; Extended_Pictographic# NA [3] (🛭..🛯) <reserved-1F6ED>..<reserved-1F6EF>
1F6F0..1F6F3 ; Extended_Pictographic# 7.0 [4] (🛰️..🛳️) satellite..passenger ship
1F6F4..1F6F6 ; Extended_Pictographic# 9.0 [3] (🛴..🛶) kick scooter..canoe
1F6F7..1F6F8 ; Extended_Pictographic# 10.0 [2] (🛷..🛸) sled..flying saucer
1F6F9 ; Extended_Pictographic# 11.0 [1] (🛹) skateboard
1F6FA ; Extended_Pictographic# 12.0 [1] (🛺) auto rickshaw
1F6FB..1F6FF ; Extended_Pictographic# NA [5] (🛻..🛿) <reserved-1F6FB>..<reserved-1F6FF>
1F774..1F77F ; Extended_Pictographic# NA [12] (🝴..🝿) <reserved-1F774>..<reserved-1F77F>
1F7D5..1F7D8 ; Extended_Pictographic# 11.0 [4] (🟕..🟘) CIRCLED TRIANGLE..NEGATIVE CIRCLED SQUARE
1F7D9..1F7DF ; Extended_Pictographic# NA [7] (🟙..🟟) <reserved-1F7D9>..<reserved-1F7DF>
1F7E0..1F7EB ; Extended_Pictographic# 12.0 [12] (🟠..🟫) orange circle..brown square
1F7EC..1F7FF ; Extended_Pictographic# NA [20] (🟬..🟿) <reserved-1F7EC>..<reserved-1F7FF>
1F80C..1F80F ; Extended_Pictographic# NA [4] (🠌..🠏) <reserved-1F80C>..<reserved-1F80F>
1F848..1F84F ; Extended_Pictographic# NA [8] (🡈..🡏) <reserved-1F848>..<reserved-1F84F>
1F85A..1F85F ; Extended_Pictographic# NA [6] (🡚..🡟) <reserved-1F85A>..<reserved-1F85F>
1F888..1F88F ; Extended_Pictographic# NA [8] (🢈..🢏) <reserved-1F888>..<reserved-1F88F>
1F8AE..1F8FF ; Extended_Pictographic# NA [82] (🢮..🣿) <reserved-1F8AE>..<reserved-1F8FF>
1F90C ; Extended_Pictographic# NA [1] (🤌) <reserved-1F90C>
1F90D..1F90F ; Extended_Pictographic# 12.0 [3] (🤍..🤏) white heart..pinching hand
1F910..1F918 ; Extended_Pictographic# 8.0 [9] (🤐..🤘) zipper-mouth face..sign of the horns
1F919..1F91E ; Extended_Pictographic# 9.0 [6] (🤙..🤞) call me hand..crossed fingers
1F91F ; Extended_Pictographic# 10.0 [1] (🤟) love-you gesture
1F920..1F927 ; Extended_Pictographic# 9.0 [8] (🤠..🤧) cowboy hat face..sneezing face
1F928..1F92F ; Extended_Pictographic# 10.0 [8] (🤨..🤯) face with raised eyebrow..exploding head
1F930 ; Extended_Pictographic# 9.0 [1] (🤰) pregnant woman
1F931..1F932 ; Extended_Pictographic# 10.0 [2] (🤱..🤲) breast-feeding..palms up together
1F933..1F93A ; Extended_Pictographic# 9.0 [8] (🤳..🤺) selfie..person fencing
1F93C..1F93E ; Extended_Pictographic# 9.0 [3] (🤼..🤾) people wrestling..person playing handball
1F93F ; Extended_Pictographic# 12.0 [1] (🤿) diving mask
1F940..1F945 ; Extended_Pictographic# 9.0 [6] (🥀..🥅) wilted flower..goal net
1F947..1F94B ; Extended_Pictographic# 9.0 [5] (🥇..🥋) 1st place medal..martial arts uniform
1F94C ; Extended_Pictographic# 10.0 [1] (🥌) curling stone
1F94D..1F94F ; Extended_Pictographic# 11.0 [3] (🥍..🥏) lacrosse..flying disc
1F950..1F95E ; Extended_Pictographic# 9.0 [15] (🥐..🥞) croissant..pancakes
1F95F..1F96B ; Extended_Pictographic# 10.0 [13] (🥟..🥫) dumpling..canned food
1F96C..1F970 ; Extended_Pictographic# 11.0 [5] (🥬..🥰) leafy green..smiling face with hearts
1F971 ; Extended_Pictographic# 12.0 [1] (🥱) yawning face
1F972 ; Extended_Pictographic# NA [1] (🥲) <reserved-1F972>
1F973..1F976 ; Extended_Pictographic# 11.0 [4] (🥳..🥶) partying face..cold face
1F977..1F979 ; Extended_Pictographic# NA [3] (🥷..🥹) <reserved-1F977>..<reserved-1F979>
1F97A ; Extended_Pictographic# 11.0 [1] (🥺) pleading face
1F97B ; Extended_Pictographic# 12.0 [1] (🥻) sari
1F97C..1F97F ; Extended_Pictographic# 11.0 [4] (🥼..🥿) lab coat..flat shoe
1F980..1F984 ; Extended_Pictographic# 8.0 [5] (🦀..🦄) crab..unicorn
1F985..1F991 ; Extended_Pictographic# 9.0 [13] (🦅..🦑) eagle..squid
1F992..1F997 ; Extended_Pictographic# 10.0 [6] (🦒..🦗) giraffe..cricket
1F998..1F9A2 ; Extended_Pictographic# 11.0 [11] (🦘..🦢) kangaroo..swan
1F9A3..1F9A4 ; Extended_Pictographic# NA [2] (🦣..🦤) <reserved-1F9A3>..<reserved-1F9A4>
1F9A5..1F9AA ; Extended_Pictographic# 12.0 [6] (🦥..🦪) sloth..oyster
1F9AB..1F9AD ; Extended_Pictographic# NA [3] (🦫..🦭) <reserved-1F9AB>..<reserved-1F9AD>
1F9AE..1F9AF ; Extended_Pictographic# 12.0 [2] (🦮..🦯) guide dog..probing cane
1F9B0..1F9B9 ; Extended_Pictographic# 11.0 [10] (🦰..🦹) red hair..supervillain
1F9BA..1F9BF ; Extended_Pictographic# 12.0 [6] (🦺..🦿) safety vest..mechanical leg
1F9C0 ; Extended_Pictographic# 8.0 [1] (🧀) cheese wedge
1F9C1..1F9C2 ; Extended_Pictographic# 11.0 [2] (🧁..🧂) cupcake..salt
1F9C3..1F9CA ; Extended_Pictographic# 12.0 [8] (🧃..🧊) beverage box..ice cube
1F9CB..1F9CC ; Extended_Pictographic# NA [2] (🧋..🧌) <reserved-1F9CB>..<reserved-1F9CC>
1F9CD..1F9CF ; Extended_Pictographic# 12.0 [3] (🧍..🧏) person standing..deaf person
1F9D0..1F9E6 ; Extended_Pictographic# 10.0 [23] (🧐..🧦) face with monocle..socks
1F9E7..1F9FF ; Extended_Pictographic# 11.0 [25] (🧧..🧿) red envelope..nazar amulet
1FA00..1FA53 ; Extended_Pictographic# 12.0 [84] (🨀..🩓) NEUTRAL CHESS KING..BLACK CHESS KNIGHT-BISHOP
1FA54..1FA5F ; Extended_Pictographic# NA [12] (🩔..🩟) <reserved-1FA54>..<reserved-1FA5F>
1FA60..1FA6D ; Extended_Pictographic# 11.0 [14] (🩠..🩭) XIANGQI RED GENERAL..XIANGQI BLACK SOLDIER
1FA6E..1FA6F ; Extended_Pictographic# NA [2] (🩮..🩯) <reserved-1FA6E>..<reserved-1FA6F>
1FA70..1FA73 ; Extended_Pictographic# 12.0 [4] (🩰..🩳) ballet shoes..shorts
1FA74..1FA77 ; Extended_Pictographic# NA [4] (🩴..🩷) <reserved-1FA74>..<reserved-1FA77>
1FA78..1FA7A ; Extended_Pictographic# 12.0 [3] (🩸..🩺) drop of blood..stethoscope
1FA7B..1FA7F ; Extended_Pictographic# NA [5] (🩻..🩿) <reserved-1FA7B>..<reserved-1FA7F>
1FA80..1FA82 ; Extended_Pictographic# 12.0 [3] (🪀..🪂) yo-yo..parachute
1FA83..1FA8F ; Extended_Pictographic# NA [13] (🪃..🪏) <reserved-1FA83>..<reserved-1FA8F>
1FA90..1FA95 ; Extended_Pictographic# 12.0 [6] (🪐..🪕) ringed planet..banjo
1FA96..1FFFD ; Extended_Pictographic# NA[1384] (🪖..🿽) <reserved-1FA96>..<reserved-1FFFD>
# Total elements: 3793
#EOF

4879
lib/pleroma/emoji-test.txt Normal file

File diff suppressed because it is too large Load diff

View file

@ -102,31 +102,36 @@ defp update_emojis(emojis) do
:ets.insert(@ets, emojis)
end
@external_resource "lib/pleroma/emoji-data.txt"
@external_resource "lib/pleroma/emoji-test.txt"
regional_indicators =
Enum.map(127_462..127_487, fn codepoint ->
<<codepoint::utf8>>
end)
emojis =
@external_resource
|> File.read!()
|> String.split("\n")
|> Enum.filter(fn line -> line != "" and not String.starts_with?(line, "#") end)
|> Enum.filter(fn line ->
line != "" and not String.starts_with?(line, "#") and
String.contains?(line, "fully-qualified")
end)
|> Enum.map(fn line ->
line
|> String.split(";", parts: 2)
|> hd()
|> String.trim()
|> String.split("..")
|> case do
[number] ->
<<String.to_integer(number, 16)::utf8>>
[first, last] ->
String.to_integer(first, 16)..String.to_integer(last, 16)
|> Enum.map(&<<&1::utf8>>)
end
|> String.split()
|> Enum.map(fn codepoint ->
<<String.to_integer(codepoint, 16)::utf8>>
end)
|> Enum.join()
end)
|> List.flatten()
|> Enum.uniq()
emojis = emojis ++ regional_indicators
for emoji <- emojis do
def is_unicode_emoji?(unquote(emoji)), do: true
end

View file

@ -22,14 +22,14 @@ defmodule Pleroma.Emoji.Pack do
alias Pleroma.Emoji
alias Pleroma.Emoji.Pack
alias Pleroma.Utils
@spec create(String.t()) :: {:ok, t()} | {:error, File.posix()} | {:error, :empty_values}
def create(name) do
with :ok <- validate_not_empty([name]),
dir <- Path.join(emoji_path(), name),
:ok <- File.mkdir(dir) do
%__MODULE__{pack_file: Path.join(dir, "pack.json")}
|> save_pack()
save_pack(%__MODULE__{pack_file: Path.join(dir, "pack.json")})
end
end
@ -62,10 +62,9 @@ def show(opts) do
@spec delete(String.t()) ::
{:ok, [binary()]} | {:error, File.posix(), binary()} | {:error, :empty_values}
def delete(name) do
with :ok <- validate_not_empty([name]) do
emoji_path()
|> Path.join(name)
|> File.rm_rf()
with :ok <- validate_not_empty([name]),
pack_path <- Path.join(emoji_path(), name) do
File.rm_rf(pack_path)
end
end
@ -94,7 +93,7 @@ defp unpack_zip_emojies(zip_files) do
def add_file(%Pack{} = pack, _, _, %Plug.Upload{content_type: "application/zip"} = file) do
with {:ok, zip_files} <- :zip.table(to_charlist(file.path)),
[_ | _] = emojies <- unpack_zip_emojies(zip_files),
{:ok, tmp_dir} <- Pleroma.Utils.tmp_dir("emoji") do
{:ok, tmp_dir} <- Utils.tmp_dir("emoji") do
try do
{:ok, _emoji_files} =
:zip.unzip(
@ -282,18 +281,21 @@ def update_metadata(name, data) do
end
end
@spec load_pack(String.t()) :: {:ok, t()} | {:error, :not_found}
@spec load_pack(String.t()) :: {:ok, t()} | {:error, :file.posix()}
def load_pack(name) do
pack_file = Path.join([emoji_path(), name, "pack.json"])
if File.exists?(pack_file) do
with {:ok, _} <- File.stat(pack_file),
{:ok, pack_data} <- File.read(pack_file) do
pack =
pack_file
|> File.read!()
|> from_json()
|> Map.put(:pack_file, pack_file)
|> Map.put(:path, Path.dirname(pack_file))
|> Map.put(:name, name)
from_json(
pack_data,
%{
pack_file: pack_file,
path: Path.dirname(pack_file),
name: name
}
)
files_count =
pack.files
@ -301,8 +303,6 @@ def load_pack(name) do
|> length()
{:ok, Map.put(pack, :files_count, files_count)}
else
{:error, :not_found}
end
end
@ -434,10 +434,17 @@ defp save_pack(pack) do
end
end
defp from_json(json) do
defp from_json(json, attrs) do
map = Jason.decode!(json)
struct(__MODULE__, %{files: map["files"], pack: map["pack"]})
pack_attrs =
attrs
|> Map.merge(%{
files: map["files"],
pack: map["pack"]
})
struct(__MODULE__, pack_attrs)
end
defp validate_shareable_packs_available(uri) do
@ -491,10 +498,10 @@ defp rename_file(pack, filename, new_filename) do
end
defp create_subdirs(file_path) do
if String.contains?(file_path, "/") do
file_path
|> Path.dirname()
|> File.mkdir_p!()
with true <- String.contains?(file_path, "/"),
path <- Path.dirname(file_path),
false <- File.exists?(path) do
File.mkdir_p!(path)
end
end
@ -518,10 +525,15 @@ defp remove_dir_if_empty(emoji, filename) do
defp get_filename(pack, shortcode) do
with %{^shortcode => filename} when is_binary(filename) <- pack.files,
true <- pack.path |> Path.join(filename) |> File.exists?() do
file_path <- Path.join(pack.path, filename),
{:ok, _} <- File.stat(file_path) do
{:ok, filename}
else
_ -> {:error, :doesnt_exist}
{:error, _} = error ->
error
_ ->
{:error, :doesnt_exist}
end
end

View file

@ -62,23 +62,47 @@ def update(%User{} = follower, %User{} = following, state) do
follow(follower, following, state)
following_relationship ->
following_relationship
|> cast(%{state: state}, [:state])
|> validate_required([:state])
|> Repo.update()
with {:ok, _following_relationship} <-
following_relationship
|> cast(%{state: state}, [:state])
|> validate_required([:state])
|> Repo.update() do
after_update(state, follower, following)
end
end
end
def follow(%User{} = follower, %User{} = following, state \\ :follow_accept) do
%__MODULE__{}
|> changeset(%{follower: follower, following: following, state: state})
|> Repo.insert(on_conflict: :nothing)
with {:ok, _following_relationship} <-
%__MODULE__{}
|> changeset(%{follower: follower, following: following, state: state})
|> Repo.insert(on_conflict: :nothing) do
after_update(state, follower, following)
end
end
def unfollow(%User{} = follower, %User{} = following) do
case get(follower, following) do
%__MODULE__{} = following_relationship -> Repo.delete(following_relationship)
_ -> {:ok, nil}
%__MODULE__{} = following_relationship ->
with {:ok, _following_relationship} <- Repo.delete(following_relationship) do
after_update(:unfollow, follower, following)
end
_ ->
{:ok, nil}
end
end
defp after_update(state, %User{} = follower, %User{} = following) do
with {:ok, following} <- User.update_follower_count(following),
{:ok, follower} <- User.update_following_count(follower) do
Pleroma.Web.Streamer.stream("follow_relationship", %{
state: state,
following: following,
follower: follower
})
{:ok, follower, following}
end
end

View file

@ -0,0 +1,46 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Helpers.AuthHelper do
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Plug.Conn
import Plug.Conn
@oauth_token_session_key :oauth_token
@doc """
Skips OAuth permissions (scopes) checks, assigns nil `:token`.
Intended to be used with explicit authentication and only when OAuth token cannot be determined.
"""
def skip_oauth(conn) do
conn
|> assign(:token, nil)
|> OAuthScopesPlug.skip_plug()
end
@doc "Drops authentication info from connection"
def drop_auth_info(conn) do
# To simplify debugging, setting a private variable on `conn` if auth info is dropped
conn
|> assign(:user, nil)
|> assign(:token, nil)
|> put_private(:authentication_ignored, true)
end
@doc "Gets OAuth token string from session"
def get_session_token(%Conn{} = conn) do
get_session(conn, @oauth_token_session_key)
end
@doc "Updates OAuth token string in session"
def put_session_token(%Conn{} = conn, token) when is_binary(token) do
put_session(conn, @oauth_token_session_key, token)
end
@doc "Deletes OAuth token string from session"
def delete_session_token(%Conn{} = conn) do
delete_session(conn, @oauth_token_session_key)
end
end

View file

@ -77,7 +77,7 @@ def reachable?(url_or_host) when is_binary(url_or_host) do
)
end
def reachable?(_), do: true
def reachable?(url_or_host) when is_binary(url_or_host), do: true
def set_reachable(url_or_host) when is_binary(url_or_host) do
with host <- host(url_or_host),
@ -166,7 +166,8 @@ def get_or_update_favicon(%URI{host: host} = instance_uri) do
defp scrape_favicon(%URI{} = instance_uri) do
try do
with {:ok, %Tesla.Env{body: html}} <-
with {_, true} <- {:reachable, reachable?(instance_uri.host)},
{:ok, %Tesla.Env{body: html}} <-
Pleroma.HTTP.get(to_string(instance_uri), [{"accept", "text/html"}], pool: :media),
{_, [favicon_rel | _]} when is_binary(favicon_rel) <-
{:parse,
@ -175,7 +176,15 @@ defp scrape_favicon(%URI{} = instance_uri) do
{:merge, URI.merge(instance_uri, favicon_rel) |> to_string()} do
favicon
else
_ -> nil
{:reachable, false} ->
Logger.debug(
"Instance.scrape_favicon(\"#{to_string(instance_uri)}\") ignored unreachable host"
)
nil
_ ->
nil
end
rescue
e ->

View file

@ -12,6 +12,26 @@ defmodule Pleroma.ModerationLog do
import Ecto.Query
@type t :: %__MODULE__{}
@type log_subject :: Activity.t() | User.t() | list(User.t())
@type log_params :: %{
required(:actor) => User.t(),
required(:action) => String.t(),
optional(:subject) => log_subject(),
optional(:subject_actor) => User.t(),
optional(:subject_id) => String.t(),
optional(:subjects) => list(User.t()),
optional(:permission) => String.t(),
optional(:text) => String.t(),
optional(:sensitive) => String.t(),
optional(:visibility) => String.t(),
optional(:followed) => User.t(),
optional(:follower) => User.t(),
optional(:nicknames) => list(String.t()),
optional(:tags) => list(String.t()),
optional(:target) => String.t()
}
schema "moderation_log" do
field(:data, :map)
@ -90,203 +110,105 @@ defp parse_datetime(datetime) do
parsed_datetime
end
@spec insert_log(%{actor: User, subject: [User], action: String.t(), permission: String.t()}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
subject: subjects,
action: action,
permission: permission
}) do
%ModerationLog{
data: %{
"actor" => user_to_map(actor),
"subject" => user_to_map(subjects),
"action" => action,
"permission" => permission,
"message" => ""
}
defp prepare_log_data(%{actor: actor, action: action} = attrs) do
%{
"actor" => user_to_map(actor),
"action" => action,
"message" => ""
}
|> insert_log_entry_with_message()
|> Pleroma.Maps.put_if_present("subject_actor", user_to_map(attrs[:subject_actor]))
end
@spec insert_log(%{actor: User, subject: User, action: String.t()}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
action: "report_update",
subject: %Activity{data: %{"type" => "Flag"}} = subject
}) do
%ModerationLog{
data: %{
"actor" => user_to_map(actor),
"action" => "report_update",
"subject" => report_to_map(subject),
"message" => ""
}
}
|> insert_log_entry_with_message()
defp prepare_log_data(attrs), do: attrs
@spec insert_log(log_params()) :: {:ok, ModerationLog} | {:error, any}
def insert_log(%{actor: %User{}, subject: subjects, permission: permission} = attrs) do
data =
attrs
|> prepare_log_data
|> Map.merge(%{"subject" => user_to_map(subjects), "permission" => permission})
insert_log_entry_with_message(%ModerationLog{data: data})
end
@spec insert_log(%{actor: User, subject: Activity, action: String.t(), text: String.t()}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
action: "report_note",
subject: %Activity{} = subject,
text: text
}) do
%ModerationLog{
data: %{
"actor" => user_to_map(actor),
"action" => "report_note",
"subject" => report_to_map(subject),
"text" => text
}
}
|> insert_log_entry_with_message()
def insert_log(%{actor: %User{}, action: action, subject: %Activity{} = subject} = attrs)
when action in ["report_note_delete", "report_update", "report_note"] do
data =
attrs
|> prepare_log_data
|> Pleroma.Maps.put_if_present("text", attrs[:text])
|> Map.merge(%{"subject" => report_to_map(subject)})
insert_log_entry_with_message(%ModerationLog{data: data})
end
@spec insert_log(%{actor: User, subject: Activity, action: String.t(), text: String.t()}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
action: "report_note_delete",
subject: %Activity{} = subject,
text: text
}) do
%ModerationLog{
data: %{
"actor" => user_to_map(actor),
"action" => "report_note_delete",
"subject" => report_to_map(subject),
"text" => text
}
}
|> insert_log_entry_with_message()
end
@spec insert_log(%{
actor: User,
subject: Activity,
action: String.t(),
sensitive: String.t(),
visibility: String.t()
}) :: {:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
action: "status_update",
subject: %Activity{} = subject,
sensitive: sensitive,
visibility: visibility
}) do
%ModerationLog{
data: %{
"actor" => user_to_map(actor),
"action" => "status_update",
def insert_log(
%{
actor: %User{},
action: action,
subject: %Activity{} = subject,
sensitive: sensitive,
visibility: visibility
} = attrs
)
when action == "status_update" do
data =
attrs
|> prepare_log_data
|> Map.merge(%{
"subject" => status_to_map(subject),
"sensitive" => sensitive,
"visibility" => visibility,
"message" => ""
}
}
|> insert_log_entry_with_message()
"visibility" => visibility
})
insert_log_entry_with_message(%ModerationLog{data: data})
end
@spec insert_log(%{actor: User, action: String.t(), subject_id: String.t()}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
action: "status_delete",
subject_id: subject_id
}) do
%ModerationLog{
data: %{
"actor" => user_to_map(actor),
"action" => "status_delete",
"subject_id" => subject_id,
"message" => ""
}
}
|> insert_log_entry_with_message()
def insert_log(%{actor: %User{}, action: action, subject_id: subject_id} = attrs)
when action == "status_delete" do
data =
attrs
|> prepare_log_data
|> Map.merge(%{"subject_id" => subject_id})
insert_log_entry_with_message(%ModerationLog{data: data})
end
@spec insert_log(%{actor: User, subject: User, action: String.t()}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{actor: %User{} = actor, subject: subject, action: action}) do
%ModerationLog{
data: %{
"actor" => user_to_map(actor),
"action" => action,
"subject" => user_to_map(subject),
"message" => ""
}
}
|> insert_log_entry_with_message()
def insert_log(%{actor: %User{}, subject: subject, action: _action} = attrs) do
data =
attrs
|> prepare_log_data
|> Map.merge(%{"subject" => user_to_map(subject)})
insert_log_entry_with_message(%ModerationLog{data: data})
end
@spec insert_log(%{actor: User, subjects: [User], action: String.t()}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{actor: %User{} = actor, subjects: subjects, action: action}) do
subjects = Enum.map(subjects, &user_to_map/1)
def insert_log(%{actor: %User{}, subjects: subjects, action: _action} = attrs) do
data =
attrs
|> prepare_log_data
|> Map.merge(%{"subjects" => user_to_map(subjects)})
%ModerationLog{
data: %{
"actor" => user_to_map(actor),
"action" => action,
"subjects" => subjects,
"message" => ""
}
}
|> insert_log_entry_with_message()
insert_log_entry_with_message(%ModerationLog{data: data})
end
@spec insert_log(%{actor: User, action: String.t(), followed: User, follower: User}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
followed: %User{} = followed,
follower: %User{} = follower,
action: "follow"
}) do
%ModerationLog{
data: %{
"actor" => user_to_map(actor),
"action" => "follow",
"followed" => user_to_map(followed),
"follower" => user_to_map(follower),
"message" => ""
}
}
|> insert_log_entry_with_message()
def insert_log(
%{
actor: %User{},
followed: %User{} = followed,
follower: %User{} = follower,
action: action
} = attrs
)
when action in ["unfollow", "follow"] do
data =
attrs
|> prepare_log_data
|> Map.merge(%{"followed" => user_to_map(followed), "follower" => user_to_map(follower)})
insert_log_entry_with_message(%ModerationLog{data: data})
end
@spec insert_log(%{actor: User, action: String.t(), followed: User, follower: User}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
followed: %User{} = followed,
follower: %User{} = follower,
action: "unfollow"
}) do
%ModerationLog{
data: %{
"actor" => user_to_map(actor),
"action" => "unfollow",
"followed" => user_to_map(followed),
"follower" => user_to_map(follower),
"message" => ""
}
}
|> insert_log_entry_with_message()
end
@spec insert_log(%{
actor: User,
action: String.t(),
nicknames: [String.t()],
tags: [String.t()]
}) :: {:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
nicknames: nicknames,
@ -305,27 +227,16 @@ def insert_log(%{
|> insert_log_entry_with_message()
end
@spec insert_log(%{actor: User, action: String.t(), target: String.t()}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
action: action,
target: target
})
def insert_log(%{actor: %User{}, action: action, target: target} = attrs)
when action in ["relay_follow", "relay_unfollow"] do
%ModerationLog{
data: %{
"actor" => user_to_map(actor),
"action" => action,
"target" => target,
"message" => ""
}
}
|> insert_log_entry_with_message()
data =
attrs
|> prepare_log_data
|> Map.merge(%{"target" => target})
insert_log_entry_with_message(%ModerationLog{data: data})
end
@spec insert_log(%{actor: User, action: String.t(), subject_id: String.t()}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{actor: %User{} = actor, action: "chat_message_delete", subject_id: subject_id}) do
%ModerationLog{
data: %{
@ -345,32 +256,27 @@ defp insert_log_entry_with_message(entry) do
end
defp user_to_map(users) when is_list(users) do
users |> Enum.map(&user_to_map/1)
Enum.map(users, &user_to_map/1)
end
defp user_to_map(%User{} = user) do
user
|> Map.from_struct()
|> Map.take([:id, :nickname])
|> Map.new(fn {k, v} -> {Atom.to_string(k), v} end)
|> Map.put("type", "user")
end
defp user_to_map(_), do: nil
defp report_to_map(%Activity{} = report) do
%{
"type" => "report",
"id" => report.id,
"state" => report.data["state"]
}
%{"type" => "report", "id" => report.id, "state" => report.data["state"]}
end
defp status_to_map(%Activity{} = status) do
%{
"type" => "status",
"id" => status.id
}
%{"type" => "status", "id" => status.id}
end
@spec get_log_entry_message(ModerationLog.t()) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@ -382,7 +288,6 @@ def get_log_entry_message(%ModerationLog{
"@#{actor_nickname} made @#{follower_nickname} #{action} @#{followed_nickname}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@ -393,7 +298,6 @@ def get_log_entry_message(%ModerationLog{
"@#{actor_nickname} deleted users: #{users_to_nicknames_string(subjects)}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@ -404,7 +308,6 @@ def get_log_entry_message(%ModerationLog{
"@#{actor_nickname} created users: #{users_to_nicknames_string(subjects)}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@ -415,7 +318,6 @@ def get_log_entry_message(%ModerationLog{
"@#{actor_nickname} activated users: #{users_to_nicknames_string(users)}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@ -426,7 +328,6 @@ def get_log_entry_message(%ModerationLog{
"@#{actor_nickname} deactivated users: #{users_to_nicknames_string(users)}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@ -437,7 +338,6 @@ def get_log_entry_message(%ModerationLog{
"@#{actor_nickname} approved users: #{users_to_nicknames_string(users)}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@ -451,7 +351,6 @@ def get_log_entry_message(%ModerationLog{
"@#{actor_nickname} added tags: #{tags_string} to users: #{nicknames_to_string(nicknames)}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@ -465,7 +364,6 @@ def get_log_entry_message(%ModerationLog{
"@#{actor_nickname} removed tags: #{tags_string} from users: #{nicknames_to_string(nicknames)}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@ -477,7 +375,6 @@ def get_log_entry_message(%ModerationLog{
"@#{actor_nickname} made #{users_to_nicknames_string(users)} #{permission}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@ -489,7 +386,6 @@ def get_log_entry_message(%ModerationLog{
"@#{actor_nickname} revoked #{permission} role from #{users_to_nicknames_string(users)}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@ -500,7 +396,6 @@ def get_log_entry_message(%ModerationLog{
"@#{actor_nickname} followed relay: #{target}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@ -511,42 +406,48 @@ def get_log_entry_message(%ModerationLog{
"@#{actor_nickname} unfollowed relay: #{target}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "report_update",
"subject" => %{"id" => subject_id, "state" => state, "type" => "report"}
}
}) do
"@#{actor_nickname} updated report ##{subject_id} with '#{state}' state"
def get_log_entry_message(
%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "report_update",
"subject" => %{"id" => subject_id, "state" => state, "type" => "report"}
}
} = log
) do
"@#{actor_nickname} updated report ##{subject_id}" <>
subject_actor_nickname(log, " (on user ", ")") <>
" with '#{state}' state"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "report_note",
"subject" => %{"id" => subject_id, "type" => "report"},
"text" => text
}
}) do
"@#{actor_nickname} added note '#{text}' to report ##{subject_id}"
def get_log_entry_message(
%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "report_note",
"subject" => %{"id" => subject_id, "type" => "report"},
"text" => text
}
} = log
) do
"@#{actor_nickname} added note '#{text}' to report ##{subject_id}" <>
subject_actor_nickname(log, " on user ")
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "report_note_delete",
"subject" => %{"id" => subject_id, "type" => "report"},
"text" => text
}
}) do
"@#{actor_nickname} deleted note '#{text}' from report ##{subject_id}"
def get_log_entry_message(
%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "report_note_delete",
"subject" => %{"id" => subject_id, "type" => "report"},
"text" => text
}
} = log
) do
"@#{actor_nickname} deleted note '#{text}' from report ##{subject_id}" <>
subject_actor_nickname(log, " on user ")
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@ -559,7 +460,6 @@ def get_log_entry_message(%ModerationLog{
"@#{actor_nickname} updated status ##{subject_id}, set visibility: '#{visibility}'"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@ -572,7 +472,6 @@ def get_log_entry_message(%ModerationLog{
"@#{actor_nickname} updated status ##{subject_id}, set sensitive: '#{sensitive}'"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@ -587,7 +486,6 @@ def get_log_entry_message(%ModerationLog{
}'"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@ -598,7 +496,6 @@ def get_log_entry_message(%ModerationLog{
"@#{actor_nickname} deleted status ##{subject_id}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@ -609,7 +506,6 @@ def get_log_entry_message(%ModerationLog{
"@#{actor_nickname} forced password reset for users: #{users_to_nicknames_string(subjects)}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@ -620,7 +516,6 @@ def get_log_entry_message(%ModerationLog{
"@#{actor_nickname} confirmed email for users: #{users_to_nicknames_string(subjects)}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@ -633,7 +528,6 @@ def get_log_entry_message(%ModerationLog{
}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@ -644,7 +538,6 @@ def get_log_entry_message(%ModerationLog{
"@#{actor_nickname} updated users: #{users_to_nicknames_string(subjects)}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@ -676,4 +569,16 @@ defp users_to_nicknames_string(users) do
|> Enum.map(&"@#{&1["nickname"]}")
|> Enum.join(", ")
end
defp subject_actor_nickname(%ModerationLog{data: data}, prefix_msg, postfix_msg \\ "") do
case data do
%{"subject_actor" => %{"nickname" => subject_actor}} ->
[prefix_msg, "@#{subject_actor}", postfix_msg]
|> Enum.reject(&(&1 == ""))
|> Enum.join()
_ ->
""
end
end
end

View file

@ -473,6 +473,18 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, max: name_limit)
|> validate_fields(true)
|> validate_non_local()
end
defp validate_non_local(cng) do
local? = get_field(cng, :local)
if local? do
cng
|> add_error(:local, "User is local, can't update with this changeset.")
else
cng
end
end
def update_changeset(struct, params \\ %{}) do
@ -914,7 +926,7 @@ def maybe_direct_follow(%User{} = follower, %User{} = followed) do
if not ap_enabled?(followed) do
follow(follower, followed)
else
{:ok, follower}
{:ok, follower, followed}
end
end
@ -940,11 +952,6 @@ def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do
true ->
FollowingRelationship.follow(follower, followed, state)
{:ok, _} = update_follower_count(followed)
follower
|> update_following_count()
end
end
@ -968,11 +975,6 @@ defp do_unfollow(%User{} = follower, %User{} = followed) do
case get_follow_state(follower, followed) do
state when state in [:follow_pending, :follow_accept] ->
FollowingRelationship.unfollow(follower, followed)
{:ok, followed} = update_follower_count(followed)
{:ok, follower} = update_following_count(follower)
{:ok, follower, followed}
nil ->
{:error, "Not subscribed!"}
@ -2449,4 +2451,8 @@ def sanitize_html(%User{} = user, filter) do
|> Map.put(:bio, HTML.filter_tags(user.bio, filter))
|> Map.put(:fields, fields)
end
def get_host(%User{ap_id: ap_id} = _user) do
URI.parse(ap_id).host
end
end

View file

@ -45,7 +45,7 @@ def perform(:follow_import, %User{} = follower, [_ | _] = identifiers) do
identifiers,
fn identifier ->
with {:ok, %User{} = followed} <- User.get_or_fetch(identifier),
{:ok, follower} <- User.maybe_direct_follow(follower, followed),
{:ok, follower, followed} <- User.maybe_direct_follow(follower, followed),
{:ok, _, _, _} <- CommonAPI.follow(follower, followed) do
followed
else

View file

@ -3,6 +3,14 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Utils do
@posix_error_codes ~w(
eacces eagain ebadf ebadmsg ebusy edeadlk edeadlock edquot eexist efault
efbig eftype eintr einval eio eisdir eloop emfile emlink emultihop
enametoolong enfile enobufs enodev enolck enolink enoent enomem enospc
enosr enostr enosys enotblk enotdir enotsup enxio eopnotsupp eoverflow
eperm epipe erange erofs espipe esrch estale etxtbsy exdev
)a
def compile_dir(dir) when is_binary(dir) do
dir
|> File.ls!()
@ -44,4 +52,12 @@ def tmp_dir(prefix \\ "") do
error -> error
end
end
@spec posix_error_message(atom()) :: binary()
def posix_error_message(code) when code in @posix_error_codes do
error_message = Gettext.dgettext(Pleroma.Web.Gettext, "posix_errors", "#{code}")
"(POSIX error: #{error_message})"
end
def posix_error_message(_), do: ""
end

View file

@ -20,6 +20,7 @@ defmodule Pleroma.Web do
below.
"""
alias Pleroma.Helpers.AuthHelper
alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug
alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Web.Plugs.ExpectAuthenticatedCheckPlug
@ -75,7 +76,7 @@ defp action(conn, params) do
defp maybe_drop_authentication_if_oauth_check_ignored(conn) do
if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) and
not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do
OAuthScopesPlug.drop_auth_info(conn)
AuthHelper.drop_auth_info(conn)
else
conn
end

View file

@ -47,10 +47,9 @@ def handle(
%User{} = followed <- User.get_cached_by_ap_id(actor),
%User{} = follower <- User.get_cached_by_ap_id(follower_id),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept) do
{:ok, _follower, followed} <-
FollowingRelationship.update(follower, followed, :follow_accept) do
Notification.update_notification_type(followed, follow_activity)
User.update_follower_count(followed)
User.update_following_count(follower)
end
{:ok, object, meta}
@ -99,7 +98,7 @@ def handle(
) do
with %User{} = follower <- User.get_cached_by_ap_id(following_user),
%User{} = followed <- User.get_cached_by_ap_id(followed_user),
{_, {:ok, _}, _, _} <-
{_, {:ok, _, _}, _, _} <-
{:following, User.follow(follower, followed, :follow_pending), follower, followed} do
if followed.local && !followed.is_locked do
{:ok, accept_data, _} = Builder.accept(followed, object)

View file

@ -50,10 +50,13 @@ def update(%{assigns: %{user: admin}, body_params: %{reports: reports}} = conn,
Enum.map(reports, fn report ->
case CommonAPI.update_report_state(report.id, report.state) do
{:ok, activity} ->
report = Activity.get_by_id_with_user_actor(activity.id)
ModerationLog.insert_log(%{
action: "report_update",
actor: admin,
subject: activity
subject: activity,
subject_actor: report.user_actor
})
activity
@ -73,11 +76,13 @@ def update(%{assigns: %{user: admin}, body_params: %{reports: reports}} = conn,
def notes_create(%{assigns: %{user: user}, body_params: %{content: content}} = conn, %{
id: report_id
}) do
with {:ok, _} <- ReportNote.create(user.id, report_id, content) do
with {:ok, _} <- ReportNote.create(user.id, report_id, content),
report <- Activity.get_by_id_with_user_actor(report_id) do
ModerationLog.insert_log(%{
action: "report_note",
actor: user,
subject: Activity.get_by_id(report_id),
subject: report,
subject_actor: report.user_actor,
text: content
})
@ -91,11 +96,13 @@ def notes_delete(%{assigns: %{user: user}} = conn, %{
id: note_id,
report_id: report_id
}) do
with {:ok, note} <- ReportNote.destroy(note_id) do
with {:ok, note} <- ReportNote.destroy(note_id),
report <- Activity.get_by_id_with_user_actor(report_id) do
ModerationLog.insert_log(%{
action: "report_note_delete",
actor: user,
subject: Activity.get_by_id(report_id),
subject: report,
subject_actor: report.user_actor,
text: note.content
})

View file

@ -27,7 +27,8 @@ def create_operation do
422 => Operation.response("Unprocessable Entity", "application/json", ApiError),
404 => Operation.response("Not Found", "application/json", ApiError),
400 => Operation.response("Bad Request", "application/json", ApiError),
409 => Operation.response("Conflict", "application/json", ApiError)
409 => Operation.response("Conflict", "application/json", ApiError),
500 => Operation.response("Error", "application/json", ApiError)
}
}
end

View file

@ -169,7 +169,8 @@ def delete_operation do
responses: %{
200 => ok_response(),
400 => Operation.response("Bad Request", "application/json", ApiError),
404 => Operation.response("Not Found", "application/json", ApiError)
404 => Operation.response("Not Found", "application/json", ApiError),
500 => Operation.response("Error", "application/json", ApiError)
}
}
end
@ -184,7 +185,8 @@ def update_operation do
parameters: [name_param()],
responses: %{
200 => Operation.response("Metadata", "application/json", metadata()),
400 => Operation.response("Bad Request", "application/json", ApiError)
400 => Operation.response("Bad Request", "application/json", ApiError),
500 => Operation.response("Error", "application/json", ApiError)
}
}
end

View file

@ -51,7 +51,7 @@ def most_recent_update(activities, user) do
def feed_logo do
case Pleroma.Config.get([:feed, :logo]) do
nil ->
"#{Pleroma.Web.base_url()}/static/logo.png"
"#{Pleroma.Web.base_url()}/static/logo.svg"
logo ->
"#{Pleroma.Web.base_url()}#{logo}"

View file

@ -6,6 +6,8 @@ defmodule Pleroma.Web.MastoFEController do
use Pleroma.Web, :controller
alias Pleroma.User
alias Pleroma.Web.MastodonAPI.AuthController
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Web.Plugs.OAuthScopesPlug
@ -26,27 +28,27 @@ defmodule Pleroma.Web.MastoFEController do
)
@doc "GET /web/*path"
def index(%{assigns: %{user: user, token: token}} = conn, _params)
when not is_nil(user) and not is_nil(token) do
conn
|> put_layout(false)
|> render("index.html",
token: token.token,
user: user,
custom_emojis: Pleroma.Emoji.get_all()
)
end
def index(conn, _params) do
conn
|> put_session(:return_to, conn.request_path)
|> redirect(to: "/web/login")
with %{assigns: %{user: %User{} = user, token: %Token{app_id: token_app_id} = token}} <- conn,
{:ok, %{id: ^token_app_id}} <- AuthController.local_mastofe_app() do
conn
|> put_layout(false)
|> render("index.html",
token: token.token,
user: user,
custom_emojis: Pleroma.Emoji.get_all()
)
else
_ ->
conn
|> put_session(:return_to, conn.request_path)
|> redirect(to: "/web/login")
end
end
@doc "GET /web/manifest.json"
def manifest(conn, _params) do
conn
|> render("manifest.json")
render(conn, "manifest.json")
end
@doc "PUT /api/web/settings: Backend-obscure settings blob for MastoFE, don't parse/reuse elsewhere"

View file

@ -25,7 +25,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
alias Pleroma.Web.MastodonAPI.MastodonAPIController
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.OAuth.OAuthController
alias Pleroma.Web.OAuth.OAuthView
alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.Plugs.RateLimiter
@ -103,7 +102,7 @@ def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
{:ok, user} <- TwitterAPI.register_user(params),
{_, {:ok, token}} <-
{:login, OAuthController.login(user, app, app.scopes)} do
json(conn, OAuthView.render("token.json", %{user: user, token: token}))
OAuthController.after_token_exchange(conn, %{user: user, token: token})
else
{:login, {:account_status, :confirmation_pending}} ->
json_response(conn, :ok, %{

View file

@ -7,10 +7,13 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
alias Pleroma.Helpers.AuthHelper
alias Pleroma.Helpers.UriHelper
alias Pleroma.User
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken
alias Pleroma.Web.TwitterAPI.TwitterAPI
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
@ -20,24 +23,35 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
@local_mastodon_name "Mastodon-Local"
@doc "GET /web/login"
def login(%{assigns: %{user: %User{}}} = conn, _params) do
redirect(conn, to: local_mastodon_root_path(conn))
end
# Local Mastodon FE login init action
def login(conn, %{"code" => auth_token}) do
with {:ok, app} <- get_or_make_app(),
# Local Mastodon FE login callback action
def login(conn, %{"code" => auth_token} = params) do
with {:ok, app} <- local_mastofe_app(),
{:ok, auth} <- Authorization.get_by_token(app, auth_token),
{:ok, token} <- Token.exchange_token(app, auth) do
{:ok, oauth_token} <- Token.exchange_token(app, auth) do
redirect_to =
conn
|> local_mastodon_post_login_path()
|> UriHelper.modify_uri_params(%{"access_token" => oauth_token.token})
conn
|> put_session(:oauth_token, token.token)
|> redirect(to: local_mastodon_root_path(conn))
|> AuthHelper.put_session_token(oauth_token.token)
|> redirect(to: redirect_to)
else
_ -> redirect_to_oauth_form(conn, params)
end
end
# Local Mastodon FE callback action
def login(conn, _) do
with {:ok, app} <- get_or_make_app() do
def login(conn, params) do
with %{assigns: %{user: %User{}, token: %Token{app_id: app_id}}} <- conn,
{:ok, %{id: ^app_id}} <- local_mastofe_app() do
redirect(conn, to: local_mastodon_post_login_path(conn))
else
_ -> redirect_to_oauth_form(conn, params)
end
end
defp redirect_to_oauth_form(conn, _params) do
with {:ok, app} <- local_mastofe_app() do
path =
o_auth_path(conn, :authorize,
response_type: "code",
@ -52,9 +66,16 @@ def login(conn, _) do
@doc "DELETE /auth/sign_out"
def logout(conn, _) do
conn
|> clear_session
|> redirect(to: "/")
conn =
with %{assigns: %{token: %Token{} = oauth_token}} <- conn,
session_token = AuthHelper.get_session_token(conn),
{:ok, %Token{token: ^session_token}} <- RevokeToken.revoke(oauth_token) do
AuthHelper.delete_session_token(conn)
else
_ -> conn
end
redirect(conn, to: "/")
end
@doc "POST /auth/password"
@ -66,7 +87,7 @@ def password_reset(conn, params) do
json_response(conn, :no_content, "")
end
defp local_mastodon_root_path(conn) do
defp local_mastodon_post_login_path(conn) do
case get_session(conn, :return_to) do
nil ->
masto_fe_path(conn, :index, ["getting-started"])
@ -77,9 +98,11 @@ defp local_mastodon_root_path(conn) do
end
end
@spec get_or_make_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
defp get_or_make_app do
%{client_name: @local_mastodon_name, redirect_uris: "."}
|> App.get_or_make(["read", "write", "follow", "push", "admin"])
@spec local_mastofe_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
def local_mastofe_app do
App.get_or_make(
%{client_name: @local_mastodon_name, redirect_uris: "."},
["read", "write", "follow", "push", "admin"]
)
end
end

View file

@ -187,18 +187,14 @@ defp do_render("show.json", %{user: user} = opts) do
header_static = User.banner_url(user) |> MediaProxy.preview_url(static: true)
following_count =
if !user.hide_follows_count or !user.hide_follows or opts[:for] == user do
user.following_count || 0
else
0
end
if !user.hide_follows_count or !user.hide_follows or opts[:for] == user,
do: user.following_count,
else: 0
followers_count =
if !user.hide_followers_count or !user.hide_followers or opts[:for] == user do
user.follower_count || 0
else
0
end
if !user.hide_followers_count or !user.hide_followers or opts[:for] == user,
do: user.follower_count,
else: 0
bot = user.actor_type == "Service"

View file

@ -9,6 +9,7 @@ defmodule Pleroma.Web.OAuth.Authorization do
alias Pleroma.User
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Token
import Ecto.Changeset
import Ecto.Query
@ -53,7 +54,8 @@ defp add_token(changeset) do
end
defp add_lifetime(changeset) do
put_change(changeset, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10))
lifespan = Token.lifespan()
put_change(changeset, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), lifespan))
end
@spec use_changeset(Authtorizatiton.t(), map()) :: Changeset.t()

View file

@ -13,7 +13,6 @@ defmodule Pleroma.Web.OAuth.MFAController do
alias Pleroma.Web.Auth.TOTPAuthenticator
alias Pleroma.Web.OAuth.MFAView, as: View
alias Pleroma.Web.OAuth.OAuthController
alias Pleroma.Web.OAuth.OAuthView
alias Pleroma.Web.OAuth.Token
plug(:fetch_session when action in [:show, :verify])
@ -75,7 +74,7 @@ def challenge(conn, %{"mfa_token" => mfa_token} = params) do
{:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token),
{:ok, _} <- validates_challenge(user, params),
{:ok, token} <- Token.exchange_token(app, auth) do
json(conn, OAuthView.render("token.json", %{user: user, token: token}))
OAuthController.after_token_exchange(conn, %{user: user, token: token})
else
_error ->
conn

View file

@ -5,6 +5,7 @@
defmodule Pleroma.Web.OAuth.OAuthController do
use Pleroma.Web, :controller
alias Pleroma.Helpers.AuthHelper
alias Pleroma.Helpers.UriHelper
alias Pleroma.Maps
alias Pleroma.MFA
@ -79,6 +80,13 @@ defp do_authorize(%Plug.Conn{} = conn, params) do
available_scopes = (app && app.scopes) || []
scopes = Scopes.fetch_scopes(params, available_scopes)
user =
with %{assigns: %{user: %User{} = user}} <- conn do
user
else
_ -> nil
end
scopes =
if scopes == [] do
available_scopes
@ -88,6 +96,8 @@ defp do_authorize(%Plug.Conn{} = conn, params) do
# Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
render(conn, Authenticator.auth_template(), %{
user: user,
app: app && Map.delete(app, :client_secret),
response_type: params["response_type"],
client_id: params["client_id"],
available_scopes: available_scopes,
@ -131,11 +141,13 @@ defp handle_existing_authorization(
end
end
def create_authorization(
%Plug.Conn{} = conn,
%{"authorization" => _} = params,
opts \\ []
) do
def create_authorization(_, _, opts \\ [])
def create_authorization(%Plug.Conn{assigns: %{user: %User{} = user}} = conn, params, []) do
create_authorization(conn, params, user: user)
end
def create_authorization(%Plug.Conn{} = conn, %{"authorization" => _} = params, opts) do
with {:ok, auth, user} <- do_create_authorization(conn, params, opts[:user]),
{:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)} do
after_create_authorization(conn, auth, params)
@ -248,7 +260,7 @@ def token_exchange(
with {:ok, app} <- Token.Utils.fetch_app(conn),
{:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token),
{:ok, token} <- RefreshToken.grant(token) do
json(conn, OAuthView.render("token.json", %{user: user, token: token}))
after_token_exchange(conn, %{user: user, token: token})
else
_error -> render_invalid_credentials_error(conn)
end
@ -260,7 +272,7 @@ def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"}
{:ok, auth} <- Authorization.get_by_token(app, fixed_token),
%User{} = user <- User.get_cached_by_id(auth.user_id),
{:ok, token} <- Token.exchange_token(app, auth) do
json(conn, OAuthView.render("token.json", %{user: user, token: token}))
after_token_exchange(conn, %{user: user, token: token})
else
error ->
handle_token_exchange_error(conn, error)
@ -275,7 +287,7 @@ def token_exchange(
{:ok, app} <- Token.Utils.fetch_app(conn),
requested_scopes <- Scopes.fetch_scopes(params, app.scopes),
{:ok, token} <- login(user, app, requested_scopes) do
json(conn, OAuthView.render("token.json", %{user: user, token: token}))
after_token_exchange(conn, %{user: user, token: token})
else
error ->
handle_token_exchange_error(conn, error)
@ -298,7 +310,7 @@ def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"}
with {:ok, app} <- Token.Utils.fetch_app(conn),
{:ok, auth} <- Authorization.create_authorization(app, %User{}),
{:ok, token} <- Token.exchange_token(app, auth) do
json(conn, OAuthView.render("token.json", %{token: token}))
after_token_exchange(conn, %{token: token})
else
_error ->
handle_token_exchange_error(conn, :invalid_credentails)
@ -308,6 +320,12 @@ def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"}
# Bad request
def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
def after_token_exchange(%Plug.Conn{} = conn, %{token: token} = view_params) do
conn
|> AuthHelper.put_session_token(token.token)
|> json(OAuthView.render("token.json", view_params))
end
defp handle_token_exchange_error(%Plug.Conn{} = conn, {:mfa_required, user, auth, _}) do
conn
|> put_status(:forbidden)
@ -361,9 +379,17 @@ defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do
render_invalid_credentials_error(conn)
end
def token_revoke(%Plug.Conn{} = conn, %{"token" => _token} = params) do
with {:ok, app} <- Token.Utils.fetch_app(conn),
{:ok, _token} <- RevokeToken.revoke(app, params) do
def token_revoke(%Plug.Conn{} = conn, %{"token" => token}) do
with {:ok, %Token{} = oauth_token} <- Token.get_by_token(token),
{:ok, oauth_token} <- RevokeToken.revoke(oauth_token) do
conn =
with session_token = AuthHelper.get_session_token(conn),
%Token{token: ^session_token} <- oauth_token do
AuthHelper.delete_session_token(conn)
else
_ -> conn
end
json(conn, %{})
else
_error ->

View file

@ -13,7 +13,7 @@ def render("token.json", %{token: token} = opts) do
token_type: "Bearer",
access_token: token.token,
refresh_token: token.refresh_token,
expires_in: expires_in(),
expires_in: NaiveDateTime.diff(token.valid_until, NaiveDateTime.utc_now()),
scope: Enum.join(token.scopes, " "),
created_at: Utils.format_created_at(token)
}
@ -25,6 +25,4 @@ def render("token.json", %{token: token} = opts) do
response
end
end
defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600)
end

View file

@ -27,6 +27,18 @@ defmodule Pleroma.Web.OAuth.Token do
timestamps()
end
def lifespan do
Pleroma.Config.get!([:oauth2, :token_expires_in])
end
@doc "Gets token by unique access token"
@spec get_by_token(String.t()) :: {:ok, t()} | {:error, :not_found}
def get_by_token(token) do
token
|> Query.get_by_token()
|> Repo.find_resource()
end
@doc "Gets token for app by access token"
@spec get_by_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found}
def get_by_token(%App{id: app_id} = _app, token) do
@ -75,11 +87,11 @@ defp put_refresh_token(changeset, attrs) do
end
defp put_valid_until(changeset, attrs) do
expires_in =
Map.get(attrs, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), expires_in()))
valid_until =
Map.get(attrs, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), lifespan()))
changeset
|> change(%{valid_until: expires_in})
|> change(%{valid_until: valid_until})
|> validate_required([:valid_until])
end
@ -130,6 +142,4 @@ def is_expired?(%__MODULE__{valid_until: valid_until}) do
end
def is_expired?(_), do: false
defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600)
end

View file

@ -42,7 +42,10 @@ def create(%{body_params: params} = conn, %{name: pack_name}) do
|> json(%{error: "pack name, shortcode or filename cannot be empty"})
{:error, _} = error ->
handle_error(conn, error, %{pack_name: pack_name})
handle_error(conn, error, %{
pack_name: pack_name,
message: "Unexpected error occurred while adding file to pack."
})
end
end
@ -69,7 +72,11 @@ def update(%{body_params: %{shortcode: shortcode} = params} = conn, %{name: pack
|> json(%{error: "new_shortcode or new_filename cannot be empty"})
{:error, _} = error ->
handle_error(conn, error, %{pack_name: pack_name, code: shortcode})
handle_error(conn, error, %{
pack_name: pack_name,
code: shortcode,
message: "Unexpected error occurred while updating."
})
end
end
@ -84,7 +91,11 @@ def delete(conn, %{name: pack_name, shortcode: shortcode}) do
|> json(%{error: "pack name or shortcode cannot be empty"})
{:error, _} = error ->
handle_error(conn, error, %{pack_name: pack_name, code: shortcode})
handle_error(conn, error, %{
pack_name: pack_name,
code: shortcode,
message: "Unexpected error occurred while deleting emoji file."
})
end
end
@ -94,18 +105,24 @@ defp handle_error(conn, {:error, :doesnt_exist}, %{code: emoji_code}) do
|> json(%{error: "Emoji \"#{emoji_code}\" does not exist"})
end
defp handle_error(conn, {:error, :not_found}, %{pack_name: pack_name}) do
defp handle_error(conn, {:error, :enoent}, %{pack_name: pack_name}) do
conn
|> put_status(:not_found)
|> json(%{error: "pack \"#{pack_name}\" is not found"})
end
defp handle_error(conn, {:error, _}, _) do
render_error(
conn,
:internal_server_error,
"Unexpected error occurred while adding file to pack."
)
defp handle_error(conn, {:error, error}, opts) do
message =
[
Map.get(opts, :message, "Unexpected error occurred."),
Pleroma.Utils.posix_error_message(error)
]
|> Enum.join(" ")
|> String.trim()
conn
|> put_status(:internal_server_error)
|> json(%{error: message})
end
defp get_filename(%Plug.Upload{filename: filename}), do: filename

View file

@ -71,7 +71,7 @@ def show(conn, %{name: name, page: page, page_size: page_size}) do
with {:ok, pack} <- Pack.show(name: name, page: page, page_size: page_size) do
json(conn, pack)
else
{:error, :not_found} ->
{:error, :enoent} ->
conn
|> put_status(:not_found)
|> json(%{error: "Pack #{name} does not exist"})
@ -80,6 +80,17 @@ def show(conn, %{name: name, page: page, page_size: page_size}) do
conn
|> put_status(:bad_request)
|> json(%{error: "pack name cannot be empty"})
{:error, error} ->
error_message =
add_posix_error(
"Failed to get the contents of the `#{name}` pack.",
error
)
conn
|> put_status(:internal_server_error)
|> json(%{error: error_message})
end
end
@ -95,7 +106,7 @@ def archive(conn, %{name: name}) do
"Pack #{name} cannot be downloaded from this instance, either pack sharing was disabled for this pack or some files are missing"
})
{:error, :not_found} ->
{:error, :enoent} ->
conn
|> put_status(:not_found)
|> json(%{error: "Pack #{name} does not exist"})
@ -116,10 +127,10 @@ def download(%{body_params: %{url: url, name: name} = params} = conn, _) do
|> put_status(:internal_server_error)
|> json(%{error: "SHA256 for the pack doesn't match the one sent by the server"})
{:error, e} ->
{:error, error} ->
conn
|> put_status(:internal_server_error)
|> json(%{error: e})
|> json(%{error: error})
end
end
@ -139,12 +150,16 @@ def create(conn, %{name: name}) do
|> put_status(:bad_request)
|> json(%{error: "pack name cannot be empty"})
{:error, _} ->
render_error(
conn,
:internal_server_error,
"Unexpected error occurred while creating pack."
)
{:error, error} ->
error_message =
add_posix_error(
"Unexpected error occurred while creating pack.",
error
)
conn
|> put_status(:internal_server_error)
|> json(%{error: error_message})
end
end
@ -164,10 +179,12 @@ def delete(conn, %{name: name}) do
|> put_status(:bad_request)
|> json(%{error: "pack name cannot be empty"})
{:error, _, _} ->
{:error, error, _} ->
error_message = add_posix_error("Couldn't delete the `#{name}` pack", error)
conn
|> put_status(:internal_server_error)
|> json(%{error: "Couldn't delete the pack #{name}"})
|> json(%{error: error_message})
end
end
@ -180,12 +197,16 @@ def update(%{body_params: %{metadata: metadata}} = conn, %{name: name}) do
|> put_status(:bad_request)
|> json(%{error: "The fallback archive does not have all files specified in pack.json"})
{:error, _} ->
render_error(
conn,
:internal_server_error,
"Unexpected error occurred while updating pack metadata."
)
{:error, error} ->
error_message =
add_posix_error(
"Unexpected error occurred while updating pack metadata.",
error
)
conn
|> put_status(:internal_server_error)
|> json(%{error: error_message})
end
end
@ -204,4 +225,10 @@ def import_from_filesystem(conn, _params) do
|> json(%{error: "Error accessing emoji pack directory"})
end
end
defp add_posix_error(msg, error) do
[msg, Pleroma.Utils.posix_error_message(error)]
|> Enum.join(" ")
|> String.trim()
end
end

View file

@ -5,21 +5,14 @@
defmodule Pleroma.Web.Plugs.AdminSecretAuthenticationPlug do
import Plug.Conn
alias Pleroma.Helpers.AuthHelper
alias Pleroma.User
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.Plugs.RateLimiter
def init(options) do
options
end
def secret_token do
case Pleroma.Config.get(:admin_token) do
blank when blank in [nil, ""] -> nil
token -> token
end
end
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
def call(conn, _) do
@ -30,7 +23,7 @@ def call(conn, _) do
end
end
def authenticate(%{params: %{"admin_token" => admin_token}} = conn) do
defp authenticate(%{params: %{"admin_token" => admin_token}} = conn) do
if admin_token == secret_token() do
assign_admin_user(conn)
else
@ -38,7 +31,7 @@ def authenticate(%{params: %{"admin_token" => admin_token}} = conn) do
end
end
def authenticate(conn) do
defp authenticate(conn) do
token = secret_token()
case get_req_header(conn, "x-admin-token") do
@ -48,10 +41,17 @@ def authenticate(conn) do
end
end
defp secret_token do
case Pleroma.Config.get(:admin_token) do
blank when blank in [nil, ""] -> nil
token -> token
end
end
defp assign_admin_user(conn) do
conn
|> assign(:user, %User{is_admin: true})
|> OAuthScopesPlug.skip_plug()
|> AuthHelper.skip_oauth()
end
defp handle_bad_token(conn) do

View file

@ -3,6 +3,9 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.AuthenticationPlug do
@moduledoc "Password authentication plug."
alias Pleroma.Helpers.AuthHelper
alias Pleroma.User
import Plug.Conn
@ -11,6 +14,30 @@ defmodule Pleroma.Web.Plugs.AuthenticationPlug do
def init(options), do: options
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
def call(
%{
assigns: %{
auth_user: %{password_hash: password_hash} = auth_user,
auth_credentials: %{password: password}
}
} = conn,
_
) do
if checkpw(password, password_hash) do
{:ok, auth_user} = maybe_update_password(auth_user, password)
conn
|> assign(:user, auth_user)
|> AuthHelper.skip_oauth()
else
conn
end
end
def call(conn, _), do: conn
def checkpw(password, "$6" <> _ = password_hash) do
:crypt.crypt(password, password_hash) == password_hash
end
@ -40,40 +67,6 @@ def maybe_update_password(%User{password_hash: "$6" <> _} = user, password) do
def maybe_update_password(user, _), do: {:ok, user}
defp do_update_password(user, password) do
user
|> User.password_update_changeset(%{
"password" => password,
"password_confirmation" => password
})
|> Pleroma.Repo.update()
User.reset_password(user, %{password: password, password_confirmation: password})
end
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
def call(
%{
assigns: %{
auth_user: %{password_hash: password_hash} = auth_user,
auth_credentials: %{password: password}
}
} = conn,
_
) do
if checkpw(password, password_hash) do
{:ok, auth_user} = maybe_update_password(auth_user, password)
conn
|> assign(:user, auth_user)
|> Pleroma.Web.Plugs.OAuthScopesPlug.skip_plug()
else
conn
end
end
def call(%{assigns: %{auth_credentials: %{password: _}}} = conn, _) do
Pbkdf2.no_user_verify()
conn
end
def call(conn, _), do: conn
end

View file

@ -3,6 +3,12 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.BasicAuthDecoderPlug do
@moduledoc """
Decodes HTTP Basic Auth information and assigns `:auth_credentials`.
NOTE: no checks are performed at this step, auth_credentials/username could be easily faked.
"""
import Plug.Conn
def init(options) do

View file

@ -7,8 +7,22 @@ defmodule Pleroma.Web.Plugs.DigestPlug do
require Logger
def read_body(conn, opts) do
digest_algorithm =
with [digest_header] <- Conn.get_req_header(conn, "digest") do
digest_header
|> String.split("=", parts: 2)
|> List.first()
else
_ -> "SHA-256"
end
unless String.downcase(digest_algorithm) == "sha-256" do
raise ArgumentError,
message: "invalid value for digest algorithm, got: #{digest_algorithm}"
end
{:ok, body, conn} = Conn.read_body(conn, opts)
digest = "SHA-256=" <> (:crypto.hash(:sha256, body) |> Base.encode64())
{:ok, body, Conn.assign(conn, :digest, digest)}
encoded_digest = :crypto.hash(:sha256, body) |> Base.encode64()
{:ok, body, Conn.assign(conn, :digest, "#{digest_algorithm}=#{encoded_digest}")}
end
end

View file

@ -1,18 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.EnsureUserKeyPlug do
import Plug.Conn
def init(opts) do
opts
end
def call(%{assigns: %{user: _}} = conn, _), do: conn
def call(conn, _) do
conn
|> assign(:user, nil)
end
end

View file

@ -0,0 +1,36 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug do
import Plug.Conn
alias Pleroma.Helpers.AuthHelper
alias Pleroma.User
alias Pleroma.Web.OAuth.Token
@moduledoc "Ensures presence and consistency of :user and :token assigns."
def init(opts) do
opts
end
def call(%{assigns: %{user: %User{id: user_id}} = assigns} = conn, _) do
with %Token{user_id: ^user_id} <- assigns[:token] do
conn
else
%Token{} ->
# A safety net for abnormal (unexpected) scenario: :token belongs to another user
AuthHelper.drop_auth_info(conn)
_ ->
assign(conn, :token, nil)
end
end
def call(conn, _) do
conn
|> assign(:user, nil)
|> assign(:token, nil)
end
end

View file

@ -1,41 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.LegacyAuthenticationPlug do
import Plug.Conn
alias Pleroma.User
def init(options) do
options
end
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
def call(
%{
assigns: %{
auth_user: %{password_hash: "$6$" <> _ = password_hash} = auth_user,
auth_credentials: %{password: password}
}
} = conn,
_
) do
with ^password_hash <- :crypt.crypt(password, password_hash),
{:ok, user} <-
User.reset_password(auth_user, %{password: password, password_confirmation: password}) do
conn
|> assign(:auth_user, user)
|> assign(:user, user)
|> Pleroma.Web.Plugs.OAuthScopesPlug.skip_plug()
else
_ ->
conn
end
end
def call(conn, _) do
conn
end
end

View file

@ -3,6 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do
alias Pleroma.Helpers.AuthHelper
alias Pleroma.Signature
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Utils
@ -12,6 +13,47 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do
def init(options), do: options
def call(%{assigns: %{user: %User{}}} = conn, _opts), do: conn
# if this has payload make sure it is signed by the same actor that made it
def call(%{assigns: %{valid_signature: true}, params: %{"actor" => actor}} = conn, _opts) do
with actor_id <- Utils.get_ap_id(actor),
{:user, %User{} = user} <- {:user, user_from_key_id(conn)},
{:user_match, true} <- {:user_match, user.ap_id == actor_id} do
conn
|> assign(:user, user)
|> AuthHelper.skip_oauth()
else
{:user_match, false} ->
Logger.debug("Failed to map identity from signature (payload actor mismatch)")
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{inspect(actor)}")
assign(conn, :valid_signature, false)
# remove me once testsuite uses mapped capabilities instead of what we do now
{:user, nil} ->
Logger.debug("Failed to map identity from signature (lookup failure)")
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{actor}")
conn
end
end
# no payload, probably a signed fetch
def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
with %User{} = user <- user_from_key_id(conn) do
conn
|> assign(:user, user)
|> AuthHelper.skip_oauth()
else
_ ->
Logger.debug("Failed to map identity from signature (no payload actor mismatch)")
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}")
assign(conn, :valid_signature, false)
end
end
# no signature at all
def call(conn, _opts), do: conn
defp key_id_from_conn(conn) do
with %{"keyId" => key_id} <- HTTPSignatures.signature_for_conn(conn),
{:ok, ap_id} <- Signature.key_id_to_actor_id(key_id) do
@ -31,41 +73,4 @@ defp user_from_key_id(conn) do
nil
end
end
def call(%{assigns: %{user: _}} = conn, _opts), do: conn
# if this has payload make sure it is signed by the same actor that made it
def call(%{assigns: %{valid_signature: true}, params: %{"actor" => actor}} = conn, _opts) do
with actor_id <- Utils.get_ap_id(actor),
{:user, %User{} = user} <- {:user, user_from_key_id(conn)},
{:user_match, true} <- {:user_match, user.ap_id == actor_id} do
assign(conn, :user, user)
else
{:user_match, false} ->
Logger.debug("Failed to map identity from signature (payload actor mismatch)")
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{inspect(actor)}")
assign(conn, :valid_signature, false)
# remove me once testsuite uses mapped capabilities instead of what we do now
{:user, nil} ->
Logger.debug("Failed to map identity from signature (lookup failure)")
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{actor}")
conn
end
end
# no payload, probably a signed fetch
def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
with %User{} = user <- user_from_key_id(conn) do
assign(conn, :user, user)
else
_ ->
Logger.debug("Failed to map identity from signature (no payload actor mismatch)")
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}")
assign(conn, :valid_signature, false)
end
end
# no signature at all
def call(conn, _opts), do: conn
end

View file

@ -3,9 +3,12 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.OAuthPlug do
@moduledoc "Performs OAuth authentication by token from params / headers / cookies."
import Plug.Conn
import Ecto.Query
alias Pleroma.Helpers.AuthHelper
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.OAuth.App
@ -17,45 +20,26 @@ def init(options), do: options
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
def call(%{params: %{"access_token" => access_token}} = conn, _) do
with {:ok, user, token_record} <- fetch_user_and_token(access_token) do
conn
|> assign(:token, token_record)
|> assign(:user, user)
else
_ ->
# token found, but maybe only with app
with {:ok, app, token_record} <- fetch_app_and_token(access_token) do
conn
|> assign(:token, token_record)
|> assign(:app, app)
else
_ -> conn
end
end
end
def call(conn, _) do
case fetch_token_str(conn) do
{:ok, token} ->
with {:ok, user, token_record} <- fetch_user_and_token(token) do
conn
|> assign(:token, token_record)
|> assign(:user, user)
else
_ ->
# token found, but maybe only with app
with {:ok, app, token_record} <- fetch_app_and_token(token) do
conn
|> assign(:token, token_record)
|> assign(:app, app)
else
_ -> conn
end
end
_ ->
with {:ok, token_str} <- fetch_token_str(conn) do
with {:ok, user, user_token} <- fetch_user_and_token(token_str),
false <- Token.is_expired?(user_token) do
conn
|> assign(:token, user_token)
|> assign(:user, user)
else
_ ->
with {:ok, app, app_token} <- fetch_app_and_token(token_str),
false <- Token.is_expired?(app_token) do
conn
|> assign(:token, app_token)
|> assign(:app, app)
else
_ -> conn
end
end
else
_ -> conn
end
end
@ -70,7 +54,6 @@ defp fetch_user_and_token(token) do
preload: [user: user]
)
# credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
with %Token{user: user} = token_record <- Repo.one(query) do
{:ok, user, token_record}
end
@ -86,29 +69,23 @@ defp fetch_app_and_token(token) do
end
end
# Gets token from session by :oauth_token key
# Gets token string from conn (in params / headers / session)
#
@spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}
defp fetch_token_from_session(conn) do
case get_session(conn, :oauth_token) do
nil -> :no_token_found
token -> {:ok, token}
end
@spec fetch_token_str(Plug.Conn.t() | list(String.t())) :: :no_token_found | {:ok, String.t()}
defp fetch_token_str(%Plug.Conn{params: %{"access_token" => access_token}} = _conn) do
{:ok, access_token}
end
# Gets token from headers
#
@spec fetch_token_str(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}
defp fetch_token_str(%Plug.Conn{} = conn) do
headers = get_req_header(conn, "authorization")
with :no_token_found <- fetch_token_str(headers),
do: fetch_token_from_session(conn)
with {:ok, token} <- fetch_token_str(headers) do
{:ok, token}
else
_ -> fetch_token_from_session(conn)
end
end
@spec fetch_token_str(Keyword.t()) :: :no_token_found | {:ok, String.t()}
defp fetch_token_str([]), do: :no_token_found
defp fetch_token_str([token | tail]) do
trimmed_token = String.trim(token)
@ -117,4 +94,14 @@ defp fetch_token_str([token | tail]) do
_ -> fetch_token_str(tail)
end
end
defp fetch_token_str([]), do: :no_token_found
@spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}
defp fetch_token_from_session(conn) do
case AuthHelper.get_session_token(conn) do
nil -> :no_token_found
token -> {:ok, token}
end
end
end

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Web.Plugs.OAuthScopesPlug do
import Pleroma.Web.Gettext
alias Pleroma.Config
alias Pleroma.Helpers.AuthHelper
use Pleroma.Web, :plug
@ -28,7 +29,7 @@ def perform(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do
conn
options[:fallback] == :proceed_unauthenticated ->
drop_auth_info(conn)
AuthHelper.drop_auth_info(conn)
true ->
missing_scopes = scopes -- matched_scopes
@ -44,15 +45,6 @@ def perform(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do
end
end
@doc "Drops authentication info from connection"
def drop_auth_info(conn) do
# To simplify debugging, setting a private variable on `conn` if auth info is dropped
conn
|> put_private(:authentication_ignored, true)
|> assign(:user, nil)
|> assign(:token, nil)
end
@doc "Keeps those of `scopes` which are descendants of `supported_scopes`"
def filter_descendants(scopes, supported_scopes) do
Enum.filter(

View file

@ -1,21 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.SessionAuthenticationPlug do
import Plug.Conn
def init(options) do
options
end
def call(conn, _) do
with saved_user_id <- get_session(conn, :user_id),
%{auth_user: %{id: ^saved_user_id}} <- conn.assigns do
conn
|> assign(:user, conn.assigns.auth_user)
else
_ -> conn
end
end
end

View file

@ -3,16 +3,15 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.SetUserSessionIdPlug do
import Plug.Conn
alias Pleroma.User
alias Pleroma.Helpers.AuthHelper
alias Pleroma.Web.OAuth.Token
def init(opts) do
opts
end
def call(%{assigns: %{user: %User{id: id}}} = conn, _) do
conn
|> put_session(:user_id, id)
def call(%{assigns: %{token: %Token{} = oauth_token}} = conn, _) do
AuthHelper.put_session_token(conn, oauth_token.token)
end
def call(conn, _), do: conn

View file

@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.UserEnabledPlug do
import Plug.Conn
alias Pleroma.Helpers.AuthHelper
alias Pleroma.User
def init(options) do
@ -11,9 +11,10 @@ def init(options) do
end
def call(%{assigns: %{user: %User{} = user}} = conn, _) do
case User.account_status(user) do
:active -> conn
_ -> assign(conn, :user, nil)
if User.account_status(user) == :active do
conn
else
AuthHelper.drop_auth_info(conn)
end
end

View file

@ -3,6 +3,12 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.UserFetcherPlug do
@moduledoc """
Assigns `:auth_user` basing on `:auth_credentials`.
NOTE: no checks are performed at this step, auth_credentials/username could be easily faked.
"""
alias Pleroma.User
import Plug.Conn

View file

@ -34,6 +34,7 @@ defmodule Pleroma.Web.Router do
plug(:fetch_session)
plug(Pleroma.Web.Plugs.OAuthPlug)
plug(Pleroma.Web.Plugs.UserEnabledPlug)
plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug)
end
pipeline :expect_authentication do
@ -48,15 +49,13 @@ defmodule Pleroma.Web.Router do
plug(Pleroma.Web.Plugs.OAuthPlug)
plug(Pleroma.Web.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Web.Plugs.UserFetcherPlug)
plug(Pleroma.Web.Plugs.SessionAuthenticationPlug)
plug(Pleroma.Web.Plugs.LegacyAuthenticationPlug)
plug(Pleroma.Web.Plugs.AuthenticationPlug)
end
pipeline :after_auth do
plug(Pleroma.Web.Plugs.UserEnabledPlug)
plug(Pleroma.Web.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Web.Plugs.EnsureUserKeyPlug)
plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug)
end
pipeline :base_api do
@ -100,7 +99,7 @@ defmodule Pleroma.Web.Router do
pipeline :pleroma_html do
plug(:browser)
plug(:authenticate)
plug(Pleroma.Web.Plugs.EnsureUserKeyPlug)
plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug)
end
pipeline :well_known do
@ -292,7 +291,6 @@ defmodule Pleroma.Web.Router do
post("/main/ostatus", UtilController, :remote_subscribe)
get("/ostatus_subscribe", RemoteFollowController, :follow)
post("/ostatus_subscribe", RemoteFollowController, :do_follow)
end
@ -321,20 +319,26 @@ defmodule Pleroma.Web.Router do
end
scope "/oauth", Pleroma.Web.OAuth do
scope [] do
pipe_through(:oauth)
get("/authorize", OAuthController, :authorize)
end
post("/authorize", OAuthController, :create_authorization)
post("/token", OAuthController, :token_exchange)
post("/revoke", OAuthController, :token_revoke)
get("/registration_details", OAuthController, :registration_details)
post("/mfa/challenge", MFAController, :challenge)
post("/mfa/verify", MFAController, :verify, as: :mfa_verify)
get("/mfa", MFAController, :show)
scope [] do
pipe_through(:oauth)
get("/authorize", OAuthController, :authorize)
post("/authorize", OAuthController, :create_authorization)
end
scope [] do
pipe_through(:fetch_session)
post("/token", OAuthController, :token_exchange)
post("/revoke", OAuthController, :token_revoke)
post("/mfa/challenge", MFAController, :challenge)
end
scope [] do
pipe_through(:browser)

View file

@ -36,9 +36,8 @@ def registry, do: @registry
) ::
{:ok, topic :: String.t()} | {:error, :bad_topic} | {:error, :unauthorized}
def get_topic_and_add_socket(stream, user, oauth_token, params \\ %{}) do
case get_topic(stream, user, oauth_token, params) do
{:ok, topic} -> add_socket(topic, user)
error -> error
with {:ok, topic} <- get_topic(stream, user, oauth_token, params) do
add_socket(topic, user)
end
end
@ -70,10 +69,10 @@ def get_topic("public:remote:media", _user, _oauth_token, %{"instance" => instan
def get_topic(
stream,
%User{id: user_id} = user,
%Token{user_id: token_user_id} = oauth_token,
%Token{user_id: user_id} = oauth_token,
_params
)
when stream in @user_streams and user_id == token_user_id do
when stream in @user_streams do
# Note: "read" works for all user streams (not mentioning it since it's an ancestor scope)
required_scopes =
if stream == "user:notification" do
@ -97,10 +96,9 @@ def get_topic(stream, _user, _oauth_token, _params) when stream in @user_streams
def get_topic(
"list",
%User{id: user_id} = user,
%Token{user_id: token_user_id} = oauth_token,
%Token{user_id: user_id} = oauth_token,
%{"list" => id}
)
when user_id == token_user_id do
) do
cond do
OAuthScopesPlug.filter_descendants(["read", "read:lists"], oauth_token.scopes) == [] ->
{:error, :unauthorized}
@ -137,16 +135,10 @@ def remove_socket(topic) do
def stream(topics, items) do
if should_env_send?() do
List.wrap(topics)
|> Enum.each(fn topic ->
List.wrap(items)
|> Enum.each(fn item ->
spawn(fn -> do_stream(topic, item) end)
end)
end)
for topic <- List.wrap(topics), item <- List.wrap(items) do
spawn(fn -> do_stream(topic, item) end)
end
end
:ok
end
def filtered_by_user?(user, item, streamed_type \\ :activity)
@ -160,8 +152,7 @@ def filtered_by_user?(%User{} = user, %Activity{} = item, streamed_type) do
domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
with parent <- Object.normalize(item) || item,
true <-
Enum.all?([blocked_ap_ids, muted_ap_ids], &(item.actor not in &1)),
true <- Enum.all?([blocked_ap_ids, muted_ap_ids], &(item.actor not in &1)),
true <- item.data["type"] != "Announce" || item.actor not in reblog_muted_ap_ids,
true <-
!(streamed_type == :activity && item.data["type"] == "Announce" &&
@ -195,6 +186,19 @@ defp do_stream("direct", item) do
end)
end
defp do_stream("follow_relationship", item) do
text = StreamerView.render("follow_relationships_update.json", item)
user_topic = "user:#{item.follower.id}"
Logger.debug("Trying to push follow relationship update to #{user_topic}\n\n")
Registry.dispatch(@registry, user_topic, fn list ->
Enum.each(list, fn {pid, _auth} ->
send(pid, {:text, text})
end)
end)
end
defp do_stream("participation", participation) do
user_topic = "direct:#{participation.user_id}"
Logger.debug("Trying to push a conversation participation to #{user_topic}\n\n")

View file

@ -126,7 +126,7 @@
<div align="center" class="img-container center"
style="padding-right: 0px;padding-left: 0px;">
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr style="line-height:0px"><td style="padding-right: 0px;padding-left: 0px;" align="center"><![endif]--><img
align="center" alt="Image" border="0" class="center" src="cid:logo.png"
align="center" alt="Image" border="0" class="center" src="cid:logo.svg"
style="text-decoration: none; -ms-interpolation-mode: bicubic; border: 0; height: 80px; width: auto; max-height: 80px; display: block;"
title="Image" height="80" />
<!--[if mso]></td></tr></table><![endif]-->

View file

@ -1,233 +1,19 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui" />
<title>
<%= Pleroma.Config.get([:instance, :name]) %>
</title>
<style>
body {
background-color: #121a24;
font-family: sans-serif;
color: #b9b9ba;
text-align: center;
}
.container {
max-width: 420px;
padding: 20px;
background-color: #182230;
border-radius: 4px;
margin: auto;
margin-top: 10vh;
box-shadow: 0 1px 4px 0px rgba(0, 0, 0, 0.5);
}
h1 {
margin: 0;
font-size: 24px;
}
h2 {
color: #b9b9ba;
font-weight: normal;
font-size: 18px;
margin-bottom: 20px;
}
a {
color: #d8a070;
text-decoration: none;
}
form {
width: 100%;
}
.input {
text-align: left;
color: #89898a;
display: flex;
flex-direction: column;
}
input {
box-sizing: content-box;
padding: 10px;
margin-top: 5px;
margin-bottom: 10px;
background-color: #121a24;
color: #b9b9ba;
border: 0;
transition-property: border-bottom;
transition-duration: 0.35s;
border-bottom: 2px solid #2a384a;
font-size: 14px;
}
.scopes-input {
display: flex;
flex-direction: column;
margin-top: 1em;
text-align: left;
color: #89898a;
}
.scopes-input label:first-child {
height: 2em;
}
.scopes {
display: flex;
flex-wrap: wrap;
text-align: left;
color: #b9b9ba;
}
.scope {
display: flex;
flex-basis: 100%;
height: 2em;
align-items: center;
}
.scope:before {
color: #b9b9ba;
content: "\fe0e";
margin-left: 1em;
margin-right: 1em;
}
[type="checkbox"] + label {
display: none;
cursor: pointer;
margin: 0.5em;
}
[type="checkbox"] {
display: none;
}
[type="checkbox"] + label:before {
cursor: pointer;
display: inline-block;
color: white;
background-color: #121a24;
border: 4px solid #121a24;
box-shadow: 0px 0px 1px 0 #d8a070;
box-sizing: border-box;
width: 1.2em;
height: 1.2em;
margin-right: 1.0em;
content: "";
transition-property: background-color;
transition-duration: 0.35s;
color: #121a24;
margin-bottom: -0.2em;
border-radius: 2px;
}
[type="checkbox"]:checked + label:before {
background-color: #d8a070;
}
input:focus {
outline: none;
border-bottom: 2px solid #d8a070;
}
button {
box-sizing: border-box;
width: 100%;
background-color: #1c2a3a;
color: #b9b9ba;
border-radius: 4px;
border: none;
padding: 10px;
margin-top: 20px;
margin-bottom: 20px;
text-transform: uppercase;
font-size: 16px;
box-shadow: 0px 0px 2px 0px black,
0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset,
0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
}
button:hover {
cursor: pointer;
box-shadow: 0px 0px 0px 1px #d8a070,
0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset,
0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
}
.alert-danger {
box-sizing: border-box;
width: 100%;
background-color: #931014;
border: 1px solid #a06060;
border-radius: 4px;
padding: 10px;
margin-top: 20px;
font-weight: 500;
font-size: 16px;
}
.alert-info {
box-sizing: border-box;
width: 100%;
border-radius: 4px;
border: 1px solid #7d796a;
padding: 10px;
margin-top: 20px;
font-weight: 500;
font-size: 16px;
}
@media all and (max-width: 440px) {
.container {
margin-top: 0
}
.scope {
flex-basis: 0%;
}
.scope:before {
content: "";
margin-left: 0em;
margin-right: 1em;
}
.scope:first-child:before {
margin-left: 1em;
content: "\fe0e";
}
.scope:after {
content: ",";
}
.scope:last-child:after {
content: "";
}
}
.form-row {
display: flex;
}
.form-row > label {
text-align: left;
line-height: 47px;
flex: 1;
}
.form-row > input {
flex: 2;
}
</style>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui">
<title><%= Pleroma.Config.get([:instance, :name]) %></title>
<link rel="stylesheet" href="/instance/static.css">
</head>
<body>
<div class="instance-header">
<a class="instance-header__content" href="/">
<img class="instance-header__thumbnail" src="<%= Pleroma.Config.get([:instance, :instance_thumbnail]) %>">
<h1 class="instance-header__title"><%= Pleroma.Config.get([:instance, :name]) %></h1>
</a>
</div>
<div class="container">
<h1><%= Pleroma.Config.get([:instance, :name]) %></h1>
<%= @inner_content %>
</div>
</body>

View file

@ -5,32 +5,55 @@
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
<% end %>
<h2>OAuth Authorization</h2>
<%= form_for @conn, o_auth_path(@conn, :authorize), [as: "authorization"], fn f -> %>
<%= 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>
<div class="input">
<%= label f, :nickname, "Pleroma Handle" %>
<%= text_input f, :nickname, placeholder: "lain" %>
<%= if @user do %>
<div class="account-header">
<div class="account-header__banner" style="background-image: url('<%= Pleroma.User.banner_url(@user) %>')"></div>
<div class="account-header__avatar" style="background-image: url('<%= Pleroma.User.avatar_url(@user) %>')"></div>
<div class="account-header__meta">
<div class="account-header__display-name"><%= @user.name %></div>
<div class="account-header__nickname">@<%= @user.nickname %>@<%= Pleroma.User.get_host(@user) %></div>
</div>
</div>
<%= hidden_input f, :name, value: @params["name"] %>
<%= hidden_input f, :password, value: @params["password"] %>
<br>
<% else %>
<div class="input">
<%= label f, :name, "Username" %>
<%= text_input f, :name %>
</div>
<div class="input">
<%= label f, :password, "Password" %>
<%= password_input f, :password %>
</div>
<%= submit "Log In" %>
<%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f}) %>
<% end %>
<div class="container__content">
<%= if @app do %>
<p>Application <strong><%= @app.client_name %></strong> is requesting access to your account.</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" %>
</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>
<div class="input">
<%= label f, :nickname, "Pleroma Handle" %>
<%= text_input f, :nickname, placeholder: "lain" %>
</div>
<%= hidden_input f, :name, value: @params["name"] %>
<%= hidden_input f, :password, value: @params["password"] %>
<br>
<% else %>
<div class="input">
<%= label f, :name, "Username" %>
<%= text_input f, :name %>
</div>
<div class="input">
<%= label f, :password, "Password" %>
<%= password_input f, :password %>
</div>
<%= submit "Log In" %>
<% end %>
<% end %>
</div>
<%= hidden_input f, :client_id, value: @client_id %>
<%= hidden_input f, :response_type, value: @response_type %>
<%= hidden_input f, :redirect_uri, value: @redirect_uri %>
@ -40,4 +63,3 @@
<%= if Pleroma.Config.oauth_consumer_enabled?() do %>
<%= render @view_module, Pleroma.Web.Auth.Authenticator.oauth_consumer_template(), assigns %>
<% end %>

View file

@ -74,6 +74,28 @@ def render("chat_update.json", %{chat_message_reference: cm_ref}) do
|> Jason.encode!()
end
def render("follow_relationships_update.json", item) do
%{
event: "pleroma:follow_relationships_update",
payload:
%{
state: item.state,
follower: %{
id: item.follower.id,
follower_count: item.follower.follower_count,
following_count: item.follower.following_count
},
following: %{
id: item.following.id,
follower_count: item.following.follower_count,
following_count: item.following.following_count
}
}
|> Jason.encode!()
}
|> Jason.encode!()
end
def render("conversation.json", %Participation{} = participation) do
%{
event: "conversation",

15
mix.exs
View file

@ -22,7 +22,7 @@ def project do
docs: [
source_url_pattern:
"https://git.pleroma.social/pleroma/pleroma/blob/develop/%{path}#L%{line}",
logo: "priv/static/static/logo.png",
logo: "priv/static/images/logo.png",
extras: ["README.md", "CHANGELOG.md"] ++ Path.wildcard("docs/**/*.md"),
groups_for_extras: [
"Installation manuals": Path.wildcard("docs/installation/*.md"),
@ -37,7 +37,8 @@ def project do
pleroma: [
include_executables_for: [:unix],
applications: [ex_syslogger: :load, syslog: :load, eldap: :transient],
steps: [:assemble, &put_otp_version/1, &copy_files/1, &copy_nginx_config/1]
steps: [:assemble, &put_otp_version/1, &copy_files/1, &copy_nginx_config/1],
config_providers: [{Pleroma.Config.ReleaseRuntimeProvider, nil}]
]
]
]
@ -157,7 +158,7 @@ defp deps do
{:floki, "~> 0.27"},
{:timex, "~> 3.6"},
{:ueberauth, "~> 0.4"},
{:linkify, "~> 0.2.0"},
{:linkify, "~> 0.4.0"},
{:http_signatures, "~> 0.1.0"},
{:telemetry, "~> 0.3"},
{:poolboy, "~> 1.5"},
@ -193,7 +194,8 @@ defp deps do
ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"},
{:restarter, path: "./restarter"},
{:majic,
git: "https://git.pleroma.social/pleroma/elixir-libraries/majic.git", branch: "develop"},
git: "https://git.pleroma.social/pleroma/elixir-libraries/majic.git",
ref: "4c692e544b28d1f5e543fb8a44be090f8cd96f80"},
{:open_api_spex,
git: "https://git.pleroma.social/pleroma/elixir-libraries/open_api_spex.git",
ref: "f296ac0924ba3cf79c7a588c4c252889df4c2edd"},
@ -205,7 +207,10 @@ defp deps do
{:mock, "~> 0.3.5", only: :test},
# temporary downgrade for excoveralls, hackney until hackney max_connections bug will be fixed
{:excoveralls, "0.12.3", only: :test},
{:hackney, "1.15.2", override: true},
{:hackney,
git: "https://git.pleroma.social/pleroma/elixir-libraries/hackney.git",
ref: "7d7119f0651515d6d7669c78393fd90950a3ec6e",
override: true},
{:mox, "~> 0.5", only: :test},
{:websocket_client, git: "https://github.com/jeremyong/websocket_client.git", only: :test}
] ++ oauth_deps()

View file

@ -11,7 +11,7 @@
"calendar": {:hex, :calendar, "1.0.0", "f52073a708528482ec33d0a171954ca610fe2bd28f1e871f247dc7f1565fa807", [:mix], [{:tzdata, "~> 0.5.20 or ~> 0.1.201603 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "990e9581920c82912a5ee50e62ff5ef96da6b15949a2ee4734f935fdef0f0a6f"},
"captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "e0f16822d578866e186a0974d65ad58cddc1e2ab", [ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"]},
"castore": {:hex, :castore, "0.1.7", "1ca19eee705cde48c9e809e37fdd0730510752cc397745e550f6065a56a701e9", [:mix], [], "hexpm", "a2ae2c13d40e9c308387f1aceb14786dca019ebc2a11484fb2a9f797ea0aa0d8"},
"certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "805abd97539caf89ec6d4732c91e62ba9da0cda51ac462380bbd28ee697a8c42"},
"certifi": {:git, "https://github.com/certifi/erlang-certifi", "e08b12e8993502240c25b78563993776f87ecd2a", [tag: "2.5.1"]},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
"comeonin": {:hex, :comeonin, "5.3.1", "7fe612b739c78c9c1a75186ef2d322ce4d25032d119823269d0aa1e2f1e20025", [:mix], [], "hexpm", "d6222483060c17f0977fad1b7401ef0c5863c985a64352755f366aee3799c245"},
"concurrent_limiter": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/concurrent_limiter.git", "d81be41024569330f296fc472e24198d7499ba78", [ref: "d81be41024569330f296fc472e24198d7499ba78"]},
@ -53,26 +53,26 @@
"gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm"},
"gettext": {:hex, :gettext, "0.18.0", "406d6b9e0e3278162c2ae1de0a60270452c553536772167e2d701f028116f870", [:mix], [], "hexpm", "c3f850be6367ebe1a08616c2158affe4a23231c70391050bf359d5f92f66a571"},
"gun": {:git, "https://github.com/ninenines/gun.git", "921c47146b2d9567eac7e9a4d2ccc60fffd4f327", [ref: "921c47146b2d9567eac7e9a4d2ccc60fffd4f327"]},
"hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "e0100f8ef7d1124222c11ad362c857d3df7cb5f4204054f9f0f4a728666591fc"},
"hackney": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/hackney.git", "7d7119f0651515d6d7669c78393fd90950a3ec6e", [ref: "7d7119f0651515d6d7669c78393fd90950a3ec6e"]},
"html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"},
"html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"},
"http_signatures": {:hex, :http_signatures, "0.1.0", "4e4b501a936dbf4cb5222597038a89ea10781776770d2e185849fa829686b34c", [:mix], [], "hexpm", "f8a7b3731e3fd17d38fa6e343fcad7b03d6874a3b0a108c8568a71ed9c2cf824"},
"httpoison": {:hex, :httpoison, "1.6.2", "ace7c8d3a361cebccbed19c283c349b3d26991eff73a1eaaa8abae2e3c8089b6", [:mix], [{:hackney, "~> 1.15 and >= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "aa2c74bd271af34239a3948779612f87df2422c2fdcfdbcec28d9c105f0773fe"},
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"},
"idna": {:git, "https://github.com/benoitc/erlang-idna", "6cff72747821110169ecfac871b0c69e5064afff", [tag: "6.0.0"]},
"inet_cidr": {:hex, :inet_cidr, "1.0.4", "a05744ab7c221ca8e395c926c3919a821eb512e8f36547c062f62c4ca0cf3d6e", [:mix], [], "hexpm", "64a2d30189704ae41ca7dbdd587f5291db5d1dda1414e0774c29ffc81088c1bc"},
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
"joken": {:hex, :joken, "2.2.0", "2daa1b12be05184aff7b5ace1d43ca1f81345962285fff3f88db74927c954d3a", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "b4f92e30388206f869dd25d1af628a1d99d7586e5cf0672f64d4df84c4d2f5e9"},
"jose": {:hex, :jose, "1.10.1", "16d8e460dae7203c6d1efa3f277e25b5af8b659febfc2f2eb4bacf87f128b80a", [:mix, :rebar3], [], "hexpm", "3c7ddc8a9394b92891db7c2771da94bf819834a1a4c92e30857b7d582e2f8257"},
"jumper": {:hex, :jumper, "1.0.1", "3c00542ef1a83532b72269fab9f0f0c82bf23a35e27d278bfd9ed0865cecabff", [:mix], [], "hexpm", "318c59078ac220e966d27af3646026db9b5a5e6703cb2aa3e26bcfaba65b7433"},
"libring": {:hex, :libring, "1.4.0", "41246ba2f3fbc76b3971f6bce83119dfec1eee17e977a48d8a9cfaaf58c2a8d6", [:mix], [], "hexpm"},
"linkify": {:hex, :linkify, "0.2.0", "2518bbbea21d2caa9d372424e1ad845b640c6630e2d016f1bd1f518f9ebcca28", [:mix], [], "hexpm", "b8ca8a68b79e30b7938d6c996085f3db14939f29538a59ca5101988bb7f917f6"},
"majic": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/majic.git", "4c692e544b28d1f5e543fb8a44be090f8cd96f80", [branch: "develop"]},
"linkify": {:hex, :linkify, "0.4.0", "7845b6ac33050a41acaf9318923ce6e7f3854418be9a5f22184de103f7a68ff9", [:mix], [], "hexpm", "a0ceb4c78591fecccf1d99fecc10c13dba75a307c663c80e28af9e2cdd9776ee"},
"majic": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/majic.git", "4c692e544b28d1f5e543fb8a44be090f8cd96f80", [ref: "4c692e544b28d1f5e543fb8a44be090f8cd96f80"]},
"makeup": {:hex, :makeup, "1.0.3", "e339e2f766d12e7260e6672dd4047405963c5ec99661abdc432e6ec67d29ef95", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "2e9b4996d11832947731f7608fed7ad2f9443011b3b479ae288011265cdd3dad"},
"makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"},
"meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm", "d34f013c156db51ad57cc556891b9720e6a1c1df5fe2e15af999c84d6cebeb1a"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"metrics": {:git, "https://github.com/benoitc/erlang-metrics", "c6eb4dcf29f9e907539915e2ab996f40c2ec7e8e", [tag: "1.0.1"]},
"mime": {:hex, :mime, "1.4.0", "5066f14944b470286146047d2f73518cf5cca82f8e4815cf35d196b58cf07c47", [:mix], [], "hexpm", "75fa42c4228ea9a23f70f123c74ba7cece6a03b1fd474fe13f6a7a85c6ea4ff6"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"mimerl": {:git, "https://github.com/benoitc/mimerl", "5a1b22a8fada5b3b40438da00a6923cb87a42bbc", [tag: "1.2.0"]},
"mochiweb": {:hex, :mochiweb, "2.18.0", "eb55f1db3e6e960fac4e6db4e2db9ec3602cc9f30b86cd1481d56545c3145d2e", [:rebar3], [], "hexpm"},
"mock": {:hex, :mock, "0.3.5", "feb81f52b8dcf0a0d65001d2fec459f6b6a8c22562d94a965862f6cc066b5431", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "6fae404799408300f863550392635d8f7e3da6b71abdd5c393faf41b131c8728"},
"mogrify": {:hex, :mogrify, "0.7.4", "9b2496dde44b1ce12676f85d7dc531900939e6367bc537c7243a1b089435b32d", [:mix], [], "hexpm", "50d79e337fba6bc95bfbef918058c90f50b17eed9537771e61d4619488f099c3"},
@ -84,7 +84,7 @@
"oban": {:hex, :oban, "2.1.0", "034144686f7e76a102b5d67731f098d98a9e4a52b07c25ad580a01f83a7f1cf5", [:mix], [{:ecto_sql, ">= 3.4.3", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c6f067fa3b308ed9e0e6beb2b34277c9c4e48bf95338edabd8f4a757a26e04c2"},
"open_api_spex": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/open_api_spex.git", "f296ac0924ba3cf79c7a588c4c252889df4c2edd", [ref: "f296ac0924ba3cf79c7a588c4c252889df4c2edd"]},
"p1_utils": {:hex, :p1_utils, "1.0.18", "3fe224de5b2e190d730a3c5da9d6e8540c96484cf4b4692921d1e28f0c32b01c", [:rebar3], [], "hexpm", "1fc8773a71a15553b179c986b22fbeead19b28fe486c332d4929700ffeb71f88"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
"parse_trans": {:git, "https://github.com/uwiger/parse_trans.git", "76abb347c3c1d00fb0ccf9e4b43e22b3d2288484", [tag: "3.3.0"]},
"pbkdf2_elixir": {:hex, :pbkdf2_elixir, "1.2.1", "9cbe354b58121075bd20eb83076900a3832324b7dd171a6895fab57b6bb2752c", [:mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}], "hexpm", "d3b40a4a4630f0b442f19eca891fcfeeee4c40871936fed2f68e1c4faa30481f"},
"phoenix": {:hex, :phoenix, "1.5.6", "8298cdb4e0f943242ba8410780a6a69cbbe972fef199b341a36898dd751bdd66", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0dc4d39af1306b6aa5122729b0a95ca779e42c708c6fe7abbb3d336d5379e956"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.2.1", "13f124cf0a3ce0f1948cf24654c7b9f2347169ff75c1123f44674afee6af3b03", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "478a1bae899cac0a6e02be1deec7e2944b7754c04e7d4107fc5a517f877743c0"},
@ -110,7 +110,7 @@
"recon": {:hex, :recon, "2.5.1", "430ffa60685ac1efdfb1fe4c97b8767c92d0d92e6e7c3e8621559ba77598678a", [:mix, :rebar3], [], "hexpm", "5721c6b6d50122d8f68cccac712caa1231f97894bab779eff5ff0f886cb44648"},
"remote_ip": {:git, "https://git.pleroma.social/pleroma/remote_ip.git", "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8", [ref: "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8"]},
"sleeplocks": {:hex, :sleeplocks, "1.1.1", "3d462a0639a6ef36cc75d6038b7393ae537ab394641beb59830a1b8271faeed3", [:rebar3], [], "hexpm", "84ee37aeff4d0d92b290fff986d6a95ac5eedf9b383fadfd1d88e9b84a1c02e1"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"},
"ssl_verify_fun": {:git, "https://github.com/deadtrickster/ssl_verify_fun.erl", "c5718226b0b9f3d1a38ef6ca3c3b4c75f53dda92", [tag: "1.1.4"]},
"sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm", "2e1ec458f892ffa81f9f8386e3f35a1af6db7a7a37748a64478f13163a1f3573"},
"swoosh": {:hex, :swoosh, "1.0.6", "6765e334c67dacabe721f0d701c7e5a6f06e4595c90df6f91e73ebd54d555833", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "7c50ef78e4acfd1cbd4907dc1fa87b5540675a6be9dc979d04890f49d7ec1830"},
"syslog": {:hex, :syslog, "1.1.0", "6419a232bea84f07b56dc575225007ffe34d9fdc91abe6f1b2f254fd71d8efc2", [:rebar3], [], "hexpm", "4c6a41373c7e20587be33ef841d3de6f3beba08519809329ecc4d27b15b659e1"},
@ -120,7 +120,7 @@
"trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"},
"tzdata": {:hex, :tzdata, "1.0.4", "a3baa4709ea8dba552dca165af6ae97c624a2d6ac14bd265165eaa8e8af94af6", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "b02637db3df1fd66dd2d3c4f194a81633d0e4b44308d36c1b2fdfd1e4e6f169b"},
"ueberauth": {:hex, :ueberauth, "0.6.3", "d42ace28b870e8072cf30e32e385579c57b9cc96ec74fa1f30f30da9c14f3cc0", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "afc293d8a1140d6591b53e3eaf415ca92842cb1d32fad3c450c6f045f7f91b60"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"},
"unicode_util_compat": {:git, "https://github.com/benoitc/unicode_util_compat.git", "38d7bc105f51159e8ea3279c40121db9db1e652f", [tag: "0.3.1"]},
"unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm", "6c7729a2d214806450d29766abc2afaa7a2cbecf415be64f36a6691afebb50e5"},
"web_push_encryption": {:hex, :web_push_encryption, "0.3.0", "598b5135e696fd1404dc8d0d7c0fa2c027244a4e5d5e5a98ba267f14fdeaabc8", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "f10bdd1afe527ede694749fb77a2f22f146a51b054c7fa541c9fd920fba7c875"},
"websocket_client": {:git, "https://github.com/jeremyong/websocket_client.git", "9a6f65d05ebf2725d62fb19262b21f1805a59fbf", []},

View file

@ -0,0 +1,141 @@
## This file is a PO Template file.
msgid "eperm"
msgstr "Operation not permitted"
msgid "eacces"
msgstr "Permission denied"
msgid "eagain"
msgstr "Resource temporarily unavailable"
msgid "ebadf"
msgstr "Bad file descriptor"
msgid "ebadmsg"
msgstr "Bad message"
msgid "ebusy"
msgstr "Device or resource busy"
msgid "edeadlk"
msgstr "Resource deadlock avoided"
msgid "edeadlock"
msgstr "Resource deadlock avoided"
msgid "edquot"
msgstr "Disk quota exceeded"
msgid "eexist"
msgstr "File exists"
msgid "efault"
msgstr "Bad address"
msgid "efbig"
msgstr "File is too large"
msgid "eftype"
msgstr "Inappropriate file type or format"
msgid "eintr"
msgstr "Interrupted system call"
msgid "einval"
msgstr "Invalid argument"
msgid "eio"
msgstr "Input/output error"
msgid "eisdir"
msgstr "Illegal operation on a directory"
msgid "eloop"
msgstr "Too many levels of symbolic links"
msgid "emfile"
msgstr "Too many open files"
msgid "emlink"
msgstr "Too many links"
msgid "emultihop"
msgstr "Multihop attempted"
msgid "enametoolong"
msgstr "File name is too long"
msgid "enfile"
msgstr "Too many open files in system"
msgid "enobufs"
msgstr "No buffer space available"
msgid "enodev"
msgstr "No such device"
msgid "enolck"
msgstr "No locks available"
msgid "enolink"
msgstr "Link has been severed"
msgid "enoent"
msgstr "No such file or directory"
msgid "enomem"
msgstr "Cannot allocate memory"
msgid "enospc"
msgstr "No space left on device"
msgid "enosr"
msgstr "Out of streams resources"
msgid "enostr"
msgstr "Device is not a stream"
msgid "enosys"
msgstr "Function not implemented"
msgid "enotblk"
msgstr "Block device required"
msgid "enotdir"
msgstr "Not a directory"
msgid "enotsup"
msgstr "Operation not supported"
msgid "enxio"
msgstr "No such device or address"
msgid "eopnotsupp"
msgstr "Operation not supported"
msgid "eoverflow"
msgstr "Value too large for defined data type"
msgid "epipe"
msgstr "Broken pipe"
msgid "erange"
msgstr "Numerical result out of range"
msgid "erofs"
msgstr "Read-only file system"
msgid "espipe"
msgstr "Illegal seek"
msgid "esrch"
msgstr "No such process"
msgid "estale"
msgstr "Stale file handle"
msgid "etxtbsy"
msgstr "Text file busy"
msgid "exdev"
msgstr "Invalid cross-device link"

View file

@ -3,14 +3,17 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-11-10 13:39+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"PO-Revision-Date: 2020-11-21 04:42+0000\n"
"Last-Translator: Guy Sheffer <guysoft@gmail.com>\n"
"Language-Team: Hebrew <https://translate.pleroma.social/projects/pleroma/"
"pleroma/he/>\n"
"Language: he\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Translate Toolkit 2.5.1\n"
"Plural-Forms: nplurals=4; plural=(n == 1) ? 0 : ((n == 2) ? 1 : ((n > 10 && "
"n % 10 == 0) ? 2 : 3));\n"
"X-Generator: Weblate 4.0.4\n"
## This file is a PO Template file.
##
@ -23,264 +26,264 @@ msgstr ""
## effect: edit them in PO (`.po`) files instead.
## From Ecto.Changeset.cast/4
msgid "can't be blank"
msgstr ""
msgstr "לא יכול להיות ריק"
## From Ecto.Changeset.unique_constraint/3
msgid "has already been taken"
msgstr ""
msgstr "כבר נלקח"
## From Ecto.Changeset.put_change/3
msgid "is invalid"
msgstr ""
msgstr "אינו תקני"
## From Ecto.Changeset.validate_format/3
msgid "has invalid format"
msgstr ""
msgstr "תבנית אינה תקנית"
## From Ecto.Changeset.validate_subset/3
msgid "has an invalid entry"
msgstr ""
msgstr "בעל.ה רשומה לא חוקית"
## From Ecto.Changeset.validate_exclusion/3
msgid "is reserved"
msgstr ""
msgstr "הינו שמור"
## From Ecto.Changeset.validate_confirmation/3
msgid "does not match confirmation"
msgstr ""
msgstr "אינו תורם את האימות"
## From Ecto.Changeset.no_assoc_constraint/3
msgid "is still associated with this entry"
msgstr ""
msgstr "עדיין משויך לרשומה זו"
msgid "are still associated with this entry"
msgstr ""
msgstr "עדיין משויכים לרשומה זו"
## From Ecto.Changeset.validate_length/3
msgid "should be %{count} character(s)"
msgid_plural "should be %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
msgstr[0] "אחד"
msgstr[1] "שני"
msgstr[2] "בודדים"
msgstr[3] "אחר"
msgid "should have %{count} item(s)"
msgid_plural "should have %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
msgstr[0] "אחד"
msgstr[1] "שני"
msgstr[2] "בודדים"
msgstr[3] "אחר"
msgid "should be at least %{count} character(s)"
msgid_plural "should be at least %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
msgstr[0] "אחד"
msgstr[1] "שנים"
msgstr[2] "בודדים"
msgstr[3] "אחר"
msgid "should have at least %{count} item(s)"
msgid_plural "should have at least %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
msgstr[0] "אחד"
msgstr[1] "שניים"
msgstr[2] "בודדים"
msgstr[3] "אחר"
msgid "should be at most %{count} character(s)"
msgid_plural "should be at most %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
msgstr[0] "אחד"
msgstr[1] "שניים"
msgstr[2] "בודדים"
msgstr[3] "אחר"
msgid "should have at most %{count} item(s)"
msgid_plural "should have at most %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
msgstr[0] "אחד"
msgstr[1] "שניים"
msgstr[2] "בודדים"
msgstr[3] "אחר"
## From Ecto.Changeset.validate_number/3
msgid "must be less than %{number}"
msgstr ""
msgstr "חייב להיות מתחת ל-%{number}"
msgid "must be greater than %{number}"
msgstr ""
msgstr "חייב להיות מעל ל-%{number}"
msgid "must be less than or equal to %{number}"
msgstr ""
msgstr "חייב להיות שווה ל-%{number}"
msgid "must be greater than or equal to %{number}"
msgstr ""
msgstr "חייב להיות גדול או שווה ל-%{number}"
msgid "must be equal to %{number}"
msgstr ""
msgstr "חייב להיות שווה ל-%{number}"
#: lib/pleroma/web/common_api/common_api.ex:505
#, elixir-format
msgid "Account not found"
msgstr ""
msgstr "חשבון לא נמצא"
#: lib/pleroma/web/common_api/common_api.ex:339
#, elixir-format
msgid "Already voted"
msgstr ""
msgstr "הצבעה כבר התבצעה"
#: lib/pleroma/web/oauth/oauth_controller.ex:359
#, elixir-format
msgid "Bad request"
msgstr ""
msgstr "בקשה שגוייה"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:426
#, elixir-format
msgid "Can't delete object"
msgstr ""
msgstr "לא ניתן למחוק אובייקט"
#: lib/pleroma/web/controller_helper.ex:105
#: lib/pleroma/web/controller_helper.ex:111
#, elixir-format
msgid "Can't display this activity"
msgstr ""
msgstr "לא ניתן להציג פעילות"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:285
#, elixir-format
msgid "Can't find user"
msgstr ""
msgstr "לא ניתן למצוא משתמש"
#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:61
#, elixir-format
msgid "Can't get favorites"
msgstr ""
msgstr "לא ניתן למצוא מועדפים"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:438
#, elixir-format
msgid "Can't like object"
msgstr ""
msgstr "לא ניתן לעשות לחבב אובייקט"
#: lib/pleroma/web/common_api/utils.ex:563
#, elixir-format
msgid "Cannot post an empty status without attachments"
msgstr ""
msgstr "לא ניתן לשלוח סטטוס ריק ללא קבצים מצורפים"
#: lib/pleroma/web/common_api/utils.ex:511
#, elixir-format
msgid "Comment must be up to %{max_size} characters"
msgstr ""
msgstr "תגובה חייבת להיות עד %{max_size} תווים"
#: lib/pleroma/config/config_db.ex:191
#, elixir-format
msgid "Config with params %{params} not found"
msgstr ""
msgstr "הגדרה עם פרמטר %{params} לא נמצאה"
#: lib/pleroma/web/common_api/common_api.ex:181
#: lib/pleroma/web/common_api/common_api.ex:185
#, elixir-format
msgid "Could not delete"
msgstr ""
msgstr "לא ניתן למחוק"
#: lib/pleroma/web/common_api/common_api.ex:231
#, elixir-format
msgid "Could not favorite"
msgstr ""
msgstr "לא ניתן לחבב"
#: lib/pleroma/web/common_api/common_api.ex:453
#, elixir-format
msgid "Could not pin"
msgstr ""
msgstr "לא ניתן לנעוץ"
#: lib/pleroma/web/common_api/common_api.ex:278
#, elixir-format
msgid "Could not unfavorite"
msgstr ""
msgstr "לא ניתן להסיר חיבוב"
#: lib/pleroma/web/common_api/common_api.ex:463
#, elixir-format
msgid "Could not unpin"
msgstr ""
msgstr "לא ניתן לבטל נעיצה"
#: lib/pleroma/web/common_api/common_api.ex:216
#, elixir-format
msgid "Could not unrepeat"
msgstr ""
msgstr "לא ניתן לבטל חזרה"
#: lib/pleroma/web/common_api/common_api.ex:512
#: lib/pleroma/web/common_api/common_api.ex:521
#, elixir-format
msgid "Could not update state"
msgstr ""
msgstr "לא ניתן לעדכן מצב"
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:207
#, elixir-format
msgid "Error."
msgstr ""
msgstr "שגיאה."
#: lib/pleroma/web/twitter_api/twitter_api.ex:106
#, elixir-format
msgid "Invalid CAPTCHA"
msgstr ""
msgstr "CAPTCHA לא תקין"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:116
#: lib/pleroma/web/oauth/oauth_controller.ex:568
#, elixir-format
msgid "Invalid credentials"
msgstr ""
msgstr "נתוני אימות לא נכונים"
#: lib/pleroma/plugs/ensure_authenticated_plug.ex:38
#, elixir-format
msgid "Invalid credentials."
msgstr ""
msgstr "נתוני אימות לא נכונים."
#: lib/pleroma/web/common_api/common_api.ex:355
#, elixir-format
msgid "Invalid indices"
msgstr ""
msgstr "אינדקס לא תקין"
#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:29
#, elixir-format
msgid "Invalid parameters"
msgstr ""
msgstr "פרמטרים לא תקינים"
#: lib/pleroma/web/common_api/utils.ex:414
#, elixir-format
msgid "Invalid password."
msgstr ""
msgstr "סיסמה לא תקינה."
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:220
#, elixir-format
msgid "Invalid request"
msgstr ""
msgstr "בקשה לא תקינה"
#: lib/pleroma/web/twitter_api/twitter_api.ex:109
#, elixir-format
msgid "Kocaptcha service unavailable"
msgstr ""
msgstr "שירות Kocaptcha לא זמין"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:112
#, elixir-format
msgid "Missing parameters"
msgstr ""
msgstr "פרמטרים חסרים"
#: lib/pleroma/web/common_api/utils.ex:547
#, elixir-format
msgid "No such conversation"
msgstr ""
msgstr "שיחה לא קיימת"
#: 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
#, elixir-format
msgid "No such permission_group"
msgstr ""
msgstr "permission_group לא קיים"
#: 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
#, elixir-format
msgid "Not found"
msgstr ""
msgstr "לא נמצא"
#: lib/pleroma/web/common_api/common_api.ex:331
#, elixir-format
msgid "Poll's author can't vote"
msgstr ""
msgstr "מחבר הסקר לא יכול.ה להצביע"
#: 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
@ -288,215 +291,215 @@ msgstr ""
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:71
#, elixir-format
msgid "Record not found"
msgstr ""
msgstr "רשומה לא נמצאה"
#: 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
#, elixir-format
msgid "Something went wrong"
msgstr ""
msgstr "משהו השתבש"
#: lib/pleroma/web/common_api/activity_draft.ex:107
#, elixir-format
msgid "The message visibility must be direct"
msgstr ""
msgstr "הנראות של ההודעה חייבת להיות ישירה"
#: lib/pleroma/web/common_api/utils.ex:573
#, elixir-format
msgid "The status is over the character limit"
msgstr ""
msgstr "הסטטוס מעל להגבלת התווים"
#: lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex:31
#, elixir-format
msgid "This resource requires authentication."
msgstr ""
msgstr "המשאב הזה דורש הרשאה."
#: lib/pleroma/plugs/rate_limiter/rate_limiter.ex:206
#, elixir-format
msgid "Throttled"
msgstr ""
msgstr "מושנק"
#: lib/pleroma/web/common_api/common_api.ex:356
#, elixir-format
msgid "Too many choices"
msgstr ""
msgstr "יותר מדיי אפשרויות"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:443
#, elixir-format
msgid "Unhandled activity type"
msgstr ""
msgstr "אין התמודדות לסוג הפעילות"
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:485
#, elixir-format
msgid "You can't revoke your own admin status."
msgstr ""
msgstr "לא ניתן לבטל את הרשאת המנהל של עצמך."
#: lib/pleroma/web/oauth/oauth_controller.ex:221
#: lib/pleroma/web/oauth/oauth_controller.ex:308
#, elixir-format
msgid "Your account is currently disabled"
msgstr ""
msgstr "החשבון שלך כרגע מבוטל"
#: lib/pleroma/web/oauth/oauth_controller.ex:183
#: lib/pleroma/web/oauth/oauth_controller.ex:331
#, elixir-format
msgid "Your login is missing a confirmed e-mail address"
msgstr ""
msgstr "חסר לחשבון שלך כתובת דואר אלקטרוני מאושר"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:390
#, elixir-format
msgid "can't read inbox of %{nickname} as %{as_nickname}"
msgstr ""
msgstr "לא ניתן לקרוא את הדואר הנכנס של %{nickname} בתור %{as_nickname}"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:473
#, elixir-format
msgid "can't update outbox of %{nickname} as %{as_nickname}"
msgstr ""
msgstr "לא ניתן לעדכן את חשבון הדואר היוצא של %{nickname} בתור %{as_nickname}"
#: lib/pleroma/web/common_api/common_api.ex:471
#, elixir-format
msgid "conversation is already muted"
msgstr ""
msgstr "שיחה כבר הושתקה"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:314
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:492
#, elixir-format
msgid "error"
msgstr ""
msgstr "שגיאה"
#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:32
#, elixir-format
msgid "mascots can only be images"
msgstr ""
msgstr "קמע יכול להיות רק תמונות"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:62
#, elixir-format
msgid "not found"
msgstr ""
msgstr "לא נמצא"
#: lib/pleroma/web/oauth/oauth_controller.ex:394
#, elixir-format
msgid "Bad OAuth request."
msgstr ""
msgstr "בקשת OAuth שגוייה."
#: lib/pleroma/web/twitter_api/twitter_api.ex:115
#, elixir-format
msgid "CAPTCHA already used"
msgstr ""
msgstr "כבר נעשה שימוש ב-CAPTCHA הזה"
#: lib/pleroma/web/twitter_api/twitter_api.ex:112
#, elixir-format
msgid "CAPTCHA expired"
msgstr ""
msgstr "פג תוקף CAPTCHA"
#: lib/pleroma/plugs/uploaded_media.ex:57
#, elixir-format
msgid "Failed"
msgstr ""
msgstr "נכשל"
#: lib/pleroma/web/oauth/oauth_controller.ex:410
#, elixir-format
msgid "Failed to authenticate: %{message}."
msgstr ""
msgstr "נכשל האימות: %{message}."
#: lib/pleroma/web/oauth/oauth_controller.ex:441
#, elixir-format
msgid "Failed to set up user account."
msgstr ""
msgstr "הגדרת חשבון משתמש נכשלה."
#: lib/pleroma/plugs/oauth_scopes_plug.ex:38
#, elixir-format
msgid "Insufficient permissions: %{permissions}."
msgstr ""
msgstr "אין מספיק הרשאות: %{permissions}."
#: lib/pleroma/plugs/uploaded_media.ex:104
#, elixir-format
msgid "Internal Error"
msgstr ""
msgstr "שגיאה פנימית"
#: lib/pleroma/web/oauth/fallback_controller.ex:22
#: lib/pleroma/web/oauth/fallback_controller.ex:29
#, elixir-format
msgid "Invalid Username/Password"
msgstr ""
msgstr "שם משתמש/סיסמה שגויים"
#: lib/pleroma/web/twitter_api/twitter_api.ex:118
#, elixir-format
msgid "Invalid answer data"
msgstr ""
msgstr "תשובה שגוייה למידע"
#: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:33
#, elixir-format
msgid "Nodeinfo schema version not handled"
msgstr ""
msgstr "Nodeinfo של של גרסת הסכמה לא ניתן לטיפול"
#: lib/pleroma/web/oauth/oauth_controller.ex:172
#, elixir-format
msgid "This action is outside the authorized scopes"
msgstr ""
msgstr "הפעולה הזו מחוץ לתחומי ההרשאות"
#: lib/pleroma/web/oauth/fallback_controller.ex:14
#, elixir-format
msgid "Unknown error, please check the details and try again."
msgstr ""
msgstr "שגיאה לא ידועה, יש לבדוק את פרטים ולנסות שוב."
#: lib/pleroma/web/oauth/oauth_controller.ex:119
#: lib/pleroma/web/oauth/oauth_controller.ex:158
#, elixir-format
msgid "Unlisted redirect_uri."
msgstr ""
msgstr "ניתב redirect_uri לא רשום."
#: lib/pleroma/web/oauth/oauth_controller.ex:390
#, elixir-format
msgid "Unsupported OAuth provider: %{provider}."
msgstr ""
msgstr "ספק OAuth לא נתמך: %{provider}."
#: lib/pleroma/uploaders/uploader.ex:72
#, elixir-format
msgid "Uploader callback timeout"
msgstr ""
msgstr "קריאה חזרה של מעלה עברה את הזמן הקצוב"
#: lib/pleroma/web/uploader_controller.ex:23
#, elixir-format
msgid "bad request"
msgstr ""
msgstr "בקשה שגוייה"
#: lib/pleroma/web/twitter_api/twitter_api.ex:103
#, elixir-format
msgid "CAPTCHA Error"
msgstr ""
msgstr "שגיאת CAPTCHA"
#: lib/pleroma/web/common_api/common_api.ex:290
#, elixir-format
msgid "Could not add reaction emoji"
msgstr ""
msgstr "לא ניתן להוסיף סמלון תגובה"
#: lib/pleroma/web/common_api/common_api.ex:301
#, elixir-format
msgid "Could not remove reaction emoji"
msgstr ""
msgstr "לא ניתן להסיר סמלון תגובה"
#: lib/pleroma/web/twitter_api/twitter_api.ex:129
#, elixir-format
msgid "Invalid CAPTCHA (Missing parameter: %{name})"
msgstr ""
msgstr "CAPTCHA לא תקני (חסר פרמטר: %{name})"
#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:92
#, elixir-format
msgid "List not found"
msgstr ""
msgstr "רשימה לא נמצאה"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:123
#, elixir-format
msgid "Missing parameter: %{name}"
msgstr ""
msgstr "חסר פרמטר: %{name}"
#: lib/pleroma/web/oauth/oauth_controller.ex:210
#: lib/pleroma/web/oauth/oauth_controller.ex:321
#, elixir-format
msgid "Password reset is required"
msgstr ""
msgstr "נדרש איפוס סיסמה"
#: 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
@ -533,64 +536,64 @@ msgstr ""
#: lib/pleroma/web/uploader_controller.ex:6 lib/pleroma/web/web_finger/web_finger_controller.ex:6
#, elixir-format
msgid "Security violation: OAuth scopes check was neither handled nor explicitly skipped."
msgstr ""
msgstr "הפרת אבטחה: OAuth בבדיקת המתחם לא נבדקה או דולגה במכוון."
#: lib/pleroma/plugs/ensure_authenticated_plug.ex:28
#, elixir-format
msgid "Two-factor authentication enabled, you must use a access token."
msgstr ""
msgstr "אימות דו-שלבי הופעל, יש להזין אסימון כניסה."
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:210
#, elixir-format
msgid "Unexpected error occurred while adding file to pack."
msgstr ""
msgstr "אירעה שגיאה לא צפויה בזמן הוספת הקובץ לחבילה."
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:138
#, elixir-format
msgid "Unexpected error occurred while creating pack."
msgstr ""
msgstr "אירעה שגיאה לא צפויה בזמן יצירת חבילה."
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:278
#, elixir-format
msgid "Unexpected error occurred while removing file from pack."
msgstr ""
msgstr "אירעה שגיאה לא צפויה בזמן הסרת הקובץ מהחבילה."
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:250
#, elixir-format
msgid "Unexpected error occurred while updating file in pack."
msgstr ""
msgstr "אירעה שגיאה לא צפויה בזמן עדכון הקובץ מהחבילה."
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:179
#, elixir-format
msgid "Unexpected error occurred while updating pack metadata."
msgstr ""
msgstr "אירעה שגיאה לא צפויה בזמן עדכון מטא-דאטה של החבילה."
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61
#, elixir-format
msgid "Web push subscription is disabled on this Pleroma instance"
msgstr ""
msgstr "הרשמה לעדכון ווב בדחיפה מבוטלת בשרת פלרומה זה"
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:451
#, elixir-format
msgid "You can't revoke your own admin/moderator status."
msgstr ""
msgstr "לא ניתן לשלול את סטטוס האדמין/מנהל של עצמך."
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:126
#, elixir-format
msgid "authorization required for timeline view"
msgstr ""
msgstr "הרשאה דרושה על מנת לצפות בציר הזמן"
#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:24
#, elixir-format
msgid "Access denied"
msgstr ""
msgstr "גישה נדחית"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:282
#, elixir-format
msgid "This API requires an authenticated user"
msgstr ""
msgstr "ה-API דורש הרשאת משתמש"
#: lib/pleroma/plugs/user_is_admin_plug.ex:21
#, elixir-format
msgid "User is not an admin."
msgstr ""
msgstr "משתמש אינו מנהל."

View file

@ -0,0 +1,149 @@
## This file is a PO Template file.
##
## `msgid`s here are often extracted from source code.
## Add new translations manually only if they're dynamic
## translations that can't be statically extracted.
##
## Run `mix gettext.extract` to bring this file up to
## date. Leave `msgstr`s empty as changing them here as no
## effect: edit them in PO (`.po`) files instead.
msgid "eperm"
msgstr ""
msgid "eacces"
msgstr ""
msgid "eagain"
msgstr ""
msgid "ebadf"
msgstr ""
msgid "ebadmsg"
msgstr ""
msgid "ebusy"
msgstr ""
msgid "edeadlk"
msgstr ""
msgid "edeadlock"
msgstr ""
msgid "edquot"
msgstr ""
msgid "eexist"
msgstr ""
msgid "efault"
msgstr ""
msgid "efbig"
msgstr ""
msgid "eftype"
msgstr ""
msgid "eintr"
msgstr ""
msgid "einval"
msgstr ""
msgid "eio"
msgstr ""
msgid "eisdir"
msgstr ""
msgid "eloop"
msgstr ""
msgid "emfile"
msgstr ""
msgid "emlink"
msgstr ""
msgid "emultihop"
msgstr ""
msgid "enametoolong"
msgstr ""
msgid "enfile"
msgstr ""
msgid "enobufs"
msgstr ""
msgid "enodev"
msgstr ""
msgid "enolck"
msgstr ""
msgid "enolink"
msgstr ""
msgid "enoent"
msgstr ""
msgid "enomem"
msgstr ""
msgid "enospc"
msgstr ""
msgid "enosr"
msgstr ""
msgid "enostr"
msgstr ""
msgid "enosys"
msgstr ""
msgid "enotblk"
msgstr ""
msgid "enotdir"
msgstr ""
msgid "enotsup"
msgstr ""
msgid "enxio"
msgstr ""
msgid "eopnotsupp"
msgstr ""
msgid "eoverflow"
msgstr ""
msgid "epipe"
msgstr ""
msgid "erange"
msgstr ""
msgid "erofs"
msgstr ""
msgid "espipe"
msgstr ""
msgid "esrch"
msgstr ""
msgid "estale"
msgstr ""
msgid "etxtbsy"
msgstr ""
msgid "exdev"
msgstr ""

View file

@ -0,0 +1,599 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-12-10 16:09+0000\n"
"PO-Revision-Date: 2020-12-11 00:56+0000\n"
"Last-Translator: ZEN <xinit.info@gmail.com>\n"
"Language-Team: Ukrainian <https://translate.pleroma.social/projects/pleroma/"
"pleroma/uk/>\n"
"Language: uk\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<="
"4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
"X-Generator: Weblate 4.0.4\n"
## This file is a PO Template file.
##
## `msgid`s here are often extracted from source code.
## Add new translations manually only if they're dynamic
## translations that can't be statically extracted.
##
## Run `mix gettext.extract` to bring this file up to
## date. Leave `msgstr`s empty as changing them here as no
## effect: edit them in PO (`.po`) files instead.
## From Ecto.Changeset.cast/4
msgid "can't be blank"
msgstr "не може бути пустим"
## From Ecto.Changeset.unique_constraint/3
msgid "has already been taken"
msgstr "вже зайнято"
## From Ecto.Changeset.put_change/3
msgid "is invalid"
msgstr "недійсний"
## From Ecto.Changeset.validate_format/3
msgid "has invalid format"
msgstr "має недійсний формат"
## From Ecto.Changeset.validate_subset/3
msgid "has an invalid entry"
msgstr "має недійсний запис"
## From Ecto.Changeset.validate_exclusion/3
msgid "is reserved"
msgstr "зарезервовано"
## From Ecto.Changeset.validate_confirmation/3
msgid "does not match confirmation"
msgstr "не збігається з підтвердженням"
## From Ecto.Changeset.no_assoc_constraint/3
msgid "is still associated with this entry"
msgstr "все ще пов'язаний з цим записом"
msgid "are still associated with this entry"
msgstr "все ще пов'язані з цим записом"
## From Ecto.Changeset.validate_length/3
msgid "should be %{count} character(s)"
msgid_plural "should be %{count} character(s)"
msgstr[0] "повинен містити %{count} символ"
msgstr[1] "повинен містити %{count} символи"
msgstr[2] "повинен містити %{count} символів"
msgid "should have %{count} item(s)"
msgid_plural "should have %{count} item(s)"
msgstr[0] "повинен містити %{count} елемент"
msgstr[1] "повинен містити %{count} елементи"
msgstr[2] "повинен містити %{count} елементів"
msgid "should be at least %{count} character(s)"
msgid_plural "should be at least %{count} character(s)"
msgstr[0] "повинен містити хоча б %{count} символ"
msgstr[1] "повинен містити хоча б %{count} символи"
msgstr[2] "повинен містити хоча б %{count} символів"
msgid "should have at least %{count} item(s)"
msgid_plural "should have at least %{count} item(s)"
msgstr[0] "повинен містити хоча б %{count} елемент"
msgstr[1] "повинен містити хоча б %{count} елементи"
msgstr[2] "повинен містити хоча б %{count} елементів"
msgid "should be at most %{count} character(s)"
msgid_plural "should be at most %{count} character(s)"
msgstr[0] "повинен бути не більше %{count} символу"
msgstr[1] "повинен бути не більше %{count} символів"
msgstr[2] "повинен бути не більше %{count} символів"
msgid "should have at most %{count} item(s)"
msgid_plural "should have at most %{count} item(s)"
msgstr[0] "повинен містити не більше %{count} елемента"
msgstr[1] "повинен містити не більше %{count} елементів"
msgstr[2] "повинен містити не більше %{count} елементів"
## From Ecto.Changeset.validate_number/3
msgid "must be less than %{number}"
msgstr "повинен мати значення менше ніж %{number}"
msgid "must be greater than %{number}"
msgstr "повинен мати значення більше ніж %{number}"
msgid "must be less than or equal to %{number}"
msgstr "повинен мати значення менше або рівне %{number}"
msgid "must be greater than or equal to %{number}"
msgstr "повинен мати значення більше або рівне %{number}"
msgid "must be equal to %{number}"
msgstr "повинен мати лише значення, рівне %{number}"
#: lib/pleroma/web/common_api/common_api.ex:505
#, elixir-format
msgid "Account not found"
msgstr "Обліковий запис не знайдено"
#: lib/pleroma/web/common_api/common_api.ex:339
#, elixir-format
msgid "Already voted"
msgstr "Вже проголосовано"
#: lib/pleroma/web/oauth/oauth_controller.ex:359
#, elixir-format
msgid "Bad request"
msgstr "Невірний запит"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:426
#, elixir-format
msgid "Can't delete object"
msgstr "Виникла помилка при видаленні об'єкту"
#: lib/pleroma/web/controller_helper.ex:105
#: lib/pleroma/web/controller_helper.ex:111
#, elixir-format
msgid "Can't display this activity"
msgstr "Не вдається відобразити цю активність"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:285
#, elixir-format
msgid "Can't find user"
msgstr "Користувача не знайдено"
#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:61
#, elixir-format
msgid "Can't get favorites"
msgstr "Не вдається отримати вподобання"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:438
#, elixir-format
msgid "Can't like object"
msgstr "Не вдається вподобати об’єкт"
#: lib/pleroma/web/common_api/utils.ex:563
#, elixir-format
msgid "Cannot post an empty status without attachments"
msgstr "Не вдається опублікувати порожнє повідомлення без вкладень"
#: lib/pleroma/web/common_api/utils.ex:511
#, elixir-format
msgid "Comment must be up to %{max_size} characters"
msgstr "Коментар може містити не більше %{max_size} символів"
#: lib/pleroma/config/config_db.ex:191
#, elixir-format
msgid "Config with params %{params} not found"
msgstr "Конфігурація з параметрами %{params} не знайдена"
#: lib/pleroma/web/common_api/common_api.ex:181
#: lib/pleroma/web/common_api/common_api.ex:185
#, elixir-format
msgid "Could not delete"
msgstr "Не можу видалити"
#: lib/pleroma/web/common_api/common_api.ex:231
#, elixir-format
msgid "Could not favorite"
msgstr "Не вдалося додати до вподобаного"
#: lib/pleroma/web/common_api/common_api.ex:453
#, elixir-format
msgid "Could not pin"
msgstr "Не вдалося закріпити"
#: lib/pleroma/web/common_api/common_api.ex:278
#, elixir-format
msgid "Could not unfavorite"
msgstr "Не вдалося видалити з вподобаного"
#: lib/pleroma/web/common_api/common_api.ex:463
#, elixir-format
msgid "Could not unpin"
msgstr "Не вдалося відкріпити"
#: lib/pleroma/web/common_api/common_api.ex:216
#, elixir-format
msgid "Could not unrepeat"
msgstr "Не вдалося скасувати поширення"
#: lib/pleroma/web/common_api/common_api.ex:512
#: lib/pleroma/web/common_api/common_api.ex:521
#, elixir-format
msgid "Could not update state"
msgstr "Не вдалося оновити стан"
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:207
#, elixir-format
msgid "Error."
msgstr "Помилка."
#: lib/pleroma/web/twitter_api/twitter_api.ex:106
#, elixir-format
msgid "Invalid CAPTCHA"
msgstr "Невірна CAPTCHA"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:116
#: lib/pleroma/web/oauth/oauth_controller.ex:568
#, elixir-format
msgid "Invalid credentials"
msgstr "Неправильні дані автентифікації"
#: lib/pleroma/plugs/ensure_authenticated_plug.ex:38
#, elixir-format
msgid "Invalid credentials."
msgstr "Неправильні дані автентифікації."
#: lib/pleroma/web/common_api/common_api.ex:355
#, elixir-format
msgid "Invalid indices"
msgstr "Неправильні індекси"
#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:29
#, elixir-format
msgid "Invalid parameters"
msgstr "Неправильні параметри"
#: lib/pleroma/web/common_api/utils.ex:414
#, elixir-format
msgid "Invalid password."
msgstr "Неправильний пароль."
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:220
#, elixir-format
msgid "Invalid request"
msgstr "Невірний запит"
#: lib/pleroma/web/twitter_api/twitter_api.ex:109
#, elixir-format
msgid "Kocaptcha service unavailable"
msgstr "Сервіс Kocaptcha недоступний"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:112
#, elixir-format
msgid "Missing parameters"
msgstr "Відсутні параметри"
#: lib/pleroma/web/common_api/utils.ex:547
#, elixir-format
msgid "No such conversation"
msgstr "Немає такої розмови"
#: 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
#, elixir-format
msgid "No such permission_group"
msgstr "Не існує такої групи повноважень"
#: 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
#, elixir-format
msgid "Not found"
msgstr "Не знайдено"
#: lib/pleroma/web/common_api/common_api.ex:331
#, elixir-format
msgid "Poll's author can't vote"
msgstr "Автор опитування не може голосувати"
#: 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/subscription_controller.ex:71
#, elixir-format
msgid "Record not found"
msgstr "Запис не знайдено"
#: 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
#, elixir-format
msgid "Something went wrong"
msgstr "Щось зламалося"
#: lib/pleroma/web/common_api/activity_draft.ex:107
#, elixir-format
msgid "The message visibility must be direct"
msgstr "Видимість у повідомлення повинна бути `Приватний`"
#: lib/pleroma/web/common_api/utils.ex:573
#, elixir-format
msgid "The status is over the character limit"
msgstr "Цей статус перевищує ліміт символів"
#: lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex:31
#, elixir-format
msgid "This resource requires authentication."
msgstr "Цей ресурс вимагає автентифікації."
#: lib/pleroma/plugs/rate_limiter/rate_limiter.ex:206
#, elixir-format
msgid "Throttled"
msgstr "Обмежено. Перевищено ліміт запитів."
#: lib/pleroma/web/common_api/common_api.ex:356
#, elixir-format
msgid "Too many choices"
msgstr "Забагато варіантів вибору"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:443
#, elixir-format
msgid "Unhandled activity type"
msgstr "Непідтримуваний тип активності"
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:485
#, elixir-format
msgid "You can't revoke your own admin status."
msgstr "Ви не можете позбавити самого себе статусу адміністратора."
#: lib/pleroma/web/oauth/oauth_controller.ex:221
#: lib/pleroma/web/oauth/oauth_controller.ex:308
#, elixir-format
msgid "Your account is currently disabled"
msgstr "Ваш обліковий запис наразі вимкнено"
#: lib/pleroma/web/oauth/oauth_controller.ex:183
#: lib/pleroma/web/oauth/oauth_controller.ex:331
#, elixir-format
msgid "Your login is missing a confirmed e-mail address"
msgstr "Ваша електрона адреса не підтверджена"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:390
#, elixir-format
msgid "can't read inbox of %{nickname} as %{as_nickname}"
msgstr ""
"Не вдається прочитати \"Вхідні\" повідомлення %{nickname} як %{as_nickname}"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:473
#, elixir-format
msgid "can't update outbox of %{nickname} as %{as_nickname}"
msgstr ""
"Не вдається оновити \"Вихідні\" повідомлення %{nickname} як %{as_nickname}"
#: lib/pleroma/web/common_api/common_api.ex:471
#, elixir-format
msgid "conversation is already muted"
msgstr "Розмова вже заглушена"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:314
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:492
#, elixir-format
msgid "error"
msgstr "помилка"
#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:32
#, elixir-format
msgid "mascots can only be images"
msgstr "талісманами можуть бути лише зображення"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:62
#, elixir-format
msgid "not found"
msgstr "не знайдено"
#: lib/pleroma/web/oauth/oauth_controller.ex:394
#, elixir-format
msgid "Bad OAuth request."
msgstr "Невірний запит OAuth."
#: lib/pleroma/web/twitter_api/twitter_api.ex:115
#, elixir-format
msgid "CAPTCHA already used"
msgstr "CAPTCHA вже використана"
#: lib/pleroma/web/twitter_api/twitter_api.ex:112
#, elixir-format
msgid "CAPTCHA expired"
msgstr "Термін дії CAPTCHA закінчився"
#: lib/pleroma/plugs/uploaded_media.ex:57
#, elixir-format
msgid "Failed"
msgstr "Не вдалося"
#: lib/pleroma/web/oauth/oauth_controller.ex:410
#, elixir-format
msgid "Failed to authenticate: %{message}."
msgstr "Помилка автентифікації: %{message}."
#: lib/pleroma/web/oauth/oauth_controller.ex:441
#, elixir-format
msgid "Failed to set up user account."
msgstr "Не вдалося створити обліковий запис."
#: lib/pleroma/plugs/oauth_scopes_plug.ex:38
#, elixir-format
msgid "Insufficient permissions: %{permissions}."
msgstr "Недостатньо прав: %{permissions}."
#: lib/pleroma/plugs/uploaded_media.ex:104
#, elixir-format
msgid "Internal Error"
msgstr "Внутрішня помилка"
#: lib/pleroma/web/oauth/fallback_controller.ex:22
#: lib/pleroma/web/oauth/fallback_controller.ex:29
#, elixir-format
msgid "Invalid Username/Password"
msgstr "Неправильне ім'я користувача або пароль"
#: lib/pleroma/web/twitter_api/twitter_api.ex:118
#, elixir-format
msgid "Invalid answer data"
msgstr "Неправильна відповідь"
#: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:33
#, elixir-format
msgid "Nodeinfo schema version not handled"
msgstr "Версія схеми Nodeinfo не враховується"
#: lib/pleroma/web/oauth/oauth_controller.ex:172
#, elixir-format
msgid "This action is outside the authorized scopes"
msgstr "Ця дія виходить за рамки доступних повноважень"
#: lib/pleroma/web/oauth/fallback_controller.ex:14
#, elixir-format
msgid "Unknown error, please check the details and try again."
msgstr "Невідома помилка. Перевірте деталі та повторіть спробу."
#: lib/pleroma/web/oauth/oauth_controller.ex:119
#: lib/pleroma/web/oauth/oauth_controller.ex:158
#, elixir-format
msgid "Unlisted redirect_uri."
msgstr "Невідомий redirect_uri."
#: lib/pleroma/web/oauth/oauth_controller.ex:390
#, elixir-format
msgid "Unsupported OAuth provider: %{provider}."
msgstr "Непідтримуваний постачальник послуг OAuth: %{provider}."
#: lib/pleroma/uploaders/uploader.ex:72
#, elixir-format
msgid "Uploader callback timeout"
msgstr "Тайм-аут при завантаженні"
#: lib/pleroma/web/uploader_controller.ex:23
#, elixir-format
msgid "bad request"
msgstr "невірний запит"
#: lib/pleroma/web/twitter_api/twitter_api.ex:103
#, elixir-format
msgid "CAPTCHA Error"
msgstr "Помилка CAPTCHA"
#: lib/pleroma/web/common_api/common_api.ex:290
#, elixir-format
msgid "Could not add reaction emoji"
msgstr "Не вдалося додати емодзі для реакції"
#: lib/pleroma/web/common_api/common_api.ex:301
#, elixir-format
msgid "Could not remove reaction emoji"
msgstr "Не вдалося видалити реакцію"
#: lib/pleroma/web/twitter_api/twitter_api.ex:129
#, elixir-format
msgid "Invalid CAPTCHA (Missing parameter: %{name})"
msgstr "Недійсна CAPTCHA (Відсутній параметр: %{name})"
#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:92
#, elixir-format
msgid "List not found"
msgstr "Список не знайдено"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:123
#, elixir-format
msgid "Missing parameter: %{name}"
msgstr "Відсутній параметр: %{name}"
#: lib/pleroma/web/oauth/oauth_controller.ex:210
#: lib/pleroma/web/oauth/oauth_controller.ex:321
#, elixir-format
msgid "Password reset is required"
msgstr "Потрібно скинути пароль"
#: 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/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/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/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/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/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
#, elixir-format
msgid "Security violation: OAuth scopes check was neither handled nor explicitly skipped."
msgstr ""
"Порушення безпеки: перевірка обсягу OAuth не була оброблена, ні явно "
"пропущена."
#: lib/pleroma/plugs/ensure_authenticated_plug.ex:28
#, elixir-format
msgid "Two-factor authentication enabled, you must use a access token."
msgstr ""
"Двофакторна автентифікація ввімкнена, ви повинні використовувати ключ "
"доступу."
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:210
#, elixir-format
msgid "Unexpected error occurred while adding file to pack."
msgstr "Несподівана помилка при додаванні файлу в пакет."
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:138
#, elixir-format
msgid "Unexpected error occurred while creating pack."
msgstr "Несподівана помилка під час створення пакета."
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:278
#, elixir-format
msgid "Unexpected error occurred while removing file from pack."
msgstr "Під час видалення файлу з пакета сталася несподівана помилка."
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:250
#, elixir-format
msgid "Unexpected error occurred while updating file in pack."
msgstr "Під час оновлення файлу в пакеті сталася несподівана помилка."
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:179
#, elixir-format
msgid "Unexpected error occurred while updating pack metadata."
msgstr "Під час оновлення метаданих пакета сталася несподівана помилка."
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61
#, elixir-format
msgid "Web push subscription is disabled on this Pleroma instance"
msgstr "Web push-сповіщення вимкнені на цьому інстансі Pleroma"
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:451
#, elixir-format
msgid "You can't revoke your own admin/moderator status."
msgstr "Ви не можете позбавити самого себе статусу адміністратора/модератора."
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:126
#, elixir-format
msgid "authorization required for timeline view"
msgstr "необхідно ввійти в систему для перегляду стрічки повідомлень"
#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:24
#, elixir-format
msgid "Access denied"
msgstr "Доступ заборонено"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:282
#, elixir-format
msgid "This API requires an authenticated user"
msgstr "Цей API вимагає автентифікованого користувача"
#: lib/pleroma/plugs/user_is_admin_plug.ex:21
#, elixir-format
msgid "User is not an admin."
msgstr "Користувач не є адміністратором."

View file

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-09-20 13:18+0000\n"
"PO-Revision-Date: 2020-10-22 18:25+0000\n"
"PO-Revision-Date: 2020-12-14 06:00+0000\n"
"Last-Translator: shironeko <shironeko@tesaguri.club>\n"
"Language-Team: Chinese (Simplified) <https://translate.pleroma.social/"
"projects/pleroma/pleroma/zh_Hans/>\n"
@ -146,9 +146,9 @@ msgid "Cannot post an empty status without attachments"
msgstr "无法发送空白且不包含附件的状态"
#: lib/pleroma/web/common_api/utils.ex:511
#, elixir-format
#, elixir-format, fuzzy
msgid "Comment must be up to %{max_size} characters"
msgstr ""
msgstr "评论最多可使用 %{max_size} 字符"
#: lib/pleroma/config/config_db.ex:191
#, elixir-format
@ -250,21 +250,21 @@ msgstr "没有该对话"
#: 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
#, elixir-format
#, elixir-format, fuzzy
msgid "No such permission_group"
msgstr ""
msgstr "没有该权限组"
#: 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
#, elixir-format
msgid "Not found"
msgstr ""
msgstr "未找到"
#: lib/pleroma/web/common_api/common_api.ex:331
#, elixir-format
msgid "Poll's author can't vote"
msgstr ""
msgstr "投票的发起者不能投票"
#: 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
@ -272,39 +272,39 @@ msgstr ""
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:71
#, elixir-format
msgid "Record not found"
msgstr ""
msgstr "未找到该记录"
#: 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
#, elixir-format
msgid "Something went wrong"
msgstr ""
msgstr "发生了一些错误"
#: lib/pleroma/web/common_api/activity_draft.ex:107
#, elixir-format
msgid "The message visibility must be direct"
msgstr ""
msgstr "该消息必须为私信"
#: lib/pleroma/web/common_api/utils.ex:573
#, elixir-format
msgid "The status is over the character limit"
msgstr ""
msgstr "状态超过了字符数限制"
#: lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex:31
#, elixir-format
msgid "This resource requires authentication."
msgstr ""
msgstr "该资源需要认证。"
#: lib/pleroma/plugs/rate_limiter/rate_limiter.ex:206
#, elixir-format
#, elixir-format, fuzzy
msgid "Throttled"
msgstr ""
msgstr "节流了"
#: lib/pleroma/web/common_api/common_api.ex:356
#, elixir-format
msgid "Too many choices"
msgstr ""
msgstr "太多选项"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:443
#, elixir-format
@ -314,101 +314,101 @@ msgstr ""
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:485
#, elixir-format
msgid "You can't revoke your own admin status."
msgstr ""
msgstr "您不能撤消自己的管理员权限。"
#: lib/pleroma/web/oauth/oauth_controller.ex:221
#: lib/pleroma/web/oauth/oauth_controller.ex:308
#, elixir-format
msgid "Your account is currently disabled"
msgstr ""
msgstr "您的账户已被禁用"
#: lib/pleroma/web/oauth/oauth_controller.ex:183
#: lib/pleroma/web/oauth/oauth_controller.ex:331
#, elixir-format
msgid "Your login is missing a confirmed e-mail address"
msgstr ""
msgstr "您的账户缺少已认证的 e-mail 地址"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:390
#, elixir-format
msgid "can't read inbox of %{nickname} as %{as_nickname}"
msgstr ""
msgstr "无法以 %{as_nickname} 读取 %{nickname} 的收件箱"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:473
#, elixir-format
msgid "can't update outbox of %{nickname} as %{as_nickname}"
msgstr ""
msgstr "无法以 %{as_nickname} 更新 %{nickname} 的出件箱"
#: lib/pleroma/web/common_api/common_api.ex:471
#, elixir-format
msgid "conversation is already muted"
msgstr ""
msgstr "对话已经被静音"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:314
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:492
#, elixir-format
msgid "error"
msgstr ""
msgstr "错误"
#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:32
#, elixir-format
msgid "mascots can only be images"
msgstr ""
msgstr "吉祥物只能是图片"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:62
#, elixir-format
msgid "not found"
msgstr ""
msgstr "未找到"
#: lib/pleroma/web/oauth/oauth_controller.ex:394
#, elixir-format
msgid "Bad OAuth request."
msgstr ""
msgstr "错误的 OAuth 请求。"
#: lib/pleroma/web/twitter_api/twitter_api.ex:115
#, elixir-format
msgid "CAPTCHA already used"
msgstr ""
msgstr "验证码已被使用"
#: lib/pleroma/web/twitter_api/twitter_api.ex:112
#, elixir-format
msgid "CAPTCHA expired"
msgstr ""
msgstr "验证码已过期"
#: lib/pleroma/plugs/uploaded_media.ex:57
#, elixir-format
msgid "Failed"
msgstr ""
msgstr "失败"
#: lib/pleroma/web/oauth/oauth_controller.ex:410
#, elixir-format
#, elixir-format, fuzzy
msgid "Failed to authenticate: %{message}."
msgstr ""
msgstr "认证失败:%{message}。"
#: lib/pleroma/web/oauth/oauth_controller.ex:441
#, elixir-format
msgid "Failed to set up user account."
msgstr ""
msgstr "建立用户帐号失败。"
#: lib/pleroma/plugs/oauth_scopes_plug.ex:38
#, elixir-format
msgid "Insufficient permissions: %{permissions}."
msgstr ""
msgstr "权限不足:%{permissions}。"
#: lib/pleroma/plugs/uploaded_media.ex:104
#, elixir-format
msgid "Internal Error"
msgstr ""
msgstr "内部错误"
#: lib/pleroma/web/oauth/fallback_controller.ex:22
#: lib/pleroma/web/oauth/fallback_controller.ex:29
#, elixir-format
msgid "Invalid Username/Password"
msgstr ""
msgstr "无效的用户名/密码"
#: lib/pleroma/web/twitter_api/twitter_api.ex:118
#, elixir-format
#, elixir-format, fuzzy
msgid "Invalid answer data"
msgstr ""
msgstr "无效的回答数据"
#: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:33
#, elixir-format
@ -418,12 +418,12 @@ msgstr ""
#: lib/pleroma/web/oauth/oauth_controller.ex:172
#, elixir-format
msgid "This action is outside the authorized scopes"
msgstr ""
msgstr "此操作在许可范围以外"
#: lib/pleroma/web/oauth/fallback_controller.ex:14
#, elixir-format
msgid "Unknown error, please check the details and try again."
msgstr ""
msgstr "未知错误,请检查并重试。"
#: lib/pleroma/web/oauth/oauth_controller.ex:119
#: lib/pleroma/web/oauth/oauth_controller.ex:158
@ -434,53 +434,53 @@ msgstr ""
#: lib/pleroma/web/oauth/oauth_controller.ex:390
#, elixir-format
msgid "Unsupported OAuth provider: %{provider}."
msgstr ""
msgstr "不支持的 OAuth 提供者:%{provider}。"
#: lib/pleroma/uploaders/uploader.ex:72
#, elixir-format
#, elixir-format, fuzzy
msgid "Uploader callback timeout"
msgstr ""
msgstr "上传回复超时"
#: lib/pleroma/web/uploader_controller.ex:23
#, elixir-format
msgid "bad request"
msgstr ""
msgstr "错误的请求"
#: lib/pleroma/web/twitter_api/twitter_api.ex:103
#, elixir-format
msgid "CAPTCHA Error"
msgstr ""
msgstr "验证码错误"
#: lib/pleroma/web/common_api/common_api.ex:290
#, elixir-format
#, elixir-format, fuzzy
msgid "Could not add reaction emoji"
msgstr ""
msgstr "无法添加表情反应"
#: lib/pleroma/web/common_api/common_api.ex:301
#, elixir-format
msgid "Could not remove reaction emoji"
msgstr ""
msgstr "无法移除表情反应"
#: lib/pleroma/web/twitter_api/twitter_api.ex:129
#, elixir-format
msgid "Invalid CAPTCHA (Missing parameter: %{name})"
msgstr ""
msgstr "无效的验证码(缺少参数:%{name}"
#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:92
#, elixir-format
msgid "List not found"
msgstr ""
msgstr "未找到列表"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:123
#, elixir-format
msgid "Missing parameter: %{name}"
msgstr ""
msgstr "缺少参数:%{name}"
#: lib/pleroma/web/oauth/oauth_controller.ex:210
#: lib/pleroma/web/oauth/oauth_controller.ex:321
#, elixir-format
msgid "Password reset is required"
msgstr ""
msgstr "需要重置密码"
#: 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
@ -520,61 +520,61 @@ msgid "Security violation: OAuth scopes check was neither handled nor explicitly
msgstr ""
#: lib/pleroma/plugs/ensure_authenticated_plug.ex:28
#, elixir-format
#, elixir-format, fuzzy
msgid "Two-factor authentication enabled, you must use a access token."
msgstr ""
msgstr "已启用两因素验证,您需要使用访问令牌。"
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:210
#, elixir-format
msgid "Unexpected error occurred while adding file to pack."
msgstr ""
msgstr "向表情包添加文件时发生了没有预料到的错误。"
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:138
#, elixir-format
msgid "Unexpected error occurred while creating pack."
msgstr ""
msgstr "创建表情包时发生了没有预料到的错误。"
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:278
#, elixir-format
msgid "Unexpected error occurred while removing file from pack."
msgstr ""
msgstr "从表情包移除文件时发生了没有预料到的错误。"
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:250
#, elixir-format
msgid "Unexpected error occurred while updating file in pack."
msgstr ""
msgstr "更新表情包内的文件时发生了没有预料到的错误。"
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:179
#, elixir-format
msgid "Unexpected error occurred while updating pack metadata."
msgstr ""
msgstr "更新表情包元数据时发生了没有预料到的错误。"
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61
#, elixir-format
#, elixir-format, fuzzy
msgid "Web push subscription is disabled on this Pleroma instance"
msgstr ""
msgstr "此 Pleroma 实例禁用了网页推送订阅"
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:451
#, elixir-format
msgid "You can't revoke your own admin/moderator status."
msgstr ""
msgstr "您不能撤消自己的管理员权限。"
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:126
#, elixir-format
msgid "authorization required for timeline view"
msgstr ""
msgstr "浏览时间线需要认证"
#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:24
#, elixir-format
msgid "Access denied"
msgstr ""
msgstr "拒绝访问"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:282
#, elixir-format
msgid "This API requires an authenticated user"
msgstr ""
msgstr "此 API 需要已认证的用户"
#: lib/pleroma/plugs/user_is_admin_plug.ex:21
#, elixir-format
msgid "User is not an admin."
msgstr ""
msgstr "该用户不是管理员。"

View file

@ -47,6 +47,11 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_these_attributes(:strong, [])
Meta.allow_tag_with_these_attributes(:sub, [])
Meta.allow_tag_with_these_attributes(:sup, [])
Meta.allow_tag_with_these_attributes(:ruby, [])
Meta.allow_tag_with_these_attributes(:rb, [])
Meta.allow_tag_with_these_attributes(:rp, [])
Meta.allow_tag_with_these_attributes(:rt, [])
Meta.allow_tag_with_these_attributes(:rtc, [])
Meta.allow_tag_with_these_attributes(:u, [])
Meta.allow_tag_with_these_attributes(:ul, [])

BIN
priv/static/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,296 @@
* {
box-sizing: border-box;
}
:root {
--brand-color: #d8a070;
--background-color: #121a24;
--foreground-color: #182230;
--primary-text-color: #b9b9ba;
--muted-text-color: #89898a;
}
body {
background-color: var(--background-color);
font-family: sans-serif;
color: var(--primary-text-color);
padding: 0;
margin: 0;
}
.instance-header {
height: 60px;
padding: 10px;
background: var(--foreground-color);
box-shadow: 0 1px 4px 0px rgba(0, 0, 0, 0.5);
}
.instance-header__content {
display: flex;
align-items: center;
max-width: 400px;
margin: 0 auto;
}
.instance-header__thumbnail {
max-width: 40px;
border-radius: 4px;
margin-right: 12px;
}
.instance-header__title {
font-size: 16px;
font-weight: bold;
color: var(--primary-text-color);
}
.container {
max-width: 400px;
background-color: var(--foreground-color);
border-radius: 4px;
overflow: hidden;
margin: 35px auto;
box-shadow: 0 1px 4px 0px rgba(0, 0, 0, 0.5);
}
.container__content {
padding: 0 20px;
}
h1 {
margin: 0;
font-size: 24px;
text-align: center;
}
h2 {
color: var(--primary-text-color);
font-weight: normal;
font-size: 18px;
margin-bottom: 20px;
}
a {
color: var(--brand-color);
text-decoration: none;
}
form {
width: 100%;
}
.input {
color: var(--muted-text-color);
display: flex;
flex-direction: column;
}
input {
box-sizing: content-box;
padding: 10px;
margin-top: 5px;
margin-bottom: 10px;
background-color: var(--background-color);
color: var(--primary-text-color);
border: 0;
transition-property: border-bottom;
transition-duration: 0.35s;
border-bottom: 2px solid #2a384a;
font-size: 14px;
}
.scopes-input {
display: flex;
flex-direction: column;
margin: 1em 0;
color: var(--muted-text-color);
}
.scopes-input label:first-child {
height: 2em;
}
.scopes {
display: flex;
flex-wrap: wrap;
color: var(--primary-text-color);
}
.scope {
display: flex;
flex-basis: 100%;
height: 2em;
align-items: center;
}
.scope:before {
color: var(--primary-text-color);
content: "✔\fe0e";
margin-left: 1em;
margin-right: 1em;
}
[type="checkbox"] + label {
display: none;
cursor: pointer;
margin: 0.5em;
}
[type="checkbox"] {
display: none;
}
[type="checkbox"] + label:before {
cursor: pointer;
display: inline-block;
color: white;
background-color: var(--background-color);
border: 4px solid var(--background-color);
box-shadow: 0px 0px 1px 0 var(--brand-color);
width: 1.2em;
height: 1.2em;
margin-right: 1.0em;
content: "";
transition-property: background-color;
transition-duration: 0.35s;
color: var(--background-color);
margin-bottom: -0.2em;
border-radius: 2px;
}
[type="checkbox"]:checked + label:before {
background-color: var(--brand-color);
}
input:focus {
outline: none;
border-bottom: 2px solid var(--brand-color);
}
.actions {
display: flex;
justify-content: flex-end;
}
.actions button,
.actions a.button {
width: auto;
margin-left: 10px;
}
a.button,
button {
width: 100%;
background-color: #1c2a3a;
color: var(--primary-text-color);
border-radius: 4px;
border: none;
padding: 10px 16px;
margin-top: 20px;
margin-bottom: 20px;
text-transform: uppercase;
font-size: 16px;
box-shadow: 0px 0px 2px 0px black,
0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset,
0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
}
a.button:hover,
button:hover {
cursor: pointer;
box-shadow: 0px 0px 0px 1px var(--brand-color),
0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset,
0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
}
.alert-danger {
width: 100%;
background-color: #931014;
border: 1px solid #a06060;
border-radius: 4px;
padding: 10px;
margin-top: 20px;
font-weight: 500;
font-size: 16px;
}
.alert-info {
width: 100%;
border-radius: 4px;
border: 1px solid #7d796a;
padding: 10px;
margin-top: 20px;
font-weight: 500;
font-size: 16px;
}
.account-header__banner {
width: 100%;
height: 112px;
background-size: cover;
background-position: center;
}
.account-header__avatar {
width: 94px;
height: 94px;
background-size: cover;
background-position: center;
margin: -47px 10px 0;
border: 6px solid var(--foreground-color);
border-radius: 999px;
}
.account-header__meta {
padding: 6px 20px 17px;
}
.account-header__display-name {
font-size: 20px;
font-weight: bold;
}
.account-header__nickname {
font-size: 14px;
color: var(--muted-text-color);
}
@media all and (max-width: 420px) {
.container {
margin: 0 auto;
border-radius: 0;
}
.scope {
flex-basis: 0%;
}
.scope:before {
content: "";
margin-left: 0em;
margin-right: 1em;
}
.scope:first-child:before {
margin-left: 1em;
content: "✔\fe0e";
}
.scope:after {
content: ",";
}
.scope:last-child:after {
content: "";
}
}
.form-row {
display: flex;
}
.form-row > label {
line-height: 47px;
flex: 1;
}
.form-row > input {
flex: 2;
}

View file

@ -2,12 +2,9 @@
"type": "Delete",
"signature": {
"type": "RsaSignature2017",
"signatureValue": "cw0RlfNREf+5VdsOYcCBDrv521eiLsDTAYNHKffjF0bozhCnOh+wHkFik7WamUk$
uEiN4L2H6vPlGRprAZGRhEwgy+A7rIFQNmLrpW5qV5UNVI/2F7kngEHqZQgbQYj9hW+5GMYmPkHdv3D72ZefGw$
4Xa2NBLGFpAjQllfzt7kzZLKKY2DM99FdUa64I2Wj3iD04Hs23SbrUdAeuGk/c1Cg6bwGNG4vxoiwn1jikgJLA$
NAlSGjsRGdR7LfbC7GqWWsW3cSNsLFPoU6FyALjgTrrYoHiXe0QHggw+L3yMLfzB2S/L46/VRbyb+WDKMBIXUL$
5owmzHSi6e/ZtCI3w==",
"creator": "http://mastodon.example.org/users/gargron#main-key", "created": "2018-03-03T16:24:11Z"
"signatureValue": "cw0RlfNREf+5VdsOYcCBDrv521eiLsDTAYNHKffjF0bozhCnOh+wHkFik7WamUk$uEiN4L2H6vPlGRprAZGRhEwgy+A7rIFQNmLrpW5qV5UNVI/2F7kngEHqZQgbQYj9hW+5GMYmPkHdv3D72ZefGw$4Xa2NBLGFpAjQllfzt7kzZLKKY2DM99FdUa64I2Wj3iD04Hs23SbrUdAeuGk/c1Cg6bwGNG4vxoiwn1jikgJLA$NAlSGjsRGdR7LfbC7GqWWsW3cSNsLFPoU6FyALjgTrrYoHiXe0QHggw+L3yMLfzB2S/L46/VRbyb+WDKMBIXUL$5owmzHSi6e/ZtCI3w==",
"creator": "http://mastodon.example.org/users/gargron#main-key",
"created": "2018-03-03T16:24:11Z"
},
"object": {
"type": "Tombstone",

View file

@ -1,56 +1,52 @@
{
"@context":[
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://apfed.club/apschema/v1.4"
],
"id":"https://apfed.club/follow/9",
"type":"Follow",
"actor":{
"type":"Person",
"id":"https://apfed.club/channel/indio",
"preferredUsername":"indio",
"name":"Indio",
"updated":"2019-08-20T23:52:34Z",
"icon":{
"type":"Image",
"mediaType":"image/jpeg",
"updated":"2019-08-20T23:53:37Z",
"url":"https://apfed.club/photo/profile/l/2",
"height":300,
"width":300
"id": "https://apfed.club/follow/9",
"type": "Follow",
"actor": {
"type": "Person",
"id": "https://apfed.club/channel/indio",
"preferredUsername": "indio",
"name": "Indio",
"updated": "2019-08-20T23:52:34Z",
"icon": {
"type": "Image",
"mediaType": "image/jpeg",
"updated": "2019-08-20T23:53:37Z",
"url": "https://apfed.club/photo/profile/l/2",
"height": 300,
"width": 300
},
"url":"https://apfed.club/channel/indio",
"inbox":"https://apfed.club/inbox/indio",
"outbox":"https://apfed.club/outbox/indio",
"followers":"https://apfed.club/followers/indio",
"following":"https://apfed.club/following/indio",
"endpoints":{
"sharedInbox":"https://apfed.club/inbox"
"url": "https://apfed.club/channel/indio",
"inbox": "https://apfed.club/inbox/indio",
"outbox": "https://apfed.club/outbox/indio",
"followers": "https://apfed.club/followers/indio",
"following": "https://apfed.club/following/indio",
"endpoints": {
"sharedInbox": "https://apfed.club/inbox"
},
"publicKey":{
"id":"https://apfed.club/channel/indio",
"owner":"https://apfed.club/channel/indio",
"publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA77TIR1VuSYFnmDRFGHHb\n4vaGdx9ranzRX4bfOKAqa++Ch5L4EqJpPy08RuM+NrYCYiYl4QQFDSSDXAEgb5g9\nC1TgWTfI7q/E0UBX2Vr0mU6X4i1ztv0tuQvegRjcSJ7l1AvoBs8Ip4MEJ3OPEQhB\ngJqAACB3Gnps4zi2I0yavkxUfGVKr6zKT3BxWh5hTpKC7Do+ChIrVZC2EwxND9K6
\nsAnQHThcb5EQuvuzUQZKeS7IEOsd0JpZDmJjbfMGrAWE81pLIfEeeA2joCJiBBTO\nglDsW+juvZ+lWqJpMr2hMWpvfrFjJeUawNJCIzsLdVIZR+aKj5yy6yqoS8hkN9Ha\n1MljZpsXl+EmwcwAIqim1YeLwERCEAQ/JWbSt8pQTQbzZ6ibwQ4mchCxacrRbIVR
\nnL59fWMBassJcbY0VwrTugm2SBsYbDjESd55UZV03Rwr8qseGTyi+hH8O7w2SIaY\nzjN6AdZiPmsh00YflzlCk8MSLOHMol1vqIUzXxU8CdXn9+KsuQdZGrTz0YKN/db4\naVwUGJatz2Tsvf7R1tJBjJfeQWOWbbn3pycLVH86LjZ83qngp9ZVnAveUnUqz0yS
\nhe+buZ6UMsfGzbIYon2bKNlz6gYTH0YPcr+cLe+29drtt0GZiXha1agbpo4RB8zE
\naNL2fucF5YT0yNpbd/5WoV0CAwEAAQ==\n-----END PUBLIC KEY-----\n"
"publicKey": {
"id": "https://apfed.club/channel/indio",
"owner": "https://apfed.club/channel/indio",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA77TIR1VuSYFnmDRFGHHb\n4vaGdx9ranzRX4bfOKAqa++Ch5L4EqJpPy08RuM+NrYCYiYl4QQFDSSDXAEgb5g9\nC1TgWTfI7q/E0UBX2Vr0mU6X4i1ztv0tuQvegRjcSJ7l1AvoBs8Ip4MEJ3OPEQhB\ngJqAACB3Gnps4zi2I0yavkxUfGVKr6zKT3BxWh5hTpKC7Do+ChIrVZC2EwxND9K6\nsAnQHThcb5EQuvuzUQZKeS7IEOsd0JpZDmJjbfMGrAWE81pLIfEeeA2joCJiBBTO\nglDsW+juvZ+lWqJpMr2hMWpvfrFjJeUawNJCIzsLdVIZR+aKj5yy6yqoS8hkN9Ha\n1MljZpsXl+EmwcwAIqim1YeLwERCEAQ/JWbSt8pQTQbzZ6ibwQ4mchCxacrRbIVR\nnL59fWMBassJcbY0VwrTugm2SBsYbDjESd55UZV03Rwr8qseGTyi+hH8O7w2SIaY\nzjN6AdZiPmsh00YflzlCk8MSLOHMol1vqIUzXxU8CdXn9+KsuQdZGrTz0YKN/db4\naVwUGJatz2Tsvf7R1tJBjJfeQWOWbbn3pycLVH86LjZ83qngp9ZVnAveUnUqz0yS\nhe+buZ6UMsfGzbIYon2bKNlz6gYTH0YPcr+cLe+29drtt0GZiXha1agbpo4RB8zE\naNL2fucF5YT0yNpbd/5WoV0CAwEAAQ==\n-----END PUBLIC KEY-----\n"
}
},
"object":"https://pleroma.site/users/kaniini",
"to":[
"object": "https://pleroma.site/users/kaniini",
"to": [
"https://pleroma.site/users/kaniini"
],
"signature":{
"@context":[
"signature": {
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1"
],
"type":"RsaSignature2017",
"nonce":"52c035e0a9e81dce8b486159204e97c22637e91f75cdfad5378de91de68e9117",
"creator":"https://apfed.club/channel/indio/public_key_pem",
"created":"2019-08-22T03:38:02Z",
"signatureValue":"oVliRCIqNIh6yUp851dYrF0y21aHp3Rz6VkIpW1pFMWfXuzExyWSfcELpyLseeRmsw5bUu9zJkH44B4G2LiJQKA9UoEQDjrDMZBmbeUpiQqq3DVUzkrBOI8bHZ7xyJ/CjSZcNHHh0MHhSKxswyxWMGi4zIqzkAZG3vRRgoPVHdjPm00sR3B8jBLw1cjoffv+KKeM/zEUpe13gqX9qHAWHHqZepxgSWmq+EKOkRvHUPBXiEJZfXzc5uW+vZ09F3WBYmaRoy8Y0e1P29fnRLqSy7EEINdrHaGclRqoUZyiawpkgy3lWWlynesV/HiLBR7EXT79eKstxf4wfTDaPKBCfTCsOWuMWHr7Genu37ew2/t7eiBGqCwwW12ylhml/OLHgNK3LOhmRABhtfpaFZSxfDVnlXfaLpY1xekVOj2oC0FpBtnoxVKLpIcyLw6dkfSil5ANd+hl59W/bpPA8KT90ii1fSNCo3+FcwQVx0YsPznJNA60XfFuVsme7zNcOst6393e1WriZxBanFpfB63zVQc9u1fjyfktx/yiUNxIlre+sz9OCc0AACn94iRhBYh4bbzdleUOTnM7lnD4Dj2FP+xeDIP8CA8wXUeq5+9kopSp2kAmlUEyFUdg4no7naIeu1SZnopfUg56PsVCp9JHiUK1SYAyWbdC+FbUECu5CvI="
"type": "RsaSignature2017",
"nonce": "52c035e0a9e81dce8b486159204e97c22637e91f75cdfad5378de91de68e9117",
"creator": "https://apfed.club/channel/indio/public_key_pem",
"created": "2019-08-22T03:38:02Z",
"signatureValue": "oVliRCIqNIh6yUp851dYrF0y21aHp3Rz6VkIpW1pFMWfXuzExyWSfcELpyLseeRmsw5bUu9zJkH44B4G2LiJQKA9UoEQDjrDMZBmbeUpiQqq3DVUzkrBOI8bHZ7xyJ/CjSZcNHHh0MHhSKxswyxWMGi4zIqzkAZG3vRRgoPVHdjPm00sR3B8jBLw1cjoffv+KKeM/zEUpe13gqX9qHAWHHqZepxgSWmq+EKOkRvHUPBXiEJZfXzc5uW+vZ09F3WBYmaRoy8Y0e1P29fnRLqSy7EEINdrHaGclRqoUZyiawpkgy3lWWlynesV/HiLBR7EXT79eKstxf4wfTDaPKBCfTCsOWuMWHr7Genu37ew2/t7eiBGqCwwW12ylhml/OLHgNK3LOhmRABhtfpaFZSxfDVnlXfaLpY1xekVOj2oC0FpBtnoxVKLpIcyLw6dkfSil5ANd+hl59W/bpPA8KT90ii1fSNCo3+FcwQVx0YsPznJNA60XfFuVsme7zNcOst6393e1WriZxBanFpfB63zVQc9u1fjyfktx/yiUNxIlre+sz9OCc0AACn94iRhBYh4bbzdleUOTnM7lnD4Dj2FP+xeDIP8CA8wXUeq5+9kopSp2kAmlUEyFUdg4no7naIeu1SZnopfUg56PsVCp9JHiUK1SYAyWbdC+FbUECu5CvI="
}
}

View file

@ -7,6 +7,7 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
import Pleroma.Factory
alias Mix.Tasks.Pleroma.Config, as: MixTask
alias Pleroma.ConfigDB
alias Pleroma.Repo
@ -22,30 +23,41 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
:ok
end
setup_all do: clear_config(:configurable_from_database, true)
defp config_records do
ConfigDB
|> Repo.all()
|> Enum.sort()
end
defp insert_config_record(group, key, value) do
insert(:config,
group: group,
key: key,
value: value
)
end
test "error if file with custom settings doesn't exist" do
Mix.Tasks.Pleroma.Config.migrate_to_db("config/not_existance_config_file.exs")
MixTask.migrate_to_db("config/non_existent_config_file.exs")
assert_receive {:mix_shell, :info,
[
"To migrate settings, you must define custom settings in config/not_existance_config_file.exs."
]},
15
msg =
"To migrate settings, you must define custom settings in config/non_existent_config_file.exs."
assert_receive {:mix_shell, :info, [^msg]}, 15
end
describe "migrate_to_db/1" do
setup do
initial = Application.get_env(:quack, :level)
on_exit(fn -> Application.put_env(:quack, :level, initial) end)
clear_config(:configurable_from_database, true)
clear_config([:quack, :level])
end
@tag capture_log: true
test "config migration refused when deprecated settings are found" do
clear_config([:media_proxy, :whitelist], ["domain_without_scheme.com"])
assert Repo.all(ConfigDB) == []
assert config_records() == []
Mix.Tasks.Pleroma.Config.migrate_to_db("test/fixtures/config/temp.secret.exs")
MixTask.migrate_to_db("test/fixtures/config/temp.secret.exs")
assert_received {:mix_shell, :error, [message]}
@ -54,9 +66,9 @@ test "config migration refused when deprecated settings are found" do
end
test "filtered settings are migrated to db" do
assert Repo.all(ConfigDB) == []
assert config_records() == []
Mix.Tasks.Pleroma.Config.migrate_to_db("test/fixtures/config/temp.secret.exs")
MixTask.migrate_to_db("test/fixtures/config/temp.secret.exs")
config1 = ConfigDB.get_by_params(%{group: ":pleroma", key: ":first_setting"})
config2 = ConfigDB.get_by_params(%{group: ":pleroma", key: ":second_setting"})
@ -71,18 +83,19 @@ test "filtered settings are migrated to db" do
end
test "config table is truncated before migration" do
insert(:config, key: :first_setting, value: [key: "value", key2: ["Activity"]])
assert Repo.aggregate(ConfigDB, :count, :id) == 1
insert_config_record(:pleroma, :first_setting, key: "value", key2: ["Activity"])
assert length(config_records()) == 1
Mix.Tasks.Pleroma.Config.migrate_to_db("test/fixtures/config/temp.secret.exs")
MixTask.migrate_to_db("test/fixtures/config/temp.secret.exs")
config = ConfigDB.get_by_params(%{group: ":pleroma", key: ":first_setting"})
assert config.value == [key: "value", key2: [Repo]]
end
end
describe "with deletion temp file" do
describe "with deletion of temp file" do
setup do
clear_config(:configurable_from_database, true)
temp_file = "config/temp.exported_from_db.secret.exs"
on_exit(fn ->
@ -93,13 +106,13 @@ test "config table is truncated before migration" do
end
test "settings are migrated to file and deleted from db", %{temp_file: temp_file} do
insert(:config, key: :setting_first, value: [key: "value", key2: ["Activity"]])
insert(:config, key: :setting_second, value: [key: "value2", key2: [Repo]])
insert(:config, group: :quack, key: :level, value: :info)
insert_config_record(:pleroma, :setting_first, key: "value", key2: ["Activity"])
insert_config_record(:pleroma, :setting_second, key: "value2", key2: [Repo])
insert_config_record(:quack, :level, :info)
Mix.Tasks.Pleroma.Config.run(["migrate_from_db", "--env", "temp", "-d"])
MixTask.run(["migrate_from_db", "--env", "temp", "-d"])
assert Repo.all(ConfigDB) == []
assert config_records() == []
file = File.read!(temp_file)
assert file =~ "config :pleroma, :setting_first,"
@ -169,9 +182,9 @@ test "load a settings with large values and pass to file", %{temp_file: temp_fil
]
)
Mix.Tasks.Pleroma.Config.run(["migrate_from_db", "--env", "temp", "-d"])
MixTask.run(["migrate_from_db", "--env", "temp", "-d"])
assert Repo.all(ConfigDB) == []
assert config_records() == []
assert File.exists?(temp_file)
{:ok, file} = File.read(temp_file)
@ -186,4 +199,114 @@ test "load a settings with large values and pass to file", %{temp_file: temp_fil
"#{header}\n\nconfig :pleroma, :instance,\n name: \"Pleroma\",\n email: \"example@example.com\",\n notify_email: \"noreply@example.com\",\n description: \"A Pleroma instance, an alternative fediverse server\",\n limit: 5000,\n chat_limit: 5000,\n remote_limit: 100_000,\n upload_limit: 16_000_000,\n avatar_upload_limit: 2_000_000,\n background_upload_limit: 4_000_000,\n banner_upload_limit: 4_000_000,\n poll_limits: %{\n max_expiration: 31_536_000,\n max_option_chars: 200,\n max_options: 20,\n min_expiration: 0\n },\n registrations_open: true,\n federating: true,\n federation_incoming_replies_max_depth: 100,\n federation_reachability_timeout_days: 7,\n federation_publisher_modules: [Pleroma.Web.ActivityPub.Publisher],\n allow_relay: true,\n public: true,\n quarantined_instances: [],\n managed_config: true,\n static_dir: \"instance/static/\",\n allowed_post_formats: [\"text/plain\", \"text/html\", \"text/markdown\", \"text/bbcode\"],\n autofollowed_nicknames: [],\n max_pinned_statuses: 1,\n attachment_links: false,\n max_report_comment_size: 1000,\n safe_dm_mentions: false,\n healthcheck: false,\n remote_post_retention_days: 90,\n skip_thread_containment: true,\n limit_to_local_content: :unauthenticated,\n user_bio_length: 5000,\n user_name_length: 100,\n max_account_fields: 10,\n max_remote_account_fields: 20,\n account_field_name_length: 512,\n account_field_value_length: 2048,\n external_user_synchronization: true,\n extended_nickname_format: true,\n multi_factor_authentication: [\n totp: [digits: 6, period: 30],\n backup_codes: [number: 2, length: 6]\n ]\n"
end
end
describe "operations on database config" do
setup do: clear_config(:configurable_from_database, true)
test "dumping a specific group" do
insert_config_record(:pleroma, :instance, name: "Pleroma Test")
insert_config_record(:web_push_encryption, :vapid_details,
subject: "mailto:administrator@example.com",
public_key:
"BOsPL-_KjNnjj_RMvLeR3dTOrcndi4TbMR0cu56gLGfGaT5m1gXxSfRHOcC4Dd78ycQL1gdhtx13qgKHmTM5xAI",
private_key: "Ism6FNdS31nLCA94EfVbJbDdJXCxAZ8cZiB1JQPN_t4"
)
MixTask.run(["dump", "pleroma"])
assert_receive {:mix_shell, :info,
["config :pleroma, :instance, [name: \"Pleroma Test\"]\r\n\r\n"]}
refute_receive {
:mix_shell,
:info,
[
"config :web_push_encryption, :vapid_details, [subject: \"mailto:administrator@example.com\", public_key: \"BOsPL-_KjNnjj_RMvLeR3dTOrcndi4TbMR0cu56gLGfGaT5m1gXxSfRHOcC4Dd78ycQL1gdhtx13qgKHmTM5xAI\", private_key: \"Ism6FNdS31nLCA94EfVbJbDdJXCxAZ8cZiB1JQPN_t4\"]\r\n\r\n"
]
}
# Ensure operations work when using atom syntax
MixTask.run(["dump", ":pleroma"])
assert_receive {:mix_shell, :info,
["config :pleroma, :instance, [name: \"Pleroma Test\"]\r\n\r\n"]}
end
test "dumping a specific key in a group" do
insert_config_record(:pleroma, :instance, name: "Pleroma Test")
insert_config_record(:pleroma, Pleroma.Captcha, enabled: false)
MixTask.run(["dump", "pleroma", "Pleroma.Captcha"])
refute_receive {:mix_shell, :info,
["config :pleroma, :instance, [name: \"Pleroma Test\"]\r\n\r\n"]}
assert_receive {:mix_shell, :info,
["config :pleroma, Pleroma.Captcha, [enabled: false]\r\n\r\n"]}
end
test "dumps all configuration successfully" do
insert_config_record(:pleroma, :instance, name: "Pleroma Test")
insert_config_record(:pleroma, Pleroma.Captcha, enabled: false)
MixTask.run(["dump"])
assert_receive {:mix_shell, :info,
["config :pleroma, :instance, [name: \"Pleroma Test\"]\r\n\r\n"]}
assert_receive {:mix_shell, :info,
["config :pleroma, Pleroma.Captcha, [enabled: false]\r\n\r\n"]}
end
end
describe "when configdb disabled" do
test "refuses to dump" do
clear_config(:configurable_from_database, false)
insert_config_record(:pleroma, :instance, name: "Pleroma Test")
MixTask.run(["dump"])
msg =
"ConfigDB not enabled. Please check the value of :configurable_from_database in your configuration."
assert_receive {:mix_shell, :error, [^msg]}
end
end
describe "destructive operations" do
setup do: clear_config(:configurable_from_database, true)
setup do
insert_config_record(:pleroma, :instance, name: "Pleroma Test")
insert_config_record(:pleroma, Pleroma.Captcha, enabled: false)
insert_config_record(:pleroma2, :key2, z: 1)
assert length(config_records()) == 3
:ok
end
test "deletes group of settings" do
MixTask.run(["delete", "--force", "pleroma"])
assert [%ConfigDB{group: :pleroma2, key: :key2}] = config_records()
end
test "deletes specified key" do
MixTask.run(["delete", "--force", "pleroma", "Pleroma.Captcha"])
assert [
%ConfigDB{group: :pleroma, key: :instance},
%ConfigDB{group: :pleroma2, key: :key2}
] = config_records()
end
test "resets entire config" do
MixTask.run(["reset", "--force"])
assert config_records() == []
end
end
end

View file

@ -73,7 +73,7 @@ test "it prunes old objects from the database" do
describe "running update_users_following_followers_counts" do
test "following and followers count are updated" do
[user, user2] = insert_pair(:user)
{:ok, %User{} = user} = User.follow(user, user2)
{:ok, %User{} = user, _user2} = User.follow(user, user2)
following = User.following(user)
@ -87,7 +87,8 @@ test "following and followers count are updated" do
assert user.follower_count == 3
assert :ok == Mix.Tasks.Pleroma.Database.run(["update_users_following_followers_counts"])
assert {:ok, :ok} ==
Mix.Tasks.Pleroma.Database.run(["update_users_following_followers_counts"])
user = User.get_by_id(user.id)

View file

@ -36,7 +36,7 @@ test "user is created" do
unsaved = build(:user)
# prepare to answer yes
send(self(), {:mix_shell_input, :yes?, true})
send(self(), {:mix_shell_input, :prompt, "Y"})
Mix.Tasks.Pleroma.User.run([
"new",
@ -55,7 +55,7 @@ test "user is created" do
assert_received {:mix_shell, :info, [message]}
assert message =~ "user will be created"
assert_received {:mix_shell, :yes?, [message]}
assert_received {:mix_shell, :prompt, [message]}
assert message =~ "Continue"
assert_received {:mix_shell, :info, [message]}
@ -73,14 +73,14 @@ test "user is not created" do
unsaved = build(:user)
# prepare to answer no
send(self(), {:mix_shell_input, :yes?, false})
send(self(), {:mix_shell_input, :prompt, "N"})
Mix.Tasks.Pleroma.User.run(["new", unsaved.nickname, unsaved.email])
assert_received {:mix_shell, :info, [message]}
assert message =~ "user will be created"
assert_received {:mix_shell, :yes?, [message]}
assert_received {:mix_shell, :prompt, [message]}
assert message =~ "Continue"
assert_received {:mix_shell, :info, [message]}
@ -503,7 +503,7 @@ test "it returns users matching" do
moot = insert(:user, nickname: "moot")
kawen = insert(:user, nickname: "kawen", name: "fediverse expert moon")
{:ok, user} = User.follow(user, moon)
{:ok, user, moon} = User.follow(user, moon)
assert [moon.id, kawen.id] == User.Search.search("moon") |> Enum.map(& &1.id)

View file

@ -0,0 +1,45 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Activity.SearchTest do
alias Pleroma.Activity.Search
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
use Pleroma.DataCase
test "it finds something" do
user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "it's wednesday my dudes"})
[result] = Search.search(nil, "wednesday")
assert result.id == post.id
end
test "using plainto_tsquery on postgres < 11" do
old_version = :persistent_term.get({Pleroma.Repo, :postgres_version})
:persistent_term.put({Pleroma.Repo, :postgres_version}, 10.0)
on_exit(fn -> :persistent_term.put({Pleroma.Repo, :postgres_version}, old_version) end)
user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "it's wednesday my dudes"})
{:ok, _post2} = CommonAPI.post(user, %{status: "it's wednesday my bros"})
# plainto doesn't understand complex queries
assert [result] = Search.search(nil, "wednesday -dudes")
assert result.id == post.id
end
test "using websearch_to_tsquery" do
user = insert(:user)
{:ok, _post} = CommonAPI.post(user, %{status: "it's wednesday my dudes"})
{:ok, other_post} = CommonAPI.post(user, %{status: "it's wednesday my bros"})
assert [result] = Search.search(nil, "wednesday -dudes")
assert result.id == other_post.id
end
end

View file

@ -197,6 +197,13 @@ test "all_by_ids_with_object/1" do
assert [%{id: ^id1, object: %Object{}}, %{id: ^id2, object: %Object{}}] = activities
end
test "get_by_id_with_user_actor/1" do
user = insert(:user)
activity = insert(:note_activity, note: insert(:note, user: user))
assert Activity.get_by_id_with_user_actor(activity.id).user_actor == user
end
test "get_by_id_with_object/1" do
%{id: id} = insert(:note_activity)

View file

@ -12,6 +12,25 @@ defmodule Pleroma.ApplicationRequirementsTest do
alias Pleroma.Config
alias Pleroma.Repo
describe "check_repo_pool_size!/1" do
test "raises if the pool size is unexpected" do
clear_config([Pleroma.Repo, :pool_size], 11)
assert_raise Pleroma.ApplicationRequirements.VerifyError,
"Repo.pool_size different than recommended value.",
fn ->
capture_log(&Pleroma.ApplicationRequirements.verify!/0)
end
end
test "doesn't raise if the pool size is unexpected but the respective flag is set" do
clear_config([Pleroma.Repo, :pool_size], 11)
clear_config([:dangerzone, :override_repo_pool_size], true)
assert Pleroma.ApplicationRequirements.verify!() == :ok
end
end
describe "check_welcome_message_config!/1" do
setup do: clear_config([:welcome])
setup do: clear_config([Pleroma.Emails.Mailer])

View file

@ -19,7 +19,7 @@ test "getting the home timeline" do
user = insert(:user)
followed = insert(:user)
{:ok, user} = User.follow(user, followed)
{:ok, user, followed} = User.follow(user, followed)
{:ok, _first} = CommonAPI.post(user, %{status: "hey"})
{:ok, _second} = CommonAPI.post(followed, %{status: "hello"})

View file

@ -9,8 +9,22 @@ defmodule Pleroma.EmojiTest do
describe "is_unicode_emoji?/1" do
test "tells if a string is an unicode emoji" do
refute Emoji.is_unicode_emoji?("X")
assert Emoji.is_unicode_emoji?("")
refute Emoji.is_unicode_emoji?("")
# Only accept fully-qualified (RGI) emoji
# See http://www.unicode.org/reports/tr51/
refute Emoji.is_unicode_emoji?("")
refute Emoji.is_unicode_emoji?("")
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?("🇵")
assert Emoji.is_unicode_emoji?("🇴")
assert Emoji.is_unicode_emoji?("🇬")
end
end

View file

@ -241,16 +241,14 @@ test "it can parse mentions and return the relevant users" do
"@@gsimg According to @archaeme, that is @daggsy. Also hello @archaeme@archae.me and @o and @@@jimm"
o = insert(:user, %{nickname: "o"})
jimm = insert(:user, %{nickname: "jimm"})
gsimg = insert(:user, %{nickname: "gsimg"})
_jimm = insert(:user, %{nickname: "jimm"})
_gsimg = insert(:user, %{nickname: "gsimg"})
archaeme = insert(:user, %{nickname: "archaeme"})
archaeme_remote = insert(:user, %{nickname: "archaeme@archae.me"})
expected_mentions = [
{"@archaeme", archaeme},
{"@archaeme@archae.me", archaeme_remote},
{"@gsimg", gsimg},
{"@jimm", jimm},
{"@o", o}
]

View file

@ -3,6 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Instances.InstanceTest do
alias Pleroma.Instances
alias Pleroma.Instances.Instance
alias Pleroma.Repo
@ -148,5 +149,13 @@ test "Handles not getting a favicon URL properly" do
)
end) =~ "Instance.scrape_favicon(\"https://no-favicon.example.org/\") error: "
end
test "Doesn't scrapes unreachable instances" do
instance = insert(:instance, unreachable_since: Instances.reachability_datetime_threshold())
url = "https://" <> instance.host
assert capture_log(fn -> assert nil == Instance.get_or_update_favicon(URI.parse(url)) end) =~
"Instance.scrape_favicon(\"#{url}\") ignored unreachable host"
end
end
end

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