Compare commits
65 commits
translatio
...
develop
Author | SHA1 | Date | |
---|---|---|---|
Floatingghost | 778b213945 | ||
floatingghost | 8f97c15b07 | ||
Floatingghost | 3af0c53a86 | ||
Oneric | fc7e07f424 | ||
Oneric | 59685e25d2 | ||
Oneric | 65aeaefa41 | ||
Oneric | 5d6cb6a459 | ||
floatingghost | 8afc3bee7a | ||
floatingghost | 72871d4514 | ||
floatingghost | 72af38c0e9 | ||
Floatingghost | ae19fd90c9 | ||
Floatingghost | 66b3248dd3 | ||
Floatingghost | 73ead8656a | ||
Floatingghost | f32a7fd76a | ||
Floatingghost | 4078fd655c | ||
floatingghost | 5bdef8c724 | ||
floatingghost | cdc918c8f1 | ||
Floatingghost | f15eded3e1 | ||
Oneric | 05eda169fe | ||
floatingghost | 3ce855cbde | ||
Floatingghost | da67e69af5 | ||
Norm | c2d3221be3 | ||
Floatingghost | 5e92f955ac | ||
Floatingghost | b72127b45a | ||
Oneric | 9a91299f96 | ||
Oneric | fbd961c747 | ||
Oneric | 0c2b33458d | ||
Floatingghost | 842cac2a50 | ||
3e1f5e5372 | |||
3a21293970 | |||
0d66237205 | |||
Oneric | 6ef6b2a289 | ||
Oneric | 94e9c8f48a | ||
Oneric | 873aa9da1c | ||
Oneric | 34a48cb87f | ||
50403351f4 | |||
a953b1d927 | |||
Norm | bb29c5bed2 | ||
Norm | bc46f3da4c | ||
Norm | 7e709768c3 | ||
floatingghost | 76ded10a70 | ||
Floatingghost | 4457928e32 | ||
floatingghost | ee03149ba1 | ||
Floatingghost | ea6bc8a7c5 | ||
Floatingghost | bd74693db6 | ||
Oneric | 5256678901 | ||
floatingghost | fdeecc7b4c | ||
floatingghost | 51482c4fe8 | ||
Oneric | b7e3d44756 | ||
Norm | 8ae54b260a | ||
Floatingghost | 21a81e1111 | ||
Floatingghost | 3738ab67bd | ||
Norm | 549d580054 | ||
Floatingghost | 010e8c7bb2 | ||
Floatingghost | 9671cdecdf | ||
Floatingghost | f531484063 | ||
Floatingghost | ec7e9da734 | ||
FloatingGhost | 3c384c1b76 | ||
FloatingGhost | 2437a3e9ba | ||
FloatingGhost | ad7dcf38a8 | ||
91d9d750c0 | |||
3c07aa506d | |||
64050b0fb5 | |||
7babc11475 | |||
Norm | 771a306dc1 |
|
@ -1 +0,0 @@
|
||||||
https://github.com/hashnuke/heroku-buildpack-elixir
|
|
|
@ -1,4 +1,5 @@
|
||||||
platform: linux/amd64
|
labels:
|
||||||
|
platform: linux/amd64
|
||||||
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- test
|
- test
|
||||||
|
@ -34,7 +35,7 @@ variables:
|
||||||
- &clean "(rm -rf release || true) && (rm -rf _build || true) && (rm -rf /root/.mix)"
|
- &clean "(rm -rf release || true) && (rm -rf _build || true) && (rm -rf /root/.mix)"
|
||||||
- &mix-clean "mix deps.clean --all && mix clean"
|
- &mix-clean "mix deps.clean --all && mix clean"
|
||||||
|
|
||||||
pipeline:
|
steps:
|
||||||
# Canonical amd64
|
# Canonical amd64
|
||||||
debian-bookworm:
|
debian-bookworm:
|
||||||
image: hexpm/elixir:1.15.4-erlang-26.0.2-debian-bookworm-20230612
|
image: hexpm/elixir:1.15.4-erlang-26.0.2-debian-bookworm-20230612
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
platform: linux/arm64
|
labels:
|
||||||
|
platform: linux/aarch64
|
||||||
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- test
|
- test
|
||||||
|
@ -34,7 +35,7 @@ variables:
|
||||||
- &clean "(rm -rf release || true) && (rm -rf _build || true) && (rm -rf /root/.mix)"
|
- &clean "(rm -rf release || true) && (rm -rf _build || true) && (rm -rf /root/.mix)"
|
||||||
- &mix-clean "mix deps.clean --all && mix clean"
|
- &mix-clean "mix deps.clean --all && mix clean"
|
||||||
|
|
||||||
pipeline:
|
steps:
|
||||||
# Canonical arm64
|
# Canonical arm64
|
||||||
debian-bookworm:
|
debian-bookworm:
|
||||||
image: hexpm/elixir:1.15.4-erlang-26.0.2-debian-bookworm-20230612
|
image: hexpm/elixir:1.15.4-erlang-26.0.2-debian-bookworm-20230612
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
platform: linux/amd64
|
labels:
|
||||||
|
platform: linux/amd64
|
||||||
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- test
|
- test
|
||||||
|
@ -45,7 +46,7 @@ variables:
|
||||||
- &clean "(rm -rf release || true) && (rm -rf _build || true) && (rm -rf /root/.mix)"
|
- &clean "(rm -rf release || true) && (rm -rf _build || true) && (rm -rf /root/.mix)"
|
||||||
- &mix-clean "mix deps.clean --all && mix clean"
|
- &mix-clean "mix deps.clean --all && mix clean"
|
||||||
|
|
||||||
pipeline:
|
steps:
|
||||||
docs:
|
docs:
|
||||||
<<: *on-point-release
|
<<: *on-point-release
|
||||||
secrets:
|
secrets:
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
platform: linux/amd64
|
labels:
|
||||||
|
platform: linux/amd64
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
- &scw-secrets
|
- &scw-secrets
|
||||||
|
@ -41,9 +42,9 @@ variables:
|
||||||
- &clean "(rm -rf release || true) && (rm -rf _build || true) && (rm -rf /root/.mix)"
|
- &clean "(rm -rf release || true) && (rm -rf _build || true) && (rm -rf /root/.mix)"
|
||||||
- &mix-clean "mix deps.clean --all && mix clean"
|
- &mix-clean "mix deps.clean --all && mix clean"
|
||||||
|
|
||||||
pipeline:
|
steps:
|
||||||
lint:
|
lint:
|
||||||
image: akkoma/ci-base:1.15-otp26
|
image: akkoma/ci-base:1.16-otp26
|
||||||
<<: *on-pr-open
|
<<: *on-pr-open
|
||||||
environment:
|
environment:
|
||||||
MIX_ENV: test
|
MIX_ENV: test
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
platform: linux/amd64
|
labels:
|
||||||
|
platform: linux/amd64
|
||||||
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- lint
|
- lint
|
||||||
|
@ -12,12 +13,6 @@ matrix:
|
||||||
- 25
|
- 25
|
||||||
- 26
|
- 26
|
||||||
include:
|
include:
|
||||||
- ELIXIR_VERSION: 1.14
|
|
||||||
OTP_VERSION: 25
|
|
||||||
- ELIXIR_VERSION: 1.15
|
|
||||||
OTP_VERSION: 25
|
|
||||||
- ELIXIR_VERSION: 1.15
|
|
||||||
OTP_VERSION: 26
|
|
||||||
- ELIXIR_VERSION: 1.16
|
- ELIXIR_VERSION: 1.16
|
||||||
OTP_VERSION: 26
|
OTP_VERSION: 26
|
||||||
|
|
||||||
|
@ -73,7 +68,7 @@ services:
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
|
|
||||||
pipeline:
|
steps:
|
||||||
test:
|
test:
|
||||||
image: akkoma/ci-base:${ELIXIR_VERSION}-otp${OTP_VERSION}
|
image: akkoma/ci-base:${ELIXIR_VERSION}-otp${OTP_VERSION}
|
||||||
<<: *on-pr-open
|
<<: *on-pr-open
|
||||||
|
|
17
CHANGELOG.md
17
CHANGELOG.md
|
@ -4,7 +4,21 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
## Unreleased
|
## 2024.04.1 (Security)
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
- Issue allowing non-owners to use media objects in posts
|
||||||
|
- Issue allowing use of non-media objects as attachments and crashing timeline rendering
|
||||||
|
- Issue allowing webfinger spoofing in certain situations
|
||||||
|
|
||||||
|
## Added
|
||||||
|
- Implement [FEP-67ff](https://codeberg.org/fediverse/fep/src/branch/main/fep/67ff/fep-67ff.md) (federation documentation)
|
||||||
|
|
||||||
|
## Added
|
||||||
|
- Meilisearch: it is now possible to use separate keys for search and admin actions
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
- Meilisearch: order of results returned from our REST API now actually matches how Meilisearch ranks results
|
||||||
|
|
||||||
## 2024.04
|
## 2024.04
|
||||||
|
|
||||||
|
@ -37,6 +51,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Issue leading to Mastodon bot accounts being rejected
|
- Issue leading to Mastodon bot accounts being rejected
|
||||||
- Scope misdetection of remote posts resulting from not recognising
|
- Scope misdetection of remote posts resulting from not recognising
|
||||||
JSON-LD-compacted forms of public scope; affected e.g. federation with bovine
|
JSON-LD-compacted forms of public scope; affected e.g. federation with bovine
|
||||||
|
- Ratelimits encountered when fetching objects are now respected; 429 responses will cause a backoff when we get one.
|
||||||
|
|
||||||
## Removed
|
## Removed
|
||||||
- ActivityPub Client-To-Server write API endpoints have been disabled;
|
- ActivityPub Client-To-Server write API endpoints have been disabled;
|
||||||
|
|
42
FEDERATION.md
Normal file
42
FEDERATION.md
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
# Federation
|
||||||
|
|
||||||
|
## Supported federation protocols and standards
|
||||||
|
|
||||||
|
- [ActivityPub](https://www.w3.org/TR/activitypub/) (Server-to-Server)
|
||||||
|
- [WebFinger](https://webfinger.net/)
|
||||||
|
- [Http Signatures](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures)
|
||||||
|
- [NodeInfo](https://nodeinfo.diaspora.software/)
|
||||||
|
|
||||||
|
## Supported FEPs
|
||||||
|
|
||||||
|
- [FEP-67ff: FEDERATION](https://codeberg.org/fediverse/fep/src/branch/main/fep/67ff/fep-67ff.md)
|
||||||
|
- [FEP-f1d5: NodeInfo in Fediverse Software](https://codeberg.org/fediverse/fep/src/branch/main/fep/f1d5/fep-f1d5.md)
|
||||||
|
- [FEP-fffd: Proxy Objects](https://codeberg.org/fediverse/fep/src/branch/main/fep/fffd/fep-fffd.md)
|
||||||
|
|
||||||
|
## ActivityPub
|
||||||
|
|
||||||
|
Akkoma mostly follows the server-to-server parts of the ActivityPub standard,
|
||||||
|
but implements quirks for Mastodon compatibility as well as Mastodon-specific
|
||||||
|
and custom extensions.
|
||||||
|
|
||||||
|
See our documentation and Mastodon’s federation information
|
||||||
|
linked further below for details on these quirks and extensions.
|
||||||
|
|
||||||
|
Akkoma does not perform JSON-LD processing.
|
||||||
|
|
||||||
|
### Required extensions
|
||||||
|
|
||||||
|
#### HTTP Signatures
|
||||||
|
All AP S2S POST requests to Akkoma instances MUST be signed.
|
||||||
|
Depending on instance configuration the same may be true for GET requests.
|
||||||
|
|
||||||
|
## Nodeinfo
|
||||||
|
|
||||||
|
Akkoma provides many additional entries in its nodeinfo response,
|
||||||
|
see the documentation linked below for details.
|
||||||
|
|
||||||
|
## Additional documentation
|
||||||
|
|
||||||
|
- [Akkoma’s ActivityPub extensions](https://docs.akkoma.dev/develop/development/ap_extensions/)
|
||||||
|
- [Akkoma’s nodeinfo extensions](https://docs.akkoma.dev/develop/development/nodeinfo_extensions/)
|
||||||
|
- [Mastodon’s federation requirements](https://github.com/mastodon/mastodon/blob/main/FEDERATION.md)
|
|
@ -1,25 +0,0 @@
|
||||||
import Config
|
|
||||||
|
|
||||||
config :pleroma, Pleroma.Web.Endpoint,
|
|
||||||
http: [
|
|
||||||
port: String.to_integer(System.get_env("PORT") || "4000"),
|
|
||||||
protocol_options: [max_request_line_length: 8192, max_header_value_length: 8192]
|
|
||||||
],
|
|
||||||
protocol: "http",
|
|
||||||
secure_cookie_flag: false,
|
|
||||||
url: [host: System.get_env("APP_HOST"), scheme: "https", port: 443],
|
|
||||||
secret_key_base: "+S+ULgf7+N37c/lc9K66SMphnjQIRGklTu0BRr2vLm2ZzvK0Z6OH/PE77wlUNtvP"
|
|
||||||
|
|
||||||
database_url =
|
|
||||||
System.get_env("DATABASE_URL") ||
|
|
||||||
raise """
|
|
||||||
environment variable DATABASE_URL is missing.
|
|
||||||
For example: ecto://USER:PASS@HOST/DATABASE
|
|
||||||
"""
|
|
||||||
|
|
||||||
config :pleroma, Pleroma.Repo,
|
|
||||||
# ssl: true,
|
|
||||||
url: database_url,
|
|
||||||
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
|
|
||||||
|
|
||||||
config :pleroma, :instance, name: "#{System.get_env("APP_NAME")} CI Instance"
|
|
|
@ -1,12 +1,15 @@
|
||||||
# Akkoma Clients
|
# Akkoma Clients
|
||||||
Note: Additional clients may work, but these are known to work with Akkoma.
|
This is a list of clients that are known to work with Akkoma.
|
||||||
Apps listed here might not support all of Akkoma's features.
|
|
||||||
|
!!! warning
|
||||||
|
**Clients listed here are not officially supported by the Akkoma project.**
|
||||||
|
Some Akkoma features may be unsupported by these clients.
|
||||||
|
|
||||||
## Multiplatform
|
## Multiplatform
|
||||||
### Kaiteki
|
### Kaiteki
|
||||||
- Homepage: <https://kaiteki.app/>
|
- Homepage: <https://kaiteki.app/>
|
||||||
- Source Code: <https://github.com/Kaiteki-Fedi/Kaiteki>
|
- Source Code: <https://github.com/Kaiteki-Fedi/Kaiteki>
|
||||||
- Contact: [@kaiteki@fedi.software](https://fedi.software/@Kaiteki)
|
- Contact: [@kaiteki@social.kaiteki.app](https://social.kaiteki.app/@kaiteki)
|
||||||
- Platforms: Web, Windows, Linux, Android
|
- Platforms: Web, Windows, Linux, Android
|
||||||
- Features: MastoAPI, Supports multiple backends
|
- Features: MastoAPI, Supports multiple backends
|
||||||
|
|
||||||
|
@ -38,12 +41,6 @@ Apps listed here might not support all of Akkoma's features.
|
||||||
- Platforms: Android
|
- Platforms: Android
|
||||||
- Features: MastoAPI, No Streaming, Emoji Reactions, Text Formatting, FE Stickers
|
- Features: MastoAPI, No Streaming, Emoji Reactions, Text Formatting, FE Stickers
|
||||||
|
|
||||||
### Fedi
|
|
||||||
- Homepage: <https://www.fediapp.com/>
|
|
||||||
- Source Code: Proprietary, but gratis
|
|
||||||
- Platforms: iOS, Android
|
|
||||||
- Features: MastoAPI, Pleroma-specific features like Reactions
|
|
||||||
|
|
||||||
### Tusky
|
### Tusky
|
||||||
- Homepage: <https://tuskyapp.github.io/>
|
- Homepage: <https://tuskyapp.github.io/>
|
||||||
- Source Code: <https://github.com/tuskyapp/Tusky>
|
- Source Code: <https://github.com/tuskyapp/Tusky>
|
||||||
|
@ -51,12 +48,18 @@ Apps listed here might not support all of Akkoma's features.
|
||||||
- Platforms: Android
|
- Platforms: Android
|
||||||
- Features: MastoAPI, No Streaming
|
- Features: MastoAPI, No Streaming
|
||||||
|
|
||||||
|
### Subway Tooter
|
||||||
|
- Source Code: <https://github.com/tateisu/SubwayTooter/>
|
||||||
|
- Contact: [@SubwayTooter@mastodon.juggler.jp](https://mastodon.juggler.jp/@SubwayTooter)
|
||||||
|
- Platforms: Android
|
||||||
|
- Features: MastoAPI, Editing, Emoji Reactions (including custom emoji)
|
||||||
|
|
||||||
## Alternative Web Interfaces
|
## Alternative Web Interfaces
|
||||||
### Pinafore
|
### Enafore
|
||||||
- Note: Pinafore is unmaintained (See [the author's original article](https://nolanlawson.com/2023/01/09/retiring-pinafore/) for details)
|
- An actively developed fork of Pinafore with improved Akkoma support
|
||||||
- Homepage: <https://pinafore.social/>
|
- Homepage: <https://enafore.social/>
|
||||||
- Source Code: <https://github.com/nolanlawson/pinafore>
|
- Source Code: <https://github.com/enafore/enafore>
|
||||||
- Contact: [@pinafore@mastodon.technology](https://mastodon.technology/users/pinafore)
|
- Contact: [@enfore@enafore.social](https://meta.enafore.social/@enafore)
|
||||||
- Features: MastoAPI, No Streaming
|
- Features: MastoAPI, No Streaming
|
||||||
|
|
||||||
### Sengi
|
### Sengi
|
||||||
|
|
|
@ -63,6 +63,8 @@ To add configuration to your config file, you can copy it from the base config.
|
||||||
* `local_bubble`: Array of domains representing instances closely related to yours. Used to populate the `bubble` timeline. e.g `["example.com"]`, (default: `[]`)
|
* `local_bubble`: Array of domains representing instances closely related to yours. Used to populate the `bubble` timeline. e.g `["example.com"]`, (default: `[]`)
|
||||||
* `languages`: List of Language Codes used by the instance. This is used to try and set a default language from the frontend. It will try and find the first match between the languages set here and the user's browser languages. It will default to the first language in this setting if there is no match.. (default `["en"]`)
|
* `languages`: List of Language Codes used by the instance. This is used to try and set a default language from the frontend. It will try and find the first match between the languages set here and the user's browser languages. It will default to the first language in this setting if there is no match.. (default `["en"]`)
|
||||||
* `export_prometheus_metrics`: Enable prometheus metrics, served at `/api/v1/akkoma/metrics`, requiring the `admin:metrics` oauth scope.
|
* `export_prometheus_metrics`: Enable prometheus metrics, served at `/api/v1/akkoma/metrics`, requiring the `admin:metrics` oauth scope.
|
||||||
|
* `privileged_staff`: Set to `true` to give moderators access to a few higher responsibility actions.
|
||||||
|
* `federated_timeline_available`: Set to `false` to remove access to the federated timeline for all users.
|
||||||
|
|
||||||
## :database
|
## :database
|
||||||
* `improved_hashtag_timeline`: Setting to force toggle / force disable improved hashtags timeline. `:enabled` forces hashtags to be fetched from `hashtags` table for hashtags timeline. `:disabled` forces object-embedded hashtags to be used (slower). Keep it `:auto` for automatic behaviour (it is auto-set to `:enabled` [unless overridden] when HashtagsTableMigrator completes).
|
* `improved_hashtag_timeline`: Setting to force toggle / force disable improved hashtags timeline. `:enabled` forces hashtags to be fetched from `hashtags` table for hashtags timeline. `:disabled` forces object-embedded hashtags to be used (slower). Keep it `:auto` for automatic behaviour (it is auto-set to `:enabled` [unless overridden] when HashtagsTableMigrator completes).
|
||||||
|
|
|
@ -6,37 +6,17 @@ With the `mediaproxy` function you can use nginx to cache this content, so users
|
||||||
|
|
||||||
## Activate it
|
## Activate it
|
||||||
|
|
||||||
* Edit your nginx config and add the following location to your main server block:
|
|
||||||
```
|
|
||||||
location /proxy {
|
|
||||||
return 404;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* Set up a subdomain for the proxy with its nginx config on the same machine
|
* Set up a subdomain for the proxy with its nginx config on the same machine
|
||||||
*(the latter is not strictly required, but for simplicity we’ll assume so)*
|
* Edit the nginx config for the upload/MediaProxy subdomain to point to the subdomain that has been set up
|
||||||
* In this subdomain’s server block add
|
|
||||||
```
|
|
||||||
location /proxy {
|
|
||||||
proxy_cache akkoma_media_cache;
|
|
||||||
proxy_cache_lock on;
|
|
||||||
proxy_pass http://localhost:4000;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Also add the following on top of the configuration, outside of the `server` block:
|
|
||||||
```
|
|
||||||
proxy_cache_path /tmp/akkoma-media-cache levels=1:2 keys_zone=akkoma_media_cache:10m max_size=10g inactive=720m use_temp_path=off;
|
|
||||||
```
|
|
||||||
If you came here from one of the installation guides, take a look at the example configuration `/installation/nginx/akkoma.nginx`, where this part is already included.
|
|
||||||
|
|
||||||
* Append the following to your `prod.secret.exs` or `dev.secret.exs` (depends on which mode your instance is running):
|
* Append the following to your `prod.secret.exs` or `dev.secret.exs` (depends on which mode your instance is running):
|
||||||
```
|
```elixir
|
||||||
|
# Replace media.example.td with the subdomain you set up earlier
|
||||||
config :pleroma, :media_proxy,
|
config :pleroma, :media_proxy,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
proxy_opts: [
|
proxy_opts: [
|
||||||
redirect_on_failure: true
|
redirect_on_failure: true
|
||||||
],
|
],
|
||||||
base_url: "https://cache.akkoma.social"
|
base_url: "https://media.example.tld"
|
||||||
```
|
```
|
||||||
You **really** should use a subdomain to serve proxied files; while we will fix bugs resulting from this, serving arbitrary remote content on your main domain namespace is a significant attack surface.
|
You **really** should use a subdomain to serve proxied files; while we will fix bugs resulting from this, serving arbitrary remote content on your main domain namespace is a significant attack surface.
|
||||||
|
|
||||||
|
|
|
@ -130,59 +130,26 @@ config :pleroma, :http_security,
|
||||||
enabled: false
|
enabled: false
|
||||||
```
|
```
|
||||||
|
|
||||||
Use this as the Nginx config:
|
In the Nginx config, add the following into the `location /` block:
|
||||||
```
|
```nginx
|
||||||
proxy_cache_path /tmp/akkoma-media-cache levels=1:2 keys_zone=akkoma_media_cache:10m max_size=10g inactive=720m use_temp_path=off;
|
|
||||||
# The above already exists in a clearnet instance's config.
|
|
||||||
# If not, add it.
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 127.0.0.1:14447;
|
|
||||||
server_name youri2paddress;
|
|
||||||
|
|
||||||
# Comment to enable logs
|
|
||||||
access_log /dev/null;
|
|
||||||
error_log /dev/null;
|
|
||||||
|
|
||||||
gzip_vary on;
|
|
||||||
gzip_proxied any;
|
|
||||||
gzip_comp_level 6;
|
|
||||||
gzip_buffers 16 8k;
|
|
||||||
gzip_http_version 1.1;
|
|
||||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript application/activity+json application/atom+xml;
|
|
||||||
|
|
||||||
client_max_body_size 16m;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
|
|
||||||
add_header X-XSS-Protection "0";
|
add_header X-XSS-Protection "0";
|
||||||
add_header X-Permitted-Cross-Domain-Policies none;
|
add_header X-Permitted-Cross-Domain-Policies none;
|
||||||
add_header X-Frame-Options DENY;
|
add_header X-Frame-Options DENY;
|
||||||
add_header X-Content-Type-Options nosniff;
|
add_header X-Content-Type-Options nosniff;
|
||||||
add_header Referrer-Policy same-origin;
|
add_header Referrer-Policy same-origin;
|
||||||
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
|
|
||||||
proxy_pass http://localhost:4000;
|
|
||||||
|
|
||||||
client_max_body_size 16m;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /proxy {
|
|
||||||
proxy_cache akkoma_media_cache;
|
|
||||||
proxy_cache_lock on;
|
|
||||||
proxy_ignore_client_abort on;
|
|
||||||
proxy_pass http://localhost:4000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
reload Nginx:
|
|
||||||
|
Change the `listen` directive to the following:
|
||||||
|
```nginx
|
||||||
|
listen 127.0.0.1:14447;
|
||||||
```
|
```
|
||||||
systemctl stop i2pd.service --no-block
|
|
||||||
systemctl start i2pd.service
|
Set `server_name` to your i2p address.
|
||||||
|
|
||||||
|
Reload Nginx:
|
||||||
|
```
|
||||||
|
systemctl restart i2pd.service --no-block
|
||||||
|
systemctl reload nginx.service
|
||||||
```
|
```
|
||||||
*Notice:* The stop command initiates a graceful shutdown process, i2pd stops after finishing to route transit tunnels (maximum 10 minutes).
|
*Notice:* The stop command initiates a graceful shutdown process, i2pd stops after finishing to route transit tunnels (maximum 10 minutes).
|
||||||
|
|
||||||
|
|
|
@ -74,56 +74,23 @@ config :pleroma, :http_security,
|
||||||
enabled: false
|
enabled: false
|
||||||
```
|
```
|
||||||
|
|
||||||
Use this as the Nginx config:
|
In the Nginx config, add the following into the `location /` block:
|
||||||
```
|
```nginx
|
||||||
proxy_cache_path /tmp/akkoma-media-cache levels=1:2 keys_zone=akkoma_media_cache:10m max_size=10g inactive=720m use_temp_path=off;
|
|
||||||
# The above already exists in a clearnet instance's config.
|
|
||||||
# If not, add it.
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 127.0.0.1:8099;
|
|
||||||
server_name youronionaddress;
|
|
||||||
|
|
||||||
# Comment to enable logs
|
|
||||||
access_log /dev/null;
|
|
||||||
error_log /dev/null;
|
|
||||||
|
|
||||||
gzip_vary on;
|
|
||||||
gzip_proxied any;
|
|
||||||
gzip_comp_level 6;
|
|
||||||
gzip_buffers 16 8k;
|
|
||||||
gzip_http_version 1.1;
|
|
||||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript application/activity+json application/atom+xml;
|
|
||||||
|
|
||||||
client_max_body_size 16m;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
|
|
||||||
add_header X-XSS-Protection "0";
|
add_header X-XSS-Protection "0";
|
||||||
add_header X-Permitted-Cross-Domain-Policies none;
|
add_header X-Permitted-Cross-Domain-Policies none;
|
||||||
add_header X-Frame-Options DENY;
|
add_header X-Frame-Options DENY;
|
||||||
add_header X-Content-Type-Options nosniff;
|
add_header X-Content-Type-Options nosniff;
|
||||||
add_header Referrer-Policy same-origin;
|
add_header Referrer-Policy same-origin;
|
||||||
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
|
|
||||||
proxy_pass http://localhost:4000;
|
|
||||||
|
|
||||||
client_max_body_size 16m;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /proxy {
|
|
||||||
proxy_cache akkoma_media_cache;
|
|
||||||
proxy_cache_lock on;
|
|
||||||
proxy_ignore_client_abort on;
|
|
||||||
proxy_pass http://localhost:4000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
reload Nginx:
|
|
||||||
|
Change the `listen` directive to the following:
|
||||||
|
```nginx
|
||||||
|
listen 127.0.0.1:8099;
|
||||||
|
```
|
||||||
|
|
||||||
|
Set the `server_name` to your onion address.
|
||||||
|
|
||||||
|
Reload Nginx:
|
||||||
```
|
```
|
||||||
systemctl reload nginx
|
systemctl reload nginx
|
||||||
```
|
```
|
||||||
|
|
|
@ -33,6 +33,7 @@ indexes faster when it can process many posts in a single batch.
|
||||||
> config :pleroma, Pleroma.Search.Meilisearch,
|
> config :pleroma, Pleroma.Search.Meilisearch,
|
||||||
> url: "http://127.0.0.1:7700/",
|
> url: "http://127.0.0.1:7700/",
|
||||||
> private_key: "private key",
|
> private_key: "private key",
|
||||||
|
> search_key: "search key",
|
||||||
> initial_indexing_chunk_size: 100_000
|
> initial_indexing_chunk_size: 100_000
|
||||||
|
|
||||||
Information about setting up meilisearch can be found in the
|
Information about setting up meilisearch can be found in the
|
||||||
|
@ -45,7 +46,7 @@ is hardly usable on a somewhat big instance.
|
||||||
### Private key authentication (optional)
|
### Private key authentication (optional)
|
||||||
|
|
||||||
To set the private key, use the `MEILI_MASTER_KEY` environment variable when starting. After setting the _master key_,
|
To set the private key, use the `MEILI_MASTER_KEY` environment variable when starting. After setting the _master key_,
|
||||||
you have to get the _private key_, which is actually used for authentication.
|
you have to get the _private key_ and possibly _search key_, which are actually used for authentication.
|
||||||
|
|
||||||
=== "OTP"
|
=== "OTP"
|
||||||
```sh
|
```sh
|
||||||
|
@ -57,7 +58,11 @@ you have to get the _private key_, which is actually used for authentication.
|
||||||
mix pleroma.search.meilisearch show-keys <your master key here>
|
mix pleroma.search.meilisearch show-keys <your master key here>
|
||||||
```
|
```
|
||||||
|
|
||||||
You will see a "Default Admin API Key", this is the key you actually put into your configuration file.
|
You will see a "Default Admin API Key", this is the key you actually put into
|
||||||
|
your configuration file as `private_key`. You should also see a
|
||||||
|
"Default Search API key", put this into your config as `search_key`.
|
||||||
|
If your version of Meilisearch only showed the former,
|
||||||
|
just leave `search_key` completely unset in Akkoma's config.
|
||||||
|
|
||||||
### Initial indexing
|
### Initial indexing
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
The following endpoints are additionally present into our actors.
|
The following endpoints are additionally present into our actors.
|
||||||
|
|
||||||
- `oauthRegistrationEndpoint` (`http://litepub.social/ns#oauthRegistrationEndpoint`)
|
- `oauthRegistrationEndpoint` (`http://litepub.social/ns#oauthRegistrationEndpoint`)
|
||||||
- `uploadMedia` (`https://www.w3.org/ns/activitystreams#uploadMedia`)
|
|
||||||
|
|
||||||
### oauthRegistrationEndpoint
|
### oauthRegistrationEndpoint
|
||||||
|
|
||||||
|
@ -12,6 +11,279 @@ Points to MastodonAPI `/api/v1/apps` for now.
|
||||||
|
|
||||||
See <https://docs.joinmastodon.org/methods/apps/>
|
See <https://docs.joinmastodon.org/methods/apps/>
|
||||||
|
|
||||||
|
## Emoji reactions
|
||||||
|
|
||||||
|
Emoji reactions are implemented as a new activity type `EmojiReact`.
|
||||||
|
A single user is allowed to react multiple times with different emoji to the
|
||||||
|
same post. However, they may only react at most once with the same emoji.
|
||||||
|
Repeated reaction from the same user with the same emoji are to be ignored.
|
||||||
|
Emoji reactions are also distinct from `Like` activities and a user may both
|
||||||
|
`Like` and react to a post.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
Misskey also supports emoji reactions, but the implementations differs.
|
||||||
|
It equates likes and reactions and only allows a single reaction per post.
|
||||||
|
|
||||||
|
The emoji is placed in the `content` field of the activity
|
||||||
|
and the `object` property points to the note reacting to.
|
||||||
|
|
||||||
|
Emoji can either be any Unicode emoji sequence or a custom emoji.
|
||||||
|
The latter must place their shortcode, including enclosing colons,
|
||||||
|
into `content` and put the emoji object inside the `tag` property.
|
||||||
|
The `tag` property MAY be omitted for Unicode emoji.
|
||||||
|
|
||||||
|
An example reaction with a Unicode emoji:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
"https://example.org/schemas/litepub-0.1.jsonld",
|
||||||
|
{
|
||||||
|
"@language": "und"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "EmojiReact",
|
||||||
|
"id": "https://example.org/activities/23143872a0346141",
|
||||||
|
"actor": "https://example.org/users/akko",
|
||||||
|
"nickname": "akko",
|
||||||
|
"to": ["https://remote.example/users/diana", "https://example.org/users/akko/followers"],
|
||||||
|
"cc": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
"content": "🧡",
|
||||||
|
"object": "https://remote.example/objects/9f0e93499d8314a9"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
An example reaction with a custom emoji:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
"https://example.org/schemas/litepub-0.1.jsonld",
|
||||||
|
{
|
||||||
|
"@language": "und"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "EmojiReact",
|
||||||
|
"id": "https://example.org/activities/d75586dec0541650",
|
||||||
|
"actor": "https://example.org/users/akko",
|
||||||
|
"nickname": "akko",
|
||||||
|
"to": ["https://remote.example/users/diana", "https://example.org/users/akko/followers"],
|
||||||
|
"cc": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
"content": ":mouse:",
|
||||||
|
"object": "https://remote.example/objects/9f0e93499d8314a9",
|
||||||
|
"tag": [{
|
||||||
|
"type": "Emoji",
|
||||||
|
"id": null,
|
||||||
|
"name": "mouse",
|
||||||
|
"icon": {
|
||||||
|
"type": "Image",
|
||||||
|
"url": "https://example.org/emoji/mouse/mouse.png"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
Although an emoji reaction can only contain a single emoji,
|
||||||
|
for compatibility with older versions of Pleroma and Akkoma,
|
||||||
|
it is recommended to wrap the emoji object in a single-element array.
|
||||||
|
|
||||||
|
When reacting with a remote custom emoji do not include the remote domain in `content`’s shortcode
|
||||||
|
*(unlike in our REST API which needs the domain)*:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
"https://example.org/schemas/litepub-0.1.jsonld",
|
||||||
|
{
|
||||||
|
"@language": "und"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "EmojiReact",
|
||||||
|
"id": "https://example.org/activities/7993dcae98d8d5ec",
|
||||||
|
"actor": "https://example.org/users/akko",
|
||||||
|
"nickname": "akko",
|
||||||
|
"to": ["https://remote.example/users/diana", "https://example.org/users/akko/followers"],
|
||||||
|
"cc": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
"content": ":hug:",
|
||||||
|
"object": "https://remote.example/objects/9f0e93499d8314a9",
|
||||||
|
"tag": [{
|
||||||
|
"type": "Emoji",
|
||||||
|
"id": "https://other.example/emojis/hug",
|
||||||
|
"name": "hug",
|
||||||
|
"icon": {
|
||||||
|
"type": "Image",
|
||||||
|
"url": "https://other.example/files/b71cea432b3fad67.webp"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Emoji reactions can be retracted using a standard `Undo` activity:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
"http://example.org/schemas/litepub-0.1.jsonld",
|
||||||
|
{
|
||||||
|
"@language": "und"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Undo",
|
||||||
|
"id": "http://example.org/activities/4685792e-efb6-4309-b508-ae4f355dd695",
|
||||||
|
"actor": "https://example.org/users/akko",
|
||||||
|
"to": ["https://remote.example/users/diana", "https://example.org/users/akko/followers"],
|
||||||
|
"cc": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
"object": "https://example.org/activities/23143872a0346141"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## User profile backgrounds
|
||||||
|
|
||||||
|
Akkoma federates user profile backgrounds the same way as Sharkey.
|
||||||
|
|
||||||
|
An actors ActivityPub representation contains an additional
|
||||||
|
`backgroundUrl` property containing an `Image` object. This property
|
||||||
|
belongs to the `"sharkey": "https://joinsharkey.org/ns#"` namespace.
|
||||||
|
|
||||||
|
## Quote Posts
|
||||||
|
|
||||||
|
Akkoma allows referencing a single other note as a quote,
|
||||||
|
which will be prominently displayed in the interface.
|
||||||
|
|
||||||
|
The quoted post is referenced by its ActivityPub id in the `quoteUri` property.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
Old Misskey only understood and modern Misskey still prefers
|
||||||
|
the `_misskey_quote` property for this. Similar some other older
|
||||||
|
software used `quoteUrl` or `quoteURL`.
|
||||||
|
All current implementations with quote support understand `quoteUri`.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
"https://example.org/schemas/litepub-0.1.jsonld",
|
||||||
|
{
|
||||||
|
"@language": "und"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Note",
|
||||||
|
"id": "https://example.org/activities/85717e587f95d5c0",
|
||||||
|
"actor": "https://example.org/users/akko",
|
||||||
|
"to": ["https://remote.example/users/diana", "https://example.org/users/akko/followers"],
|
||||||
|
"cc": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
"context": "https://example.org/contexts/1",
|
||||||
|
"content": "Look at that!",
|
||||||
|
"quoteUri": "http://remote.example/status/85717e587f95d5c0",
|
||||||
|
"contentMap": {
|
||||||
|
"en": "Look at that!"
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"content": "Look at that!",
|
||||||
|
"mediaType": "text/plain"
|
||||||
|
},
|
||||||
|
"published": "2024-04-06T23:40:28Z",
|
||||||
|
"updated": "2024-04-06T23:40:28Z",
|
||||||
|
"attachemnt": [],
|
||||||
|
"tag": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Threads
|
||||||
|
|
||||||
|
Akkoma assigns all posts of the same thread the same `context`. This is a
|
||||||
|
standard ActivityPub property but its meaning is left vague. Akkoma will
|
||||||
|
always treat posts with identical `context` as part of the same thread.
|
||||||
|
|
||||||
|
`context` must not be assumed to hold any meaning or be dereferencable.
|
||||||
|
|
||||||
|
Incoming posts without `context` will be assigned a new context.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
Mastodon uses the non-standard `conversation` property for the same purpose
|
||||||
|
*(named after an older OStatus property)*. For incoming posts without
|
||||||
|
`context` but with `converstions` Akkoma will use the value from
|
||||||
|
`conversations` to fill in `context`.
|
||||||
|
For outgoing posts Akkoma will duplicate the context into `conversation`.
|
||||||
|
|
||||||
|
## Post Source
|
||||||
|
|
||||||
|
Unlike Mastodon, Akkoma supports drafting posts in multiple source formats
|
||||||
|
besides plaintext, like Markdown or MFM. The original input is preserved
|
||||||
|
in the standard ActivityPub `source` property *(not supported by Mastodon)*.
|
||||||
|
Still, `content` will always be present and contain the prerendered HTML form.
|
||||||
|
|
||||||
|
Supported `mediaType` include:
|
||||||
|
- `text/plain`
|
||||||
|
- `text/markdown`
|
||||||
|
- `text/bbcode`
|
||||||
|
- `text/x.misskeymarkdown`
|
||||||
|
|
||||||
|
## Post Language
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
This is also supported in and compatible with Mastodon, but since
|
||||||
|
joinmastodon.org doesn’t document it yet it is included here.
|
||||||
|
[GoToSocial](https://docs.gotosocial.org/en/latest/federation/federating_with_gotosocial/#content-contentmap-and-language)
|
||||||
|
has a more refined version of this which can correctly deal with multiple language entries.
|
||||||
|
|
||||||
|
A post can indicate its language by including a `contentMap` object
|
||||||
|
which contains a sub key named after the language’s ISO 639-1 code
|
||||||
|
and it’s content identical to the post’s `content` field.
|
||||||
|
|
||||||
|
Currently Akkoma, just like Mastodon, only properly supports a single language entry,
|
||||||
|
in case of multiple entries a random language will be picked.
|
||||||
|
Furthermore, Akkoma currently only reads the `content` field
|
||||||
|
and never the value from `contentMap`.
|
||||||
|
|
||||||
|
## Local post scope
|
||||||
|
|
||||||
|
Post using this scope will never federate to other servers
|
||||||
|
but for the sake of completeness it is listed here.
|
||||||
|
|
||||||
|
In addition to the usual scopes *(public, unlisted, followers-only, direct)*
|
||||||
|
Akkoma supports an “unlisted” post scope. Such posts will not federate to
|
||||||
|
other instances and only be shown to logged-in users on the same instance.
|
||||||
|
It is included into the local timeline.
|
||||||
|
This may be useful to discuss or announce instance-specific policies and topics.
|
||||||
|
|
||||||
|
A post is addressed to the local scope by including `<base url of instance>/#Public`
|
||||||
|
in its `to` field. E.g. if the instance is on `https://example.org` it would use
|
||||||
|
`https://example.org/#Public`.
|
||||||
|
|
||||||
|
An implementation creating a new post MUST NOT address both the local and
|
||||||
|
general public scope `as:Public` at the same time. A post addressing the local
|
||||||
|
scope MUST NOT be sent to other instances or be possible to fetch by other
|
||||||
|
instances regardless of potential other listed addressees.
|
||||||
|
|
||||||
|
When receiving a remote post addressing both the public scope and what appears
|
||||||
|
to be a local-scope identifier, the post SHOULD be treated without assigning any
|
||||||
|
special meaning to the potential local-scope identifier.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
Misskey-derivatives have a similar concept of non-federated posts,
|
||||||
|
however those are also shown publicly on the local web interface
|
||||||
|
and are thus visible to non-members.
|
||||||
|
|
||||||
|
## List post scope
|
||||||
|
|
||||||
|
Messages originally addressed to a custom list will contain
|
||||||
|
a `listMessage` field with an unresolvable pseudo ActivityPub id.
|
||||||
|
|
||||||
|
# Deprecated and Removed Extensions
|
||||||
|
|
||||||
|
The following extensions were used in the past but have been dropped.
|
||||||
|
Documentation is retained here as a reference and since old objects might
|
||||||
|
still contains related fields.
|
||||||
|
|
||||||
|
## Actor endpoints
|
||||||
|
|
||||||
|
The following endpoints used to be present:
|
||||||
|
|
||||||
|
- `uploadMedia` (`https://www.w3.org/ns/activitystreams#uploadMedia`)
|
||||||
|
|
||||||
### uploadMedia
|
### uploadMedia
|
||||||
|
|
||||||
Inspired by <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>, it is part of the ActivityStreams namespace because it used to be part of the ActivityPub specification and got removed from it.
|
Inspired by <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>, it is part of the ActivityStreams namespace because it used to be part of the ActivityPub specification and got removed from it.
|
||||||
|
@ -20,9 +292,8 @@ Content-Type: multipart/form-data
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
- (required) `file`: The file being uploaded
|
- (required) `file`: The file being uploaded
|
||||||
- (optionnal) `description`: A plain-text description of the media, for accessibility purposes.
|
- (optional) `description`: A plain-text description of the media, for accessibility purposes.
|
||||||
|
|
||||||
Response: HTTP 201 Created with the object into the body, no `Location` header provided as it doesn't have an `id`
|
Response: HTTP 201 Created with the object into the body, no `Location` header provided as it doesn't have an `id`
|
||||||
|
|
||||||
The object given in the reponse should then be inserted into an Object's `attachment` field.
|
The object given in the response should then be inserted into an Object's `attachment` field.
|
||||||
|
|
||||||
|
|
141
docs/docs/development/nodeinfo_extensions.md
Normal file
141
docs/docs/development/nodeinfo_extensions.md
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
# Nodeinfo Extensions
|
||||||
|
|
||||||
|
Akkoma currently implements version 2.0 and 2.1 of nodeinfo spec,
|
||||||
|
but provides the following additional fields.
|
||||||
|
|
||||||
|
## metadata
|
||||||
|
|
||||||
|
The spec leaves the content of `metadata` up to implementations
|
||||||
|
and indeed Akkoma adds many fields here apart from the commonly
|
||||||
|
found `nodeName` and `nodeDescription` fields.
|
||||||
|
|
||||||
|
### accountActivationRequired
|
||||||
|
Whether or not users need to confirm their email before completing registration.
|
||||||
|
*(boolean)*
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
Not to be confused with account approval, where each registration needs to
|
||||||
|
be manually approved by an admin. Account approval has no nodeinfo entry.
|
||||||
|
|
||||||
|
### features
|
||||||
|
|
||||||
|
Array of strings denoting supported server features. E.g. a server supporting
|
||||||
|
quote posts should include a `"quote_posting"` entry here.
|
||||||
|
|
||||||
|
A non-exhaustive list of possible features:
|
||||||
|
- `polls`
|
||||||
|
- `quote_posting`
|
||||||
|
- `editing`
|
||||||
|
- `bubble_timeline`
|
||||||
|
- `pleroma_emoji_reactions` *(Unicode emoji)*
|
||||||
|
- `custom_emoji_reactions`
|
||||||
|
- `akkoma_api`
|
||||||
|
- `akkoma:machine_translation`
|
||||||
|
- `mastodon_api`
|
||||||
|
- `pleroma_api`
|
||||||
|
|
||||||
|
### federatedTimelineAvailable
|
||||||
|
Whether or not the “federated timeline”, i.e. a timeline containing posts from
|
||||||
|
the entire known network, is made available.
|
||||||
|
*(boolean)*
|
||||||
|
|
||||||
|
### federation
|
||||||
|
This section is optional and can contain various custom keys describing federation policies.
|
||||||
|
The following are required to be presented:
|
||||||
|
- `enabled` *(boolean)* whether the server federates at all
|
||||||
|
|
||||||
|
A non-exhaustive list of optional keys:
|
||||||
|
- `exclusions` *(boolean)* whether some federation policies are withheld
|
||||||
|
- `mrf_simple` *(object)* describes how the Simple MRF policy is configured
|
||||||
|
|
||||||
|
### fieldsLimits
|
||||||
|
A JSON object documenting restriction for user account info fields.
|
||||||
|
All properties are integers.
|
||||||
|
|
||||||
|
- `maxFields` maximum number of account info fields local users can create
|
||||||
|
- `maxRemoteFields` maximum number of account info fields remote users can have
|
||||||
|
before the user gets rejected or fields truncated
|
||||||
|
- `nameLength` maximum length of a field’s name
|
||||||
|
- `valueLength` maximum length of a field’s value
|
||||||
|
|
||||||
|
### invitesEnabled
|
||||||
|
Whether or not signing up via invite codes is possible.
|
||||||
|
*(boolean)*
|
||||||
|
|
||||||
|
### localBubbleInstances
|
||||||
|
Array of domains (as strings) of other instances chosen
|
||||||
|
by the admin which are shown in the bubble timeline.
|
||||||
|
|
||||||
|
### mailerEnabled
|
||||||
|
Whether or not the instance can send out emails.
|
||||||
|
*(boolean)*
|
||||||
|
|
||||||
|
### nodeDescription
|
||||||
|
Human-friendly description of this instance
|
||||||
|
*(string)*
|
||||||
|
|
||||||
|
### nodeName
|
||||||
|
Human-friendly name of this instance
|
||||||
|
*(string)*
|
||||||
|
|
||||||
|
### pollLimits
|
||||||
|
JSON object containing limits for polls created by local users.
|
||||||
|
All values are integers.
|
||||||
|
- `max_options` maximum number of poll options
|
||||||
|
- `max_option_chars` maximum characters per poll option
|
||||||
|
- `min_expiration` minimum time in seconds a poll must be open for
|
||||||
|
- `max_expiration` maximum time a poll is allowed to be open for
|
||||||
|
|
||||||
|
### postFormats
|
||||||
|
Array of strings containing media types for supported post source formats.
|
||||||
|
A non-exhaustive list of possible values:
|
||||||
|
- `text/plain`
|
||||||
|
- `text/markdown`
|
||||||
|
- `text/bbcode`
|
||||||
|
- `text/x.misskeymarkdown`
|
||||||
|
|
||||||
|
### private
|
||||||
|
Whether or not unauthenticated API access is permitted.
|
||||||
|
*(boolean)*
|
||||||
|
|
||||||
|
### privilegedStaff
|
||||||
|
Whether or not moderators are trusted to perform some
|
||||||
|
additional tasks like e.g. issuing password reset emails.
|
||||||
|
|
||||||
|
### publicTimelineVisibility
|
||||||
|
JSON object containing boolean-valued keys reporting
|
||||||
|
if a given timeline can be viewed without login.
|
||||||
|
- `local`
|
||||||
|
- `federated`
|
||||||
|
- `bubble`
|
||||||
|
|
||||||
|
### restrictedNicknames
|
||||||
|
Array of strings listing nicknames forbidden to be used during signup.
|
||||||
|
|
||||||
|
### skipThreadContainment
|
||||||
|
Whether broken threads are filtered out
|
||||||
|
*(boolean)*
|
||||||
|
|
||||||
|
### staffAccounts
|
||||||
|
Array containing ActivityPub IDs of local accounts
|
||||||
|
with some form of elevated privilege on the instance.
|
||||||
|
|
||||||
|
### suggestions
|
||||||
|
JSON object containing info on whether the interaction-based
|
||||||
|
Mastodon `/api/v1/suggestions` feature is enabled and optionally
|
||||||
|
additional implementation-defined fields with more details
|
||||||
|
on e.g. how suggested users are selected.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
This has no relation to the newer /api/v2/suggestions API
|
||||||
|
which also (or exclusively) contains staff-curated entries.
|
||||||
|
|
||||||
|
- `enabled` *(boolean)* whether or not user recommendations are enabled
|
||||||
|
|
||||||
|
### uploadLimits
|
||||||
|
JSON object documenting various upload-related size limits.
|
||||||
|
All values are integers and in bytes.
|
||||||
|
- `avatar` maximum size of uploaded user avatars
|
||||||
|
- `banner` maximum size of uploaded user profile banners
|
||||||
|
- `background` maximum size of uploaded user profile backgrounds
|
||||||
|
- `general` maximum size for all other kinds of uploads
|
|
@ -60,7 +60,7 @@ ServerTokens Prod
|
||||||
Include /etc/letsencrypt/options-ssl-apache.conf
|
Include /etc/letsencrypt/options-ssl-apache.conf
|
||||||
|
|
||||||
# Uncomment the following to enable MediaProxy caching on disk
|
# Uncomment the following to enable MediaProxy caching on disk
|
||||||
#CacheRoot /tmp/akkoma-media-cache/
|
#CacheRoot /var/tmp/akkoma-media-cache/
|
||||||
#CacheDirLevels 1
|
#CacheDirLevels 1
|
||||||
#CacheDirLength 2
|
#CacheDirLength 2
|
||||||
#CacheEnable disk /proxy
|
#CacheEnable disk /proxy
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
SCRIPTNAME=${0##*/}
|
SCRIPTNAME=${0##*/}
|
||||||
|
|
||||||
# mod_disk_cache directory
|
# mod_disk_cache directory
|
||||||
CACHE_DIRECTORY="/tmp/akkoma-media-cache"
|
CACHE_DIRECTORY="/var/tmp/akkoma-media-cache"
|
||||||
|
|
||||||
## Removes an item via the htcacheclean utility
|
## Removes an item via the htcacheclean utility
|
||||||
## $1 - the filename, can be a pattern .
|
## $1 - the filename, can be a pattern .
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
# See the documentation at docs.akkoma.dev for your particular distro/OS for
|
# See the documentation at docs.akkoma.dev for your particular distro/OS for
|
||||||
# installation instructions.
|
# installation instructions.
|
||||||
|
|
||||||
proxy_cache_path /tmp/akkoma-media-cache levels=1:2 keys_zone=akkoma_media_cache:10m max_size=1g
|
proxy_cache_path /var/tmp/akkoma-media-cache levels=1:2 keys_zone=akkoma_media_cache:10m max_size=1g
|
||||||
inactive=720m use_temp_path=off;
|
inactive=720m use_temp_path=off;
|
||||||
|
|
||||||
# this is explicitly IPv4 since Pleroma.Web.Endpoint binds on IPv4 only
|
# this is explicitly IPv4 since Pleroma.Web.Endpoint binds on IPv4 only
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
SCRIPTNAME=${0##*/}
|
SCRIPTNAME=${0##*/}
|
||||||
|
|
||||||
# NGINX cache directory
|
# NGINX cache directory
|
||||||
CACHE_DIRECTORY="/tmp/akkoma-media-cache"
|
CACHE_DIRECTORY="/var/tmp/akkoma-media-cache"
|
||||||
|
|
||||||
## Return the files where the items are cached.
|
## Return the files where the items are cached.
|
||||||
## $1 - the filename, can be a pattern .
|
## $1 - the filename, can be a pattern .
|
||||||
|
|
|
@ -16,7 +16,7 @@ defmodule Mix.Pleroma do
|
||||||
:fast_html,
|
:fast_html,
|
||||||
:oban
|
:oban
|
||||||
]
|
]
|
||||||
@cachex_children ["object", "user", "scrubber", "web_resp"]
|
@cachex_children ["object", "user", "scrubber", "web_resp", "http_backoff"]
|
||||||
@doc "Common functions to be reused in mix tasks"
|
@doc "Common functions to be reused in mix tasks"
|
||||||
def start_pleroma do
|
def start_pleroma do
|
||||||
Pleroma.Config.Holder.save_default()
|
Pleroma.Config.Holder.save_default()
|
||||||
|
|
|
@ -17,6 +17,13 @@ defmodule Mix.Tasks.Pleroma.Diagnostics do
|
||||||
|> IO.inspect()
|
|> IO.inspect()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def run(["fetch_object", url]) do
|
||||||
|
start_pleroma()
|
||||||
|
|
||||||
|
Pleroma.Object.Fetcher.fetch_object_from_id(url)
|
||||||
|
|> IO.inspect()
|
||||||
|
end
|
||||||
|
|
||||||
def run(["home_timeline", nickname]) do
|
def run(["home_timeline", nickname]) do
|
||||||
start_pleroma()
|
start_pleroma()
|
||||||
user = Repo.get_by!(User, nickname: nickname)
|
user = Repo.get_by!(User, nickname: nickname)
|
||||||
|
|
|
@ -126,8 +126,12 @@ defmodule Mix.Tasks.Pleroma.Search.Meilisearch do
|
||||||
decoded = Jason.decode!(result.body)
|
decoded = Jason.decode!(result.body)
|
||||||
|
|
||||||
if decoded["results"] do
|
if decoded["results"] do
|
||||||
Enum.each(decoded["results"], fn %{"description" => desc, "key" => key} ->
|
Enum.each(decoded["results"], fn
|
||||||
IO.puts("#{desc}: #{key}")
|
%{"name" => name, "key" => key} ->
|
||||||
|
IO.puts("#{name}: #{key}")
|
||||||
|
|
||||||
|
%{"description" => desc, "key" => key} ->
|
||||||
|
IO.puts("#{desc}: #{key}")
|
||||||
end)
|
end)
|
||||||
else
|
else
|
||||||
IO.puts("Error fetching the keys, check the master key is correct: #{inspect(decoded)}")
|
IO.puts("Error fetching the keys, check the master key is correct: #{inspect(decoded)}")
|
||||||
|
|
|
@ -258,6 +258,27 @@ defmodule Pleroma.Activity do
|
||||||
|
|
||||||
def get_create_by_object_ap_id(_), do: nil
|
def get_create_by_object_ap_id(_), do: nil
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Accepts a list of `ap__id`.
|
||||||
|
Returns a query yielding Create activities for the given objects,
|
||||||
|
in the same order as they were specified in the input list.
|
||||||
|
"""
|
||||||
|
@spec get_presorted_create_by_object_ap_id([String.t()]) :: Ecto.Queryable.t()
|
||||||
|
def get_presorted_create_by_object_ap_id(ap_ids) do
|
||||||
|
from(
|
||||||
|
a in Activity,
|
||||||
|
join:
|
||||||
|
ids in fragment(
|
||||||
|
"SELECT * FROM UNNEST(?::text[]) WITH ORDINALITY AS ids(ap_id, ord)",
|
||||||
|
^ap_ids
|
||||||
|
),
|
||||||
|
on:
|
||||||
|
ids.ap_id == fragment("?->>'object'", a.data) and
|
||||||
|
fragment("?->>'type'", a.data) == "Create",
|
||||||
|
order_by: [asc: ids.ord]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Accepts `ap_id` or list of `ap_id`.
|
Accepts `ap_id` or list of `ap_id`.
|
||||||
Returns a query.
|
Returns a query.
|
||||||
|
|
|
@ -179,7 +179,9 @@ defmodule Pleroma.Application do
|
||||||
build_cachex("translations", default_ttl: :timer.hours(24 * 30), limit: 2500),
|
build_cachex("translations", default_ttl: :timer.hours(24 * 30), limit: 2500),
|
||||||
build_cachex("instances", default_ttl: :timer.hours(24), ttl_interval: 1000, limit: 2500),
|
build_cachex("instances", default_ttl: :timer.hours(24), ttl_interval: 1000, limit: 2500),
|
||||||
build_cachex("request_signatures", default_ttl: :timer.hours(24 * 30), limit: 3000),
|
build_cachex("request_signatures", default_ttl: :timer.hours(24 * 30), limit: 3000),
|
||||||
build_cachex("rel_me", default_ttl: :timer.hours(24 * 30), limit: 300)
|
build_cachex("rel_me", default_ttl: :timer.hours(24 * 30), limit: 300),
|
||||||
|
build_cachex("host_meta", default_ttl: :timer.minutes(120), limit: 5000),
|
||||||
|
build_cachex("http_backoff", default_ttl: :timer.hours(24 * 30), limit: 10000)
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -64,4 +64,7 @@ defmodule Pleroma.Constants do
|
||||||
"Service"
|
"Service"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Internally used as top-level types for media attachments and user images
|
||||||
|
const(attachment_types, do: ["Document", "Image"])
|
||||||
end
|
end
|
||||||
|
|
121
lib/pleroma/http/backoff.ex
Normal file
121
lib/pleroma/http/backoff.ex
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
defmodule Pleroma.HTTP.Backoff do
|
||||||
|
alias Pleroma.HTTP
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
|
||||||
|
@backoff_cache :http_backoff_cache
|
||||||
|
|
||||||
|
# attempt to parse a timestamp from a header
|
||||||
|
# returns nil if it can't parse the timestamp
|
||||||
|
@spec timestamp_or_nil(binary) :: DateTime.t() | nil
|
||||||
|
defp timestamp_or_nil(header) do
|
||||||
|
case DateTime.from_iso8601(header) do
|
||||||
|
{:ok, stamp, _} ->
|
||||||
|
stamp
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# attempt to parse the x-ratelimit-reset header from the headers
|
||||||
|
@spec x_ratelimit_reset(headers :: list) :: DateTime.t() | nil
|
||||||
|
defp x_ratelimit_reset(headers) do
|
||||||
|
with {_header, value} <- List.keyfind(headers, "x-ratelimit-reset", 0),
|
||||||
|
true <- is_binary(value) do
|
||||||
|
timestamp_or_nil(value)
|
||||||
|
else
|
||||||
|
_ ->
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# attempt to parse the Retry-After header from the headers
|
||||||
|
# this can be either a timestamp _or_ a number of seconds to wait!
|
||||||
|
# we'll return a datetime if we can parse it, or nil if we can't
|
||||||
|
@spec retry_after(headers :: list) :: DateTime.t() | nil
|
||||||
|
defp retry_after(headers) do
|
||||||
|
with {_header, value} <- List.keyfind(headers, "retry-after", 0),
|
||||||
|
true <- is_binary(value) do
|
||||||
|
# first, see if it's an integer
|
||||||
|
case Integer.parse(value) do
|
||||||
|
{seconds, ""} ->
|
||||||
|
Logger.debug("Parsed Retry-After header: #{seconds} seconds")
|
||||||
|
DateTime.utc_now() |> Timex.shift(seconds: seconds)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
# if it's not an integer, try to parse it as a timestamp
|
||||||
|
timestamp_or_nil(value)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
_ ->
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# given a set of headers, will attempt to find the next backoff timestamp
|
||||||
|
# if it can't find one, it will default to 5 minutes from now
|
||||||
|
@spec next_backoff_timestamp(%{headers: list}) :: DateTime.t()
|
||||||
|
defp next_backoff_timestamp(%{headers: headers}) when is_list(headers) do
|
||||||
|
default_5_minute_backoff =
|
||||||
|
DateTime.utc_now()
|
||||||
|
|> Timex.shift(seconds: 5 * 60)
|
||||||
|
|
||||||
|
backoff =
|
||||||
|
[&x_ratelimit_reset/1, &retry_after/1]
|
||||||
|
|> Enum.map(& &1.(headers))
|
||||||
|
|> Enum.find(&(&1 != nil))
|
||||||
|
|
||||||
|
if is_nil(backoff) do
|
||||||
|
Logger.debug("No backoff headers found, defaulting to 5 minutes from now")
|
||||||
|
default_5_minute_backoff
|
||||||
|
else
|
||||||
|
Logger.debug("Found backoff header, will back off until: #{backoff}")
|
||||||
|
backoff
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp next_backoff_timestamp(_), do: DateTime.utc_now() |> Timex.shift(seconds: 5 * 60)
|
||||||
|
|
||||||
|
# utility function to check the HTTP response for potential backoff headers
|
||||||
|
# will check if we get a 429 or 503 response, and if we do, will back off for a bit
|
||||||
|
@spec check_backoff({:ok | :error, HTTP.Env.t()}, binary()) ::
|
||||||
|
{:ok | :error, HTTP.Env.t()} | {:error, :ratelimit}
|
||||||
|
defp check_backoff({:ok, env}, host) do
|
||||||
|
case env.status do
|
||||||
|
status when status in [429, 503] ->
|
||||||
|
Logger.error("Rate limited on #{host}! Backing off...")
|
||||||
|
timestamp = next_backoff_timestamp(env)
|
||||||
|
ttl = Timex.diff(timestamp, DateTime.utc_now(), :seconds)
|
||||||
|
# we will cache the host for 5 minutes
|
||||||
|
@cachex.put(@backoff_cache, host, true, ttl: ttl)
|
||||||
|
{:error, :ratelimit}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{:ok, env}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp check_backoff(env, _), do: env
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
this acts as a single throughput for all GET requests
|
||||||
|
we will check if the host is in the cache, and if it is, we will automatically fail the request
|
||||||
|
this ensures that we don't hammer the server with requests, and instead wait for the backoff to expire
|
||||||
|
this is a very simple implementation, and can be improved upon!
|
||||||
|
"""
|
||||||
|
@spec get(binary, list, list) :: {:ok | :error, HTTP.Env.t()} | {:error, :ratelimit}
|
||||||
|
def get(url, headers \\ [], options \\ []) do
|
||||||
|
%{host: host} = URI.parse(url)
|
||||||
|
|
||||||
|
case @cachex.get(@backoff_cache, host) do
|
||||||
|
{:ok, nil} ->
|
||||||
|
url
|
||||||
|
|> HTTP.get(headers, options)
|
||||||
|
|> check_backoff(host)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{:error, :ratelimit}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -354,7 +354,7 @@ defmodule Pleroma.Object.Fetcher do
|
||||||
|
|
||||||
with {:ok, %{body: body, status: code, headers: headers, url: final_url}}
|
with {:ok, %{body: body, status: code, headers: headers, url: final_url}}
|
||||||
when code in 200..299 <-
|
when code in 200..299 <-
|
||||||
HTTP.get(id, headers),
|
HTTP.Backoff.get(id, headers),
|
||||||
remote_host <-
|
remote_host <-
|
||||||
URI.parse(final_url).host,
|
URI.parse(final_url).host,
|
||||||
{:cross_domain_redirect, false} <-
|
{:cross_domain_redirect, false} <-
|
||||||
|
|
|
@ -28,7 +28,7 @@ defmodule Pleroma.ScheduledActivity do
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
||||||
def changeset(%ScheduledActivity{} = scheduled_activity, attrs) do
|
defp changeset(%ScheduledActivity{} = scheduled_activity, attrs) do
|
||||||
scheduled_activity
|
scheduled_activity
|
||||||
|> cast(attrs, [:scheduled_at, :params])
|
|> cast(attrs, [:scheduled_at, :params])
|
||||||
|> validate_required([:scheduled_at, :params])
|
|> validate_required([:scheduled_at, :params])
|
||||||
|
@ -40,26 +40,36 @@ defmodule Pleroma.ScheduledActivity do
|
||||||
%{changes: %{params: %{"media_ids" => media_ids} = params}} = changeset
|
%{changes: %{params: %{"media_ids" => media_ids} = params}} = changeset
|
||||||
)
|
)
|
||||||
when is_list(media_ids) do
|
when is_list(media_ids) do
|
||||||
media_attachments = Utils.attachments_from_ids(%{media_ids: media_ids})
|
user = User.get_by_id(changeset.data.user_id)
|
||||||
|
|
||||||
params =
|
case Utils.attachments_from_ids(user, %{media_ids: media_ids}) do
|
||||||
params
|
media_attachments when is_list(media_attachments) ->
|
||||||
|> Map.put("media_attachments", media_attachments)
|
params =
|
||||||
|> Map.put("media_ids", media_ids)
|
params
|
||||||
|
|> Map.put("media_attachments", media_attachments)
|
||||||
|
|> Map.put("media_ids", media_ids)
|
||||||
|
|
||||||
put_change(changeset, :params, params)
|
put_change(changeset, :params, params)
|
||||||
|
|
||||||
|
{:error, _} = e ->
|
||||||
|
e
|
||||||
|
|
||||||
|
e ->
|
||||||
|
{:error, e}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp with_media_attachments(changeset), do: changeset
|
defp with_media_attachments(changeset), do: changeset
|
||||||
|
|
||||||
def update_changeset(%ScheduledActivity{} = scheduled_activity, attrs) do
|
defp update_changeset(%ScheduledActivity{} = scheduled_activity, attrs) do
|
||||||
|
# note: should this ever allow swapping media attachments, make sure ownership is checked
|
||||||
scheduled_activity
|
scheduled_activity
|
||||||
|> cast(attrs, [:scheduled_at])
|
|> cast(attrs, [:scheduled_at])
|
||||||
|> validate_required([:scheduled_at])
|
|> validate_required([:scheduled_at])
|
||||||
|> validate_scheduled_at()
|
|> validate_scheduled_at()
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_scheduled_at(changeset) do
|
defp validate_scheduled_at(changeset) do
|
||||||
validate_change(changeset, :scheduled_at, fn _, scheduled_at ->
|
validate_change(changeset, :scheduled_at, fn _, scheduled_at ->
|
||||||
cond do
|
cond do
|
||||||
not far_enough?(scheduled_at) ->
|
not far_enough?(scheduled_at) ->
|
||||||
|
@ -77,7 +87,7 @@ defmodule Pleroma.ScheduledActivity do
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
def exceeds_daily_user_limit?(user_id, scheduled_at) do
|
defp exceeds_daily_user_limit?(user_id, scheduled_at) do
|
||||||
ScheduledActivity
|
ScheduledActivity
|
||||||
|> where(user_id: ^user_id)
|
|> where(user_id: ^user_id)
|
||||||
|> where([sa], type(sa.scheduled_at, :date) == type(^scheduled_at, :date))
|
|> where([sa], type(sa.scheduled_at, :date) == type(^scheduled_at, :date))
|
||||||
|
@ -86,7 +96,7 @@ defmodule Pleroma.ScheduledActivity do
|
||||||
|> Kernel.>=(Config.get([ScheduledActivity, :daily_user_limit]))
|
|> Kernel.>=(Config.get([ScheduledActivity, :daily_user_limit]))
|
||||||
end
|
end
|
||||||
|
|
||||||
def exceeds_total_user_limit?(user_id) do
|
defp exceeds_total_user_limit?(user_id) do
|
||||||
ScheduledActivity
|
ScheduledActivity
|
||||||
|> where(user_id: ^user_id)
|
|> where(user_id: ^user_id)
|
||||||
|> select([sa], count(sa.id))
|
|> select([sa], count(sa.id))
|
||||||
|
@ -108,20 +118,29 @@ defmodule Pleroma.ScheduledActivity do
|
||||||
diff > @min_offset
|
diff > @min_offset
|
||||||
end
|
end
|
||||||
|
|
||||||
def new(%User{} = user, attrs) do
|
defp new(%User{} = user, attrs) do
|
||||||
changeset(%ScheduledActivity{user_id: user.id}, attrs)
|
changeset(%ScheduledActivity{user_id: user.id}, attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Creates ScheduledActivity and add to queue to perform at scheduled_at date
|
Creates ScheduledActivity and add to queue to perform at scheduled_at date
|
||||||
"""
|
"""
|
||||||
@spec create(User.t(), map()) :: {:ok, ScheduledActivity.t()} | {:error, Ecto.Changeset.t()}
|
@spec create(User.t(), map()) :: {:ok, ScheduledActivity.t()} | {:error, any()}
|
||||||
def create(%User{} = user, attrs) do
|
def create(%User{} = user, attrs) do
|
||||||
Multi.new()
|
case new(user, attrs) do
|
||||||
|> Multi.insert(:scheduled_activity, new(user, attrs))
|
%Ecto.Changeset{} = sched_data ->
|
||||||
|> maybe_add_jobs(Config.get([ScheduledActivity, :enabled]))
|
Multi.new()
|
||||||
|> Repo.transaction()
|
|> Multi.insert(:scheduled_activity, sched_data)
|
||||||
|> transaction_response
|
|> maybe_add_jobs(Config.get([ScheduledActivity, :enabled]))
|
||||||
|
|> Repo.transaction()
|
||||||
|
|> transaction_response
|
||||||
|
|
||||||
|
{:error, _} = e ->
|
||||||
|
e
|
||||||
|
|
||||||
|
e ->
|
||||||
|
{:error, e}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_add_jobs(multi, true) do
|
defp maybe_add_jobs(multi, true) do
|
||||||
|
@ -187,17 +206,7 @@ defmodule Pleroma.ScheduledActivity do
|
||||||
|> where(user_id: ^user.id)
|
|> where(user_id: ^user.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def due_activities(offset \\ 0) do
|
defp job_query(scheduled_activity_id) do
|
||||||
naive_datetime =
|
|
||||||
NaiveDateTime.utc_now()
|
|
||||||
|> NaiveDateTime.add(offset, :millisecond)
|
|
||||||
|
|
||||||
ScheduledActivity
|
|
||||||
|> where([sa], sa.scheduled_at < ^naive_datetime)
|
|
||||||
|> Repo.all()
|
|
||||||
end
|
|
||||||
|
|
||||||
def job_query(scheduled_activity_id) do
|
|
||||||
from(j in Oban.Job,
|
from(j in Oban.Job,
|
||||||
where: j.queue == "scheduled_activities",
|
where: j.queue == "scheduled_activities",
|
||||||
where: fragment("args ->> 'activity_id' = ?::text", ^to_string(scheduled_activity_id))
|
where: fragment("args ->> 'activity_id' = ?::text", ^to_string(scheduled_activity_id))
|
||||||
|
|
|
@ -5,15 +5,27 @@ defmodule Pleroma.Search.Meilisearch do
|
||||||
alias Pleroma.Activity
|
alias Pleroma.Activity
|
||||||
|
|
||||||
import Pleroma.Search.DatabaseSearch
|
import Pleroma.Search.DatabaseSearch
|
||||||
import Ecto.Query
|
|
||||||
|
|
||||||
@behaviour Pleroma.Search.SearchBackend
|
@behaviour Pleroma.Search.SearchBackend
|
||||||
|
|
||||||
defp meili_headers do
|
defp meili_headers(key) do
|
||||||
private_key = Pleroma.Config.get([Pleroma.Search.Meilisearch, :private_key])
|
key_header =
|
||||||
|
if is_nil(key), do: [], else: [{"Authorization", "Bearer #{key}"}]
|
||||||
|
|
||||||
[{"Content-Type", "application/json"}] ++
|
[{"Content-Type", "application/json"} | key_header]
|
||||||
if is_nil(private_key), do: [], else: [{"Authorization", "Bearer #{private_key}"}]
|
end
|
||||||
|
|
||||||
|
defp meili_headers_admin do
|
||||||
|
private_key = Pleroma.Config.get([Pleroma.Search.Meilisearch, :private_key])
|
||||||
|
meili_headers(private_key)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp meili_headers_search do
|
||||||
|
search_key =
|
||||||
|
Pleroma.Config.get([Pleroma.Search.Meilisearch, :search_key]) ||
|
||||||
|
Pleroma.Config.get([Pleroma.Search.Meilisearch, :private_key])
|
||||||
|
|
||||||
|
meili_headers(search_key)
|
||||||
end
|
end
|
||||||
|
|
||||||
def meili_get(path) do
|
def meili_get(path) do
|
||||||
|
@ -22,7 +34,7 @@ defmodule Pleroma.Search.Meilisearch do
|
||||||
result =
|
result =
|
||||||
Pleroma.HTTP.get(
|
Pleroma.HTTP.get(
|
||||||
Path.join(endpoint, path),
|
Path.join(endpoint, path),
|
||||||
meili_headers()
|
meili_headers_admin()
|
||||||
)
|
)
|
||||||
|
|
||||||
with {:ok, res} <- result do
|
with {:ok, res} <- result do
|
||||||
|
@ -30,14 +42,14 @@ defmodule Pleroma.Search.Meilisearch do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def meili_post(path, params) do
|
defp meili_search(params) do
|
||||||
endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url])
|
endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url])
|
||||||
|
|
||||||
result =
|
result =
|
||||||
Pleroma.HTTP.post(
|
Pleroma.HTTP.post(
|
||||||
Path.join(endpoint, path),
|
Path.join(endpoint, "/indexes/objects/search"),
|
||||||
Jason.encode!(params),
|
Jason.encode!(params),
|
||||||
meili_headers()
|
meili_headers_search()
|
||||||
)
|
)
|
||||||
|
|
||||||
with {:ok, res} <- result do
|
with {:ok, res} <- result do
|
||||||
|
@ -53,7 +65,7 @@ defmodule Pleroma.Search.Meilisearch do
|
||||||
:put,
|
:put,
|
||||||
Path.join(endpoint, path),
|
Path.join(endpoint, path),
|
||||||
Jason.encode!(params),
|
Jason.encode!(params),
|
||||||
meili_headers(),
|
meili_headers_admin(),
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -70,7 +82,7 @@ defmodule Pleroma.Search.Meilisearch do
|
||||||
:delete,
|
:delete,
|
||||||
Path.join(endpoint, path),
|
Path.join(endpoint, path),
|
||||||
"",
|
"",
|
||||||
meili_headers(),
|
meili_headers_admin(),
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -81,25 +93,20 @@ defmodule Pleroma.Search.Meilisearch do
|
||||||
author = Keyword.get(options, :author)
|
author = Keyword.get(options, :author)
|
||||||
|
|
||||||
res =
|
res =
|
||||||
meili_post(
|
meili_search(%{q: query, offset: offset, limit: limit})
|
||||||
"/indexes/objects/search",
|
|
||||||
%{q: query, offset: offset, limit: limit}
|
|
||||||
)
|
|
||||||
|
|
||||||
with {:ok, result} <- res do
|
with {:ok, result} <- res do
|
||||||
hits = result["hits"] |> Enum.map(& &1["ap"])
|
hits = result["hits"] |> Enum.map(& &1["ap"])
|
||||||
|
|
||||||
try do
|
try do
|
||||||
hits
|
hits
|
||||||
|> Activity.create_by_object_ap_id()
|
|> Activity.get_presorted_create_by_object_ap_id()
|
||||||
|> Activity.with_preloaded_object()
|
|
||||||
|> Activity.with_preloaded_object()
|
|> Activity.with_preloaded_object()
|
||||||
|> Activity.restrict_deactivated_users()
|
|> Activity.restrict_deactivated_users()
|
||||||
|> maybe_restrict_local(user)
|
|> maybe_restrict_local(user)
|
||||||
|> maybe_restrict_author(author)
|
|> maybe_restrict_author(author)
|
||||||
|> maybe_restrict_blocked(user)
|
|> maybe_restrict_blocked(user)
|
||||||
|> maybe_fetch(user, query)
|
|> maybe_fetch(user, query)
|
||||||
|> order_by([object: obj], desc: obj.data["published"])
|
|
||||||
|> Pleroma.Repo.all()
|
|> Pleroma.Repo.all()
|
||||||
rescue
|
rescue
|
||||||
_ -> maybe_fetch([], user, query)
|
_ -> maybe_fetch([], user, query)
|
||||||
|
|
|
@ -10,7 +10,7 @@ defmodule Pleroma.Signature do
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
|
|
||||||
@known_suffixes ["/publickey", "/main-key"]
|
@known_suffixes ["/publickey", "/main-key", "#key"]
|
||||||
|
|
||||||
def key_id_to_actor_id(key_id) do
|
def key_id_to_actor_id(key_id) do
|
||||||
uri =
|
uri =
|
||||||
|
|
|
@ -13,7 +13,6 @@ defmodule Pleroma.Upload do
|
||||||
* `:uploader`: override uploader
|
* `:uploader`: override uploader
|
||||||
* `:filters`: override filters
|
* `:filters`: override filters
|
||||||
* `:size_limit`: override size limit
|
* `:size_limit`: override size limit
|
||||||
* `:activity_type`: override activity type
|
|
||||||
|
|
||||||
The `%Pleroma.Upload{}` struct: all documented fields are meant to be overwritten in filters:
|
The `%Pleroma.Upload{}` struct: all documented fields are meant to be overwritten in filters:
|
||||||
|
|
||||||
|
@ -48,7 +47,6 @@ defmodule Pleroma.Upload do
|
||||||
@type option ::
|
@type option ::
|
||||||
{:type, :avatar | :banner | :background}
|
{:type, :avatar | :banner | :background}
|
||||||
| {:description, String.t()}
|
| {:description, String.t()}
|
||||||
| {:activity_type, String.t()}
|
|
||||||
| {:size_limit, nil | non_neg_integer()}
|
| {:size_limit, nil | non_neg_integer()}
|
||||||
| {:uploader, module()}
|
| {:uploader, module()}
|
||||||
| {:filters, [module()]}
|
| {:filters, [module()]}
|
||||||
|
@ -143,7 +141,7 @@ defmodule Pleroma.Upload do
|
||||||
end
|
end
|
||||||
|
|
||||||
%{
|
%{
|
||||||
activity_type: Keyword.get(opts, :activity_type, activity_type),
|
activity_type: activity_type,
|
||||||
size_limit: Keyword.get(opts, :size_limit, size_limit),
|
size_limit: Keyword.get(opts, :size_limit, size_limit),
|
||||||
uploader: Keyword.get(opts, :uploader, Pleroma.Config.get([__MODULE__, :uploader])),
|
uploader: Keyword.get(opts, :uploader, Pleroma.Config.get([__MODULE__, :uploader])),
|
||||||
filters:
|
filters:
|
||||||
|
|
|
@ -33,8 +33,7 @@ defmodule Pleroma.Upload.Filter.Exiftool.ReadDescription do
|
||||||
defp read_when_empty(_, file, tag) do
|
defp read_when_empty(_, file, tag) do
|
||||||
try do
|
try do
|
||||||
{tag_content, 0} =
|
{tag_content, 0} =
|
||||||
System.cmd("exiftool", ["-b", "-s3", tag, file],
|
System.cmd("exiftool", ["-b", "-s3", "-ignoreMinorErrors", "-q", "-q", tag, file],
|
||||||
stderr_to_stdout: true,
|
|
||||||
parallelism: true
|
parallelism: true
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1545,11 +1545,19 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
defp normalize_also_known_as(aka) when is_binary(aka), do: [aka]
|
defp normalize_also_known_as(aka) when is_binary(aka), do: [aka]
|
||||||
defp normalize_also_known_as(nil), do: []
|
defp normalize_also_known_as(nil), do: []
|
||||||
|
|
||||||
|
defp normalize_attachment(%{} = attachment), do: [attachment]
|
||||||
|
defp normalize_attachment(attachment) when is_list(attachment), do: attachment
|
||||||
|
defp normalize_attachment(_), do: []
|
||||||
|
|
||||||
defp object_to_user_data(data, additional) do
|
defp object_to_user_data(data, additional) do
|
||||||
fields =
|
fields =
|
||||||
data
|
data
|
||||||
|> Map.get("attachment", [])
|
|> Map.get("attachment", [])
|
||||||
|> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
|
|> normalize_attachment()
|
||||||
|
|> Enum.filter(fn
|
||||||
|
%{"type" => t} -> t == "PropertyValue"
|
||||||
|
_ -> false
|
||||||
|
end)
|
||||||
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
|
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
|
||||||
|
|
||||||
emojis =
|
emojis =
|
||||||
|
@ -1816,19 +1824,20 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def pinned_fetch_task(nil), do: nil
|
def enqueue_pin_fetches(%{pinned_objects: pins}) do
|
||||||
|
# enqueue a task to fetch all pinned objects
|
||||||
def pinned_fetch_task(%{pinned_objects: pins}) do
|
Enum.each(pins, fn {ap_id, _} ->
|
||||||
if Enum.all?(pins, fn {ap_id, _} ->
|
if is_nil(Object.get_cached_by_ap_id(ap_id)) do
|
||||||
Object.get_cached_by_ap_id(ap_id) ||
|
Pleroma.Workers.RemoteFetcherWorker.enqueue("fetch_remote", %{
|
||||||
match?({:ok, _object}, Fetcher.fetch_object_from_id(ap_id))
|
"id" => ap_id,
|
||||||
end) do
|
"depth" => 1
|
||||||
:ok
|
})
|
||||||
else
|
end
|
||||||
:error
|
end)
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def enqueue_pin_fetches(_), do: nil
|
||||||
|
|
||||||
def make_user_from_ap_id(ap_id, additional \\ []) do
|
def make_user_from_ap_id(ap_id, additional \\ []) do
|
||||||
user = User.get_cached_by_ap_id(ap_id)
|
user = User.get_cached_by_ap_id(ap_id)
|
||||||
|
|
||||||
|
@ -1836,8 +1845,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
Transmogrifier.upgrade_user_from_ap_id(ap_id)
|
Transmogrifier.upgrade_user_from_ap_id(ap_id)
|
||||||
else
|
else
|
||||||
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id, additional) do
|
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id, additional) do
|
||||||
{:ok, _pid} = Task.start(fn -> pinned_fetch_task(data) end)
|
|
||||||
|
|
||||||
user =
|
user =
|
||||||
if data.ap_id != ap_id do
|
if data.ap_id != ap_id do
|
||||||
User.get_cached_by_ap_id(data.ap_id)
|
User.get_cached_by_ap_id(data.ap_id)
|
||||||
|
@ -1849,6 +1856,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
user
|
user
|
||||||
|> User.remote_user_changeset(data)
|
|> User.remote_user_changeset(data)
|
||||||
|> User.update_and_set_cache()
|
|> User.update_and_set_cache()
|
||||||
|
|> tap(fn _ -> enqueue_pin_fetches(data) end)
|
||||||
else
|
else
|
||||||
maybe_handle_clashing_nickname(data)
|
maybe_handle_clashing_nickname(data)
|
||||||
|
|
||||||
|
@ -1856,6 +1864,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
|> User.remote_user_changeset()
|
|> User.remote_user_changeset()
|
||||||
|> Repo.insert()
|
|> Repo.insert()
|
||||||
|> User.set_cache()
|
|> User.set_cache()
|
||||||
|
|> tap(fn _ -> enqueue_pin_fetches(data) end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1034,7 +1034,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
||||||
with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
|
with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
|
||||||
{:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
|
{:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
|
||||||
{:ok, user} <- update_user(user, data) do
|
{:ok, user} <- update_user(user, data) do
|
||||||
{:ok, _pid} = Task.start(fn -> ActivityPub.pinned_fetch_task(user) end)
|
ActivityPub.enqueue_pin_fetches(user)
|
||||||
TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
|
TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
|
||||||
{:ok, user}
|
{:ok, user}
|
||||||
else
|
else
|
||||||
|
|
|
@ -41,7 +41,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
||||||
preview?: false,
|
preview?: false,
|
||||||
changes: %{}
|
changes: %{}
|
||||||
|
|
||||||
def new(user, params) do
|
defp new(user, params) do
|
||||||
%__MODULE__{user: user}
|
%__MODULE__{user: user}
|
||||||
|> put_params(params)
|
|> put_params(params)
|
||||||
end
|
end
|
||||||
|
@ -92,9 +92,14 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp attachments(%{params: params} = draft) do
|
defp attachments(%{params: params, user: user} = draft) do
|
||||||
attachments = Utils.attachments_from_ids(params)
|
case Utils.attachments_from_ids(user, params) do
|
||||||
%__MODULE__{draft | attachments: attachments}
|
attachments when is_list(attachments) ->
|
||||||
|
%__MODULE__{draft | attachments: attachments}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
add_error(draft, reason)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp in_reply_to(%{params: %{in_reply_to_status_id: ""}} = draft), do: draft
|
defp in_reply_to(%{params: %{in_reply_to_status_id: ""}} = draft), do: draft
|
||||||
|
|
|
@ -22,43 +22,31 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
||||||
require Logger
|
require Logger
|
||||||
require Pleroma.Constants
|
require Pleroma.Constants
|
||||||
|
|
||||||
def attachments_from_ids(%{media_ids: ids, descriptions: desc}) do
|
def attachments_from_ids(user, %{media_ids: ids}) do
|
||||||
attachments_from_ids_descs(ids, desc)
|
attachments_from_ids(user, ids, [])
|
||||||
end
|
end
|
||||||
|
|
||||||
def attachments_from_ids(%{media_ids: ids}) do
|
def attachments_from_ids(_, _), do: []
|
||||||
attachments_from_ids_no_descs(ids)
|
|
||||||
|
defp attachments_from_ids(_user, [], acc), do: Enum.reverse(acc)
|
||||||
|
|
||||||
|
defp attachments_from_ids(user, [media_id | ids], acc) do
|
||||||
|
with {_, %Object{} = object} <- {:get, get_attachment(media_id)},
|
||||||
|
:ok <- Object.authorize_access(object, user) do
|
||||||
|
attachments_from_ids(user, ids, [object.data | acc])
|
||||||
|
else
|
||||||
|
{:get, _} -> attachments_from_ids(user, ids, acc)
|
||||||
|
{:error, reason} -> {:error, reason}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def attachments_from_ids(_), do: []
|
def get_attachment(media_id) do
|
||||||
|
with %Object{} = object <- Repo.get(Object, media_id),
|
||||||
def attachments_from_ids_no_descs([]), do: []
|
true <- object.data["type"] in Pleroma.Constants.attachment_types() do
|
||||||
|
object
|
||||||
def attachments_from_ids_no_descs(ids) do
|
else
|
||||||
Enum.map(ids, fn media_id ->
|
_ -> nil
|
||||||
case get_attachment(media_id) do
|
end
|
||||||
%Object{data: data} -> data
|
|
||||||
_ -> nil
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|> Enum.reject(&is_nil/1)
|
|
||||||
end
|
|
||||||
|
|
||||||
def attachments_from_ids_descs([], _), do: []
|
|
||||||
|
|
||||||
def attachments_from_ids_descs(ids, descs_str) do
|
|
||||||
{_, descs} = Jason.decode(descs_str)
|
|
||||||
|
|
||||||
Enum.map(ids, fn media_id ->
|
|
||||||
with %Object{data: data} <- get_attachment(media_id) do
|
|
||||||
Map.put(data, "name", descs[media_id])
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|> Enum.reject(&is_nil/1)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp get_attachment(media_id) do
|
|
||||||
Repo.get(Object, media_id)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec get_to_and_cc(ActivityDraft.t()) :: {list(String.t()), list(String.t())}
|
@spec get_to_and_cc(ActivityDraft.t()) :: {list(String.t()), list(String.t())}
|
||||||
|
|
|
@ -8,6 +8,7 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
|
alias Pleroma.Web.CommonAPI.Utils
|
||||||
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||||
|
|
||||||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||||
|
@ -55,12 +56,15 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
|
||||||
|
|
||||||
@doc "PUT /api/v1/media/:id"
|
@doc "PUT /api/v1/media/:id"
|
||||||
def update(%{assigns: %{user: user}, body_params: %{description: description}} = conn, %{id: id}) do
|
def update(%{assigns: %{user: user}, body_params: %{description: description}} = conn, %{id: id}) do
|
||||||
with %Object{} = object <- Object.get_by_id(id),
|
with {_, %Object{} = object} <- {:get, Utils.get_attachment(id)},
|
||||||
:ok <- Object.authorize_access(object, user),
|
:ok <- Object.authorize_access(object, user),
|
||||||
{:ok, %Object{data: data}} <- Object.update_data(object, %{"name" => description}) do
|
{:ok, %Object{data: data}} <- Object.update_data(object, %{"name" => description}) do
|
||||||
attachment_data = Map.put(data, "id", object.id)
|
attachment_data = Map.put(data, "id", object.id)
|
||||||
|
|
||||||
render(conn, "attachment.json", %{attachment: attachment_data})
|
render(conn, "attachment.json", %{attachment: attachment_data})
|
||||||
|
else
|
||||||
|
{:get, _} -> {:error, :not_found}
|
||||||
|
e -> e
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -68,11 +72,14 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
|
||||||
|
|
||||||
@doc "GET /api/v1/media/:id"
|
@doc "GET /api/v1/media/:id"
|
||||||
def show(%{assigns: %{user: user}} = conn, %{id: id}) do
|
def show(%{assigns: %{user: user}} = conn, %{id: id}) do
|
||||||
with %Object{data: data, id: object_id} = object <- Object.get_by_id(id),
|
with {_, %Object{data: data, id: object_id} = object} <- {:get, Utils.get_attachment(id)},
|
||||||
:ok <- Object.authorize_access(object, user) do
|
:ok <- Object.authorize_access(object, user) do
|
||||||
attachment_data = Map.put(data, "id", object_id)
|
attachment_data = Map.put(data, "id", object_id)
|
||||||
|
|
||||||
render(conn, "attachment.json", %{attachment: attachment_data})
|
render(conn, "attachment.json", %{attachment: attachment_data})
|
||||||
|
else
|
||||||
|
{:get, _} -> {:error, :not_found}
|
||||||
|
e -> e
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -87,7 +87,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
||||||
%{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
|
%{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
|
||||||
)
|
)
|
||||||
|
|
||||||
@rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a
|
@rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete update)a
|
||||||
|
|
||||||
plug(
|
plug(
|
||||||
RateLimiter,
|
RateLimiter,
|
||||||
|
|
|
@ -156,11 +156,21 @@ defmodule Pleroma.Web.WebFinger do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
|
||||||
def find_lrdd_template(domain) do
|
def find_lrdd_template(domain) do
|
||||||
|
@cachex.fetch!(:host_meta_cache, domain, fn _ ->
|
||||||
|
{:commit, fetch_lrdd_template(domain)}
|
||||||
|
end)
|
||||||
|
rescue
|
||||||
|
e -> {:error, "Cachex error: #{inspect(e)}"}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fetch_lrdd_template(domain) do
|
||||||
# WebFinger is restricted to HTTPS - https://tools.ietf.org/html/rfc7033#section-9.1
|
# WebFinger is restricted to HTTPS - https://tools.ietf.org/html/rfc7033#section-9.1
|
||||||
meta_url = "https://#{domain}/.well-known/host-meta"
|
meta_url = "https://#{domain}/.well-known/host-meta"
|
||||||
|
|
||||||
with {:ok, %{status: status, body: body}} when status in 200..299 <- HTTP.get(meta_url) do
|
with {:ok, %{status: status, body: body}} when status in 200..299 <-
|
||||||
|
HTTP.Backoff.get(meta_url) do
|
||||||
get_template_from_xml(body)
|
get_template_from_xml(body)
|
||||||
else
|
else
|
||||||
error ->
|
error ->
|
||||||
|
@ -169,7 +179,7 @@ defmodule Pleroma.Web.WebFinger do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_address_from_domain(domain, encoded_account) when is_binary(domain) do
|
defp get_address_from_domain(domain, "acct:" <> _ = encoded_account) when is_binary(domain) do
|
||||||
case find_lrdd_template(domain) do
|
case find_lrdd_template(domain) do
|
||||||
{:ok, template} ->
|
{:ok, template} ->
|
||||||
String.replace(template, "{uri}", encoded_account)
|
String.replace(template, "{uri}", encoded_account)
|
||||||
|
@ -179,6 +189,11 @@ defmodule Pleroma.Web.WebFinger do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp get_address_from_domain(domain, account) when is_binary(domain) do
|
||||||
|
encoded_account = URI.encode("acct:#{account}")
|
||||||
|
get_address_from_domain(domain, encoded_account)
|
||||||
|
end
|
||||||
|
|
||||||
defp get_address_from_domain(_, _), do: {:error, :webfinger_no_domain}
|
defp get_address_from_domain(_, _), do: {:error, :webfinger_no_domain}
|
||||||
|
|
||||||
@spec finger(String.t()) :: {:ok, map()} | {:error, any()}
|
@spec finger(String.t()) :: {:ok, map()} | {:error, any()}
|
||||||
|
@ -193,11 +208,9 @@ defmodule Pleroma.Web.WebFinger do
|
||||||
URI.parse(account).host
|
URI.parse(account).host
|
||||||
end
|
end
|
||||||
|
|
||||||
encoded_account = URI.encode("acct:#{account}")
|
with address when is_binary(address) <- get_address_from_domain(domain, account),
|
||||||
|
|
||||||
with address when is_binary(address) <- get_address_from_domain(domain, encoded_account),
|
|
||||||
{:ok, %{status: status, body: body, headers: headers}} when status in 200..299 <-
|
{:ok, %{status: status, body: body, headers: headers}} when status in 200..299 <-
|
||||||
HTTP.get(
|
HTTP.Backoff.get(
|
||||||
address,
|
address,
|
||||||
[{"accept", "application/xrd+xml,application/jrd+json"}]
|
[{"accept", "application/xrd+xml,application/jrd+json"}]
|
||||||
) do
|
) do
|
||||||
|
@ -217,10 +230,28 @@ defmodule Pleroma.Web.WebFinger do
|
||||||
_ ->
|
_ ->
|
||||||
{:error, {:content_type, nil}}
|
{:error, {:content_type, nil}}
|
||||||
end
|
end
|
||||||
|
|> case do
|
||||||
|
{:ok, data} -> validate_webfinger(address, data)
|
||||||
|
error -> error
|
||||||
|
end
|
||||||
else
|
else
|
||||||
error ->
|
error ->
|
||||||
Logger.debug("Couldn't finger #{account}: #{inspect(error)}")
|
Logger.debug("Couldn't finger #{account}: #{inspect(error)}")
|
||||||
error
|
error
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp validate_webfinger(request_url, %{"subject" => "acct:" <> acct = subject} = data) do
|
||||||
|
with [_name, acct_host] <- String.split(acct, "@"),
|
||||||
|
{_, url} <- {:address, get_address_from_domain(acct_host, subject)},
|
||||||
|
%URI{host: request_host} <- URI.parse(request_url),
|
||||||
|
%URI{host: acct_host} <- URI.parse(url),
|
||||||
|
{_, true} <- {:hosts_match, acct_host == request_host} do
|
||||||
|
{:ok, data}
|
||||||
|
else
|
||||||
|
_ -> {:error, {:webfinger_invalid, request_url, data}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp validate_webfinger(url, data), do: {:error, {:webfinger_invalid, url, data}}
|
||||||
end
|
end
|
||||||
|
|
2
mix.exs
2
mix.exs
|
@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do
|
||||||
def project do
|
def project do
|
||||||
[
|
[
|
||||||
app: :pleroma,
|
app: :pleroma,
|
||||||
version: version("3.13.0"),
|
version: version("3.13.2"),
|
||||||
elixir: "~> 1.14",
|
elixir: "~> 1.14",
|
||||||
elixirc_paths: elixirc_paths(Mix.env()),
|
elixirc_paths: elixirc_paths(Mix.env()),
|
||||||
compilers: Mix.compilers(),
|
compilers: Mix.compilers(),
|
||||||
|
|
|
@ -3,14 +3,16 @@ msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2022-07-29 11:37+0000\n"
|
"POT-Creation-Date: 2022-07-29 11:37+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: 2024-04-13 22:55+0000\n"
|
||||||
"Last-Translator: Automatically generated\n"
|
"Last-Translator: fadelkon <fadelkon@posteo.net>\n"
|
||||||
"Language-Team: none\n"
|
"Language-Team: Catalan <http://translate.akkoma.dev/projects/akkoma/"
|
||||||
|
"akkoma-backend-errors/ca/>\n"
|
||||||
"Language: ca\n"
|
"Language: ca\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"X-Generator: Translate Toolkit 3.7.1\n"
|
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||||
|
"X-Generator: Weblate 4.18.2\n"
|
||||||
|
|
||||||
# # This file is a PO Template file.
|
# # This file is a PO Template file.
|
||||||
# #
|
# #
|
||||||
|
@ -610,46 +612,46 @@ msgstr "El teu compte espera aprovació."
|
||||||
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:262
|
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:262
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "File is too large"
|
msgid "File is too large"
|
||||||
msgstr ""
|
msgstr "L'arxiu és massa gran"
|
||||||
|
|
||||||
#: lib/pleroma/web/mastodon_api/controllers/tag_controller.ex:37
|
#: lib/pleroma/web/mastodon_api/controllers/tag_controller.ex:37
|
||||||
#: lib/pleroma/web/mastodon_api/controllers/tag_controller.ex:48
|
#: lib/pleroma/web/mastodon_api/controllers/tag_controller.ex:48
|
||||||
#: lib/pleroma/web/mastodon_api/controllers/tag_controller.ex:59
|
#: lib/pleroma/web/mastodon_api/controllers/tag_controller.ex:59
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Hashtag not found"
|
msgid "Hashtag not found"
|
||||||
msgstr "Llista no trobada"
|
msgstr "No s'ha trobat l'etiqueta"
|
||||||
|
|
||||||
#: lib/pleroma/web/common_api/activity_draft.ex:144
|
#: lib/pleroma/web/common_api/activity_draft.ex:144
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Invalid language"
|
msgid "Invalid language"
|
||||||
msgstr ""
|
msgstr "Llengua incompatible"
|
||||||
|
|
||||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:218
|
#: lib/pleroma/web/o_auth/o_auth_controller.ex:218
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format
|
||||||
msgid "This action is outside of authorized scopes"
|
msgid "This action is outside of authorized scopes"
|
||||||
msgstr "Aquesta acció és fora dels àmbits autoritzats"
|
msgstr "Aquesta acció és fora dels àmbits autoritzats"
|
||||||
|
|
||||||
#: lib/pleroma/web/common_api/activity_draft.ex:129
|
#: lib/pleroma/web/common_api/activity_draft.ex:129
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "You can only quote public or unlisted statuses"
|
msgid "You can only quote public or unlisted statuses"
|
||||||
msgstr ""
|
msgstr "Només pots citar publicacions desllistades o públiques"
|
||||||
|
|
||||||
#: lib/pleroma/web/common_api/activity_draft.ex:126
|
#: lib/pleroma/web/common_api/activity_draft.ex:126
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "You can't quote a status that doesn't exist"
|
msgid "You can't quote a status that doesn't exist"
|
||||||
msgstr ""
|
msgstr "No pots citar una publicació que no existeix"
|
||||||
|
|
||||||
#: lib/pleroma/web/embed_controller.ex:35
|
#: lib/pleroma/web/embed_controller.ex:35
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Federated posts cannot be embedded"
|
msgid "Federated posts cannot be embedded"
|
||||||
msgstr ""
|
msgstr "No es poden incrustar publicacions federades"
|
||||||
|
|
||||||
#: lib/pleroma/web/embed_controller.ex:38
|
#: lib/pleroma/web/embed_controller.ex:38
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Not authorized to view this post"
|
msgid "Not authorized to view this post"
|
||||||
msgstr ""
|
msgstr "No tens permís per veure aquesta publicació"
|
||||||
|
|
||||||
#: lib/pleroma/web/embed_controller.ex:32
|
#: lib/pleroma/web/embed_controller.ex:32
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Post not found"
|
msgid "Post not found"
|
||||||
msgstr "Llista no trobada"
|
msgstr "No s'ha trobat la publicació"
|
||||||
|
|
|
@ -3,8 +3,8 @@ msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2022-07-28 09:15+0000\n"
|
"POT-Creation-Date: 2022-07-28 09:15+0000\n"
|
||||||
"PO-Revision-Date: 2022-07-30 21:58+0000\n"
|
"PO-Revision-Date: 2024-04-13 22:55+0000\n"
|
||||||
"Last-Translator: sola <spla@mastodont.cat>\n"
|
"Last-Translator: fadelkon <fadelkon@posteo.net>\n"
|
||||||
"Language-Team: Catalan <http://translate.akkoma.dev/projects/akkoma/"
|
"Language-Team: Catalan <http://translate.akkoma.dev/projects/akkoma/"
|
||||||
"akkoma-backend-static-pages/ca/>\n"
|
"akkoma-backend-static-pages/ca/>\n"
|
||||||
"Language: ca\n"
|
"Language: ca\n"
|
||||||
|
@ -12,7 +12,7 @@ msgstr ""
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||||
"X-Generator: Weblate 4.13.1\n"
|
"X-Generator: Weblate 4.18.2\n"
|
||||||
|
|
||||||
## This file is a PO Template file.
|
## This file is a PO Template file.
|
||||||
##
|
##
|
||||||
|
@ -531,25 +531,25 @@ msgid "Welcome to %{instance_name}!"
|
||||||
msgstr "Benvingut a %{instance_name}!"
|
msgstr "Benvingut a %{instance_name}!"
|
||||||
|
|
||||||
#: lib/pleroma/emails/user_email.ex:368
|
#: lib/pleroma/emails/user_email.ex:368
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "account archive email body - admin requested"
|
msgctxt "account archive email body - admin requested"
|
||||||
msgid "<p>Admin @%{admin_nickname} requested a full backup of your Akkoma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
|
msgid "<p>Admin @%{admin_nickname} requested a full backup of your Akkoma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"<p>L'Administrador @%{admin_nickname} ha sol·licitat una copia de seguretat "
|
"<p>L'Administrador @%{admin_nickname} ha sol·licitat una còpia de seguretat "
|
||||||
"completa del teu compte Akkoma. Està preparat per a descarrega:</p>\n"
|
"completa del teu compte Akkoma. Està preparada per descarregar:</p>\n"
|
||||||
"<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
|
"<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
|
||||||
|
|
||||||
#: lib/pleroma/emails/user_email.ex:356
|
#: lib/pleroma/emails/user_email.ex:356
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "account archive email body - self-requested"
|
msgctxt "account archive email body - self-requested"
|
||||||
msgid "<p>You requested a full backup of your Akkoma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
|
msgid "<p>You requested a full backup of your Akkoma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"<p>Has sol·licitat una copia de seguretat completa del teu compte Akkoma. "
|
"<p>Has sol·licitat una còpia de seguretat completa del teu compte Akkoma. Ja "
|
||||||
"Està llest per a descarrega:</p>\n"
|
"la pots descarregar:</p>\n"
|
||||||
"<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
|
"<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:41
|
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:41
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "oauth register page title"
|
msgctxt "oauth register page title"
|
||||||
msgid "This is your first visit! Please enter your Akkoma handle."
|
msgid "This is your first visit! Please enter your Akkoma handle."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -560,22 +560,22 @@ msgstr ""
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "remote follow error message - unknown error"
|
msgctxt "remote follow error message - unknown error"
|
||||||
msgid "Something went wrong."
|
msgid "Something went wrong."
|
||||||
msgstr ""
|
msgstr "Hi ha hagut algun problema."
|
||||||
|
|
||||||
#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:67
|
#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:67
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "remote follow error message - user not found"
|
msgctxt "remote follow error message - user not found"
|
||||||
msgid "Could not find user"
|
msgid "Could not find user"
|
||||||
msgstr ""
|
msgstr "No s'ha trobat l'usuari/a/ï"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:8
|
#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:8
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "status interact authorization button"
|
msgctxt "status interact authorization button"
|
||||||
msgid "Interact"
|
msgid "Interact"
|
||||||
msgstr ""
|
msgstr "Interacciona"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:2
|
#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:2
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "status interact error"
|
msgctxt "status interact error"
|
||||||
msgid "Error: %{error}"
|
msgid "Error: %{error}"
|
||||||
msgstr "Error: %{error}"
|
msgstr "Error: %{error}"
|
||||||
|
@ -584,33 +584,33 @@ msgstr "Error: %{error}"
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "status interact error message - status not found"
|
msgctxt "status interact error message - status not found"
|
||||||
msgid "Could not find status"
|
msgid "Could not find status"
|
||||||
msgstr ""
|
msgstr "No s'ha trobat l'estat"
|
||||||
|
|
||||||
#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:144
|
#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:144
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "status interact error message - unknown error"
|
msgctxt "status interact error message - unknown error"
|
||||||
msgid "Something went wrong."
|
msgid "Something went wrong."
|
||||||
msgstr ""
|
msgstr "Hi ha hagut algun problema."
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:4
|
#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:4
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "status interact header"
|
msgctxt "status interact header"
|
||||||
msgid "Interacting with %{nickname}'s %{status_link}"
|
msgid "Interacting with %{nickname}'s %{status_link}"
|
||||||
msgstr ""
|
msgstr "S'està interactuant amb %{status_link} de %{nickname}"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:4
|
#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:4
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "status interact header - status link text"
|
msgctxt "status interact header - status link text"
|
||||||
msgid "status"
|
msgid "status"
|
||||||
msgstr ""
|
msgstr "estat"
|
||||||
|
|
||||||
#: lib/pleroma/emails/user_email.ex:119
|
#: lib/pleroma/emails/user_email.ex:119
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "user invitation email body"
|
msgctxt "user invitation email body"
|
||||||
msgid "<h3>You are invited to %{instance_name}</h3>\n<p>%{inviter_name} invites you to join %{instance_name}, an instance of Akkoma federated social networking platform.</p>\n<p>Click the following link to register: <a href=\"%{registration_url}\">accept invitation</a>.</p>\n"
|
msgid "<h3>You are invited to %{instance_name}</h3>\n<p>%{inviter_name} invites you to join %{instance_name}, an instance of Akkoma federated social networking platform.</p>\n<p>Click the following link to register: <a href=\"%{registration_url}\">accept invitation</a>.</p>\n"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"<h3>Has estat invitat a %{instance_name}</h3>\n"
|
"<h3>T'han convidat a %{instance_name}</h3>\n"
|
||||||
"<p>%{inviter_name} t'invita a unir-te a %{instance_name}, una instància de "
|
"<p>%{inviter_name} t'anima a unir-te a %{instance_name}, una instància de la "
|
||||||
"la plataforma de xarxa social federada Akkoma.</p>\n"
|
"plataforma de xarxa social federada Akkoma.</p>\n"
|
||||||
"<p>Clica el següent enllaç per a registrar-te: <a href=\"%{registration_url}"
|
"<p>Clica el següent enllaç per a registrar-te: <a href=\"%{registration_url}"
|
||||||
"\">accepta invitació</a>.</p>\n"
|
"\">accepta la invitació</a>.</p>\n"
|
||||||
|
|
6449
priv/gettext/pt/LC_MESSAGES/config_descriptions.po
Normal file
6449
priv/gettext/pt/LC_MESSAGES/config_descriptions.po
Normal file
File diff suppressed because it is too large
Load diff
657
priv/gettext/pt/LC_MESSAGES/errors.po
Normal file
657
priv/gettext/pt/LC_MESSAGES/errors.po
Normal file
|
@ -0,0 +1,657 @@
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2024-03-18 12:55+0000\n"
|
||||||
|
"PO-Revision-Date: 2024-03-19 01:10+0000\n"
|
||||||
|
"Last-Translator: Jammer Lammer <akHarINlMYExpSmVPDRT@proton.me>\n"
|
||||||
|
"Language-Team: Portuguese <http://translate.akkoma.dev/projects/akkoma/"
|
||||||
|
"akkoma-backend-errors/pt/>\n"
|
||||||
|
"Language: pt\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=n > 1;\n"
|
||||||
|
"X-Generator: Weblate 4.18.2\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 "não pode estar em branco"
|
||||||
|
|
||||||
|
## From Ecto.Changeset.unique_constraint/3
|
||||||
|
msgid "has already been taken"
|
||||||
|
msgstr "já está em uso"
|
||||||
|
|
||||||
|
## From Ecto.Changeset.put_change/3
|
||||||
|
msgid "is invalid"
|
||||||
|
msgstr "é inválido"
|
||||||
|
|
||||||
|
## From Ecto.Changeset.validate_format/3
|
||||||
|
msgid "has invalid format"
|
||||||
|
msgstr "tem um formato inválido"
|
||||||
|
|
||||||
|
## From Ecto.Changeset.validate_subset/3
|
||||||
|
msgid "has an invalid entry"
|
||||||
|
msgstr "tem uma entrada inválida"
|
||||||
|
|
||||||
|
## From Ecto.Changeset.validate_exclusion/3
|
||||||
|
msgid "is reserved"
|
||||||
|
msgstr "está reservado"
|
||||||
|
|
||||||
|
## From Ecto.Changeset.validate_confirmation/3
|
||||||
|
msgid "does not match confirmation"
|
||||||
|
msgstr "a confirmação não coincide"
|
||||||
|
|
||||||
|
## From Ecto.Changeset.no_assoc_constraint/3
|
||||||
|
msgid "is still associated with this entry"
|
||||||
|
msgstr "ainda está associado com essa entrada"
|
||||||
|
|
||||||
|
msgid "are still associated with this entry"
|
||||||
|
msgstr "ainda estão associados com essa entrada"
|
||||||
|
|
||||||
|
## From Ecto.Changeset.validate_length/3
|
||||||
|
msgid "should be %{count} character(s)"
|
||||||
|
msgid_plural "should be %{count} character(s)"
|
||||||
|
msgstr[0] "deve ser %{count} caractere"
|
||||||
|
msgstr[1] "deve ser %{count} caracteres"
|
||||||
|
|
||||||
|
msgid "should have %{count} item(s)"
|
||||||
|
msgid_plural "should have %{count} item(s)"
|
||||||
|
msgstr[0] "deve ter %{count} item"
|
||||||
|
msgstr[1] "deve ter %{count} itens"
|
||||||
|
|
||||||
|
msgid "should be at least %{count} character(s)"
|
||||||
|
msgid_plural "should be at least %{count} character(s)"
|
||||||
|
msgstr[0] "deve ter pelo menos %{count} caractere"
|
||||||
|
msgstr[1] "deve ter pelo menos %{count} caracteres"
|
||||||
|
|
||||||
|
msgid "should have at least %{count} item(s)"
|
||||||
|
msgid_plural "should have at least %{count} item(s)"
|
||||||
|
msgstr[0] "deve ter pelo menos %{count} item"
|
||||||
|
msgstr[1] "deve ter pelo menos %{count} itens"
|
||||||
|
|
||||||
|
msgid "should be at most %{count} character(s)"
|
||||||
|
msgid_plural "should be at most %{count} character(s)"
|
||||||
|
msgstr[0] "deve ter no máximo %{count} caractere"
|
||||||
|
msgstr[1] "deve ter no máximo %{count} caracteres"
|
||||||
|
|
||||||
|
msgid "should have at most %{count} item(s)"
|
||||||
|
msgid_plural "should have at most %{count} item(s)"
|
||||||
|
msgstr[0] "deve ter no máximo %{count} item"
|
||||||
|
msgstr[1] "deve ter no máximo %{count} itens"
|
||||||
|
|
||||||
|
## From Ecto.Changeset.validate_number/3
|
||||||
|
msgid "must be less than %{number}"
|
||||||
|
msgstr "deve ser menor que %{number}"
|
||||||
|
|
||||||
|
msgid "must be greater than %{number}"
|
||||||
|
msgstr "deve ser maior que %{number}"
|
||||||
|
|
||||||
|
msgid "must be less than or equal to %{number}"
|
||||||
|
msgstr "deve ser menor ou igual a %{number}"
|
||||||
|
|
||||||
|
msgid "must be greater than or equal to %{number}"
|
||||||
|
msgstr "deve ser maior ou igual a %{number}"
|
||||||
|
|
||||||
|
msgid "must be equal to %{number}"
|
||||||
|
msgstr "deve ser igual a %{number}"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api.ex:503
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Account not found"
|
||||||
|
msgstr "Conta não encontrada"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api.ex:263
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Already voted"
|
||||||
|
msgstr "Já votado"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/o_auth/o_auth_controller.ex:427
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Bad request"
|
||||||
|
msgstr "Má requisição"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/controller_helper.ex:105
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Can't display this activity"
|
||||||
|
msgstr "Não é possível mostrar essa atividade"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:335
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Can't find user"
|
||||||
|
msgstr "Não é possível encontrar o usuário"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:61
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Can't get favorites"
|
||||||
|
msgstr "Não é possível obter os favoritos"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/utils.ex:480
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Cannot post an empty status without attachments"
|
||||||
|
msgstr "Não é possível publicar um status vazio sem anexos"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/utils.ex:468
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Comment must be up to %{max_size} characters"
|
||||||
|
msgstr "Comentários devem ter no máximo %{max_size} caracteres"
|
||||||
|
|
||||||
|
#: lib/pleroma/config_db.ex:199
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Config with params %{params} not found"
|
||||||
|
msgstr "Configuração com parâmetros %{params} não encontrada"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api.ex:114
|
||||||
|
#: lib/pleroma/web/common_api.ex:118
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Could not delete"
|
||||||
|
msgstr "Não foi possível apagar"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api.ex:164
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Could not favorite"
|
||||||
|
msgstr "Não foi possível favoritar"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api.ex:201
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Could not unfavorite"
|
||||||
|
msgstr "Não foi possível eliminar favorito"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api.ex:149
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Could not unrepeat"
|
||||||
|
msgstr "Não foi possível republicar"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api.ex:510
|
||||||
|
#: lib/pleroma/web/common_api.ex:519
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Could not update state"
|
||||||
|
msgstr "Não foi possível atualizar estado"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:278
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Error."
|
||||||
|
msgstr "Erro."
|
||||||
|
|
||||||
|
#: lib/pleroma/web/twitter_api/twitter_api.ex:104
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Invalid CAPTCHA"
|
||||||
|
msgstr "CAPTCHA inválido"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:143
|
||||||
|
#: lib/pleroma/web/o_auth/o_auth_controller.ex:660
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Invalid credentials"
|
||||||
|
msgstr "Credenciais inválidas"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/plugs/ensure_authenticated_plug.ex:42
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Invalid credentials."
|
||||||
|
msgstr "Credenciais inválidas."
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api.ex:284
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Invalid indices"
|
||||||
|
msgstr "Índices inválidos"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:29
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Invalid parameters"
|
||||||
|
msgstr "Parâmetros inválidos"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/utils.ex:376
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Invalid password."
|
||||||
|
msgstr "Senha inválida."
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:265
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Invalid request"
|
||||||
|
msgstr "Requisição inválida"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/twitter_api/twitter_api.ex:107
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Kocaptcha service unavailable"
|
||||||
|
msgstr "Serviço de Kocaptcha indisponível"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:139
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Missing parameters"
|
||||||
|
msgstr "Parâmetros faltando"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:151
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:177
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:219
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "No such permission_group"
|
||||||
|
msgstr "Não a tal permission_group"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:480
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:11
|
||||||
|
#: lib/pleroma/web/feed/tag_controller.ex:16
|
||||||
|
#: lib/pleroma/web/feed/user_controller.ex:70
|
||||||
|
#: lib/pleroma/web/o_status/o_status_controller.ex:135
|
||||||
|
#: lib/pleroma/web/plugs/uploaded_media.ex:83
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Not found"
|
||||||
|
msgstr "Não encontrado"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api.ex:255
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Poll's author can't vote"
|
||||||
|
msgstr "Autor da enquete não pode votar"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:478
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:20
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:39
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:51
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:52
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:71
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Record not found"
|
||||||
|
msgstr "Registro não encontrado"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:35
|
||||||
|
#: lib/pleroma/web/feed/user_controller.ex:79
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:42
|
||||||
|
#: lib/pleroma/web/o_status/o_status_controller.ex:141
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Something went wrong"
|
||||||
|
msgstr "Algo deu errado"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/activity_draft.ex:156
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "The message visibility must be direct"
|
||||||
|
msgstr "A visibilidade da mensagem deve ser direta"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/utils.ex:490
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "The status is over the character limit"
|
||||||
|
msgstr "O status está acima do limite de caracteres"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/plugs/ensure_public_or_authenticated_plug.ex:36
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "This resource requires authentication."
|
||||||
|
msgstr "Esse recurso requer autenticação."
|
||||||
|
|
||||||
|
#: lib/pleroma/web/plugs/rate_limiter.ex:214
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Throttled"
|
||||||
|
msgstr "Limitado"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api.ex:285
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Too many choices"
|
||||||
|
msgstr "Muitas opções"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:248
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "You can't revoke your own admin status."
|
||||||
|
msgstr "Você não pode revogar seu próprio status de administrador."
|
||||||
|
|
||||||
|
#: lib/pleroma/web/o_auth/o_auth_controller.ex:267
|
||||||
|
#: lib/pleroma/web/o_auth/o_auth_controller.ex:358
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Your account is currently disabled"
|
||||||
|
msgstr "Sua conta está atualmente desativada"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/o_auth/o_auth_controller.ex:229
|
||||||
|
#: lib/pleroma/web/o_auth/o_auth_controller.ex:381
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Your login is missing a confirmed e-mail address"
|
||||||
|
msgstr "Sua conta não possui uma endereço de email confirmado"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:368
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "can't read inbox of %{nickname} as %{as_nickname}"
|
||||||
|
msgstr "não é possível ler o inbox de %{nickname} como %{as_nickname}"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:467
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "can't update outbox of %{nickname} as %{as_nickname}"
|
||||||
|
msgstr "não é possível atualizar o inbox de %{nickname} como %{nickname}"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api.ex:455
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "conversation is already muted"
|
||||||
|
msgstr "a conversa já está silenciada"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:486
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "error"
|
||||||
|
msgstr "erro"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:34
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "mascots can only be images"
|
||||||
|
msgstr "mascotes só podem ser imagens"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:62
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "not found"
|
||||||
|
msgstr "não encontrado"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/o_auth/o_auth_controller.ex:462
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Bad OAuth request."
|
||||||
|
msgstr "Requisição de OAuth inválida."
|
||||||
|
|
||||||
|
#: lib/pleroma/web/twitter_api/twitter_api.ex:113
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "CAPTCHA already used"
|
||||||
|
msgstr "CAPTCHA já está em uso"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/twitter_api/twitter_api.ex:110
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "CAPTCHA expired"
|
||||||
|
msgstr "CAPTCHA expirado"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/plugs/uploaded_media.ex:56
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Failed"
|
||||||
|
msgstr "Falhou"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/o_auth/o_auth_controller.ex:478
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Failed to authenticate: %{message}."
|
||||||
|
msgstr "Falha ao autenticar: %{message}."
|
||||||
|
|
||||||
|
#: lib/pleroma/web/o_auth/o_auth_controller.ex:509
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Failed to set up user account."
|
||||||
|
msgstr "Falha ao definir conta de usuário."
|
||||||
|
|
||||||
|
#: lib/pleroma/web/plugs/o_auth_scopes_plug.ex:37
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Insufficient permissions: %{permissions}."
|
||||||
|
msgstr "Permissões insuficientes: %{permissions}."
|
||||||
|
|
||||||
|
#: lib/pleroma/web/plugs/uploaded_media.ex:98
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Internal Error"
|
||||||
|
msgstr "Erro Interno"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/o_auth/fallback_controller.ex:22
|
||||||
|
#: lib/pleroma/web/o_auth/fallback_controller.ex:29
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Invalid Username/Password"
|
||||||
|
msgstr "Usuário/Senha Inválidos"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/twitter_api/twitter_api.ex:116
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Invalid answer data"
|
||||||
|
msgstr "dado de resposta inválido"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:40
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Nodeinfo schema version not handled"
|
||||||
|
msgstr "Esquema de versão do Nodeinfo não tratado"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/o_auth/fallback_controller.ex:14
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Unknown error, please check the details and try again."
|
||||||
|
msgstr "Erro desconhecido. Por favor, cheque os detalhes e tente novamente."
|
||||||
|
|
||||||
|
#: lib/pleroma/web/o_auth/o_auth_controller.ex:158
|
||||||
|
#: lib/pleroma/web/o_auth/o_auth_controller.ex:204
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Unlisted redirect_uri."
|
||||||
|
msgstr "redirect_uri Não listada."
|
||||||
|
|
||||||
|
#: lib/pleroma/web/o_auth/o_auth_controller.ex:458
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Unsupported OAuth provider: %{provider}."
|
||||||
|
msgstr "Provedor de OAuth não suportado: %{provider}."
|
||||||
|
|
||||||
|
#: lib/pleroma/uploaders/uploader.ex:74
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Uploader callback timeout"
|
||||||
|
msgstr "Tempo esgotado para callback do uploader"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/uploader_controller.ex:23
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "bad request"
|
||||||
|
msgstr "má requisição"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/twitter_api/twitter_api.ex:101
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "CAPTCHA Error"
|
||||||
|
msgstr "Erro no CAPTCHA"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api.ex:213
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Could not add reaction emoji"
|
||||||
|
msgstr "Não foi possível adicionar emoji de reação"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api.ex:224
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Could not remove reaction emoji"
|
||||||
|
msgstr "Não foi possível remover emoji de reação"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/twitter_api/twitter_api.ex:127
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Invalid CAPTCHA (Missing parameter: %{name})"
|
||||||
|
msgstr "CAPTCHA inválido (Parâmetro faltando: %{name})"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:96
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "List not found"
|
||||||
|
msgstr "Lista não encontrada"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:150
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Missing parameter: %{name}"
|
||||||
|
msgstr "Parâmetro faltando: %{name}"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/o_auth/o_auth_controller.ex:256
|
||||||
|
#: lib/pleroma/web/o_auth/o_auth_controller.ex:371
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Password reset is required"
|
||||||
|
msgstr "Redefinição de senha é necessária"
|
||||||
|
|
||||||
|
#: 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/announcement_controller.ex:6
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/config_controller.ex:6
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:6
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/frontend_controller.ex:6
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/instance_controller.ex:6
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/instance_document_controller.ex:6
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/invite_controller.ex:6
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex:6
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex:6
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/relay_controller.ex:6
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/report_controller.ex:6
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/status_controller.ex:6
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/user_controller.ex:6
|
||||||
|
#: lib/pleroma/web/akkoma_api/controllers/frontend_settings_controller.ex:2
|
||||||
|
#: lib/pleroma/web/akkoma_api/controllers/frontend_switcher.ex:2
|
||||||
|
#: lib/pleroma/web/akkoma_api/controllers/metrics_controller.ex:2
|
||||||
|
#: lib/pleroma/web/akkoma_api/controllers/translation_controller.ex:2
|
||||||
|
#: lib/pleroma/web/controller_helper.ex:6
|
||||||
|
#: lib/pleroma/web/embed_controller.ex:6
|
||||||
|
#: lib/pleroma/web/fallback/redirect_controller.ex:6
|
||||||
|
#: lib/pleroma/web/feed/tag_controller.ex:6
|
||||||
|
#: lib/pleroma/web/feed/user_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mailer/subscription_controller.ex:6
|
||||||
|
#: lib/pleroma/web/manifest_controller.ex:6
|
||||||
|
#: lib/pleroma/web/masto_fe_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/announcement_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/app_controller.ex:11
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/auth_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/directory_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/filter_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/instance_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/marker_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex:14
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/media_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/notification_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/report_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/search_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/status_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:7
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/tag_controller.ex:3
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:6
|
||||||
|
#: lib/pleroma/web/media_proxy/media_proxy_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mongoose_im/mongoose_im_controller.ex:6
|
||||||
|
#: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:6
|
||||||
|
#: lib/pleroma/web/o_auth/fallback_controller.ex:6
|
||||||
|
#: lib/pleroma/web/o_auth/mfa_controller.ex:10
|
||||||
|
#: lib/pleroma/web/o_auth/o_auth_controller.ex:6
|
||||||
|
#: lib/pleroma/web/o_status/o_status_controller.ex:6
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:6
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/app_controller.ex:6
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/backup_controller.ex:6
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex:6
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex:6
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:6
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex:6
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/instances_controller.ex:6
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:6
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/notification_controller.ex:6
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/report_controller.ex:6
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex:7
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex:6
|
||||||
|
#: lib/pleroma/web/static_fe/static_fe_controller.ex:6
|
||||||
|
#: lib/pleroma/web/twitter_api/controller.ex:6
|
||||||
|
#: lib/pleroma/web/twitter_api/controllers/password_controller.ex:10
|
||||||
|
#: lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex:6
|
||||||
|
#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:6
|
||||||
|
#: lib/pleroma/web/uploader_controller.ex:6
|
||||||
|
#: lib/pleroma/web/web_finger/web_finger_controller.ex:6
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Security violation: OAuth scopes check was neither handled nor explicitly skipped."
|
||||||
|
msgstr ""
|
||||||
|
"Violação de segurança: Verificação do escopo do OAuth tanto não lidou quanto "
|
||||||
|
"não explicitamente pulou."
|
||||||
|
|
||||||
|
#: lib/pleroma/web/plugs/ensure_authenticated_plug.ex:32
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Two-factor authentication enabled, you must use a access token."
|
||||||
|
msgstr ""
|
||||||
|
"Autenticação de dois-fatores ativada, você deve usar um token de acesso."
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Web push subscription is disabled on this Pleroma instance"
|
||||||
|
msgstr "Inscrição de web push está desativada nessa instância do Akkoma"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:214
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "You can't revoke your own admin/moderator status."
|
||||||
|
msgstr "Você não pode revogar o seu próprio status de admin/moderador."
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:202
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "authorization required for timeline view"
|
||||||
|
msgstr "autorização necessária para visualização da linha do tempo"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:24
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Access denied"
|
||||||
|
msgstr "Acesso negado"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:332
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "This API requires an authenticated user"
|
||||||
|
msgstr "Essa API necessita de um usuário autentica"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/plugs/ensure_staff_privileged_plug.ex:26
|
||||||
|
#: lib/pleroma/web/plugs/user_is_admin_plug.ex:21
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "User is not an admin."
|
||||||
|
msgstr "Usuário não é um administrador."
|
||||||
|
|
||||||
|
#: lib/pleroma/user/backup.ex:73
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Last export was less than a day ago"
|
||||||
|
msgid_plural "Last export was less than %{days} days ago"
|
||||||
|
msgstr[0] "Última exportação foi a menos de um dia atrás"
|
||||||
|
msgstr[1] "Última exportação foi a menos de %{days} atrás"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:399
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Character limit (%{limit} characters) exceeded, contains %{length} characters"
|
||||||
|
msgstr ""
|
||||||
|
"Limite de caracteres (%{limit} caracteres) excedido, pois contém %{length} "
|
||||||
|
"caracteres"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/plugs/ensure_staff_privileged_plug.ex:33
|
||||||
|
#: lib/pleroma/web/plugs/user_is_staff_plug.ex:20
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "User is not a staff member."
|
||||||
|
msgstr "Usuário não é um membro da staff."
|
||||||
|
|
||||||
|
#: lib/pleroma/web/o_auth/o_auth_controller.ex:391
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Your account is awaiting approval."
|
||||||
|
msgstr "Sua conta aguarda aprovação."
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:256
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:259
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:262
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "File is too large"
|
||||||
|
msgstr "Arquivo muito grande"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/tag_controller.ex:37
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/tag_controller.ex:48
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/tag_controller.ex:59
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Hashtag not found"
|
||||||
|
msgstr "Hashtag não encontrada"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/activity_draft.ex:144
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Invalid language"
|
||||||
|
msgstr "Idioma inválido"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/o_auth/o_auth_controller.ex:218
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "This action is outside of authorized scopes"
|
||||||
|
msgstr "Essa ação está fora do escopo autorizado"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/activity_draft.ex:129
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "You can only quote public or unlisted statuses"
|
||||||
|
msgstr "Você pode apenas citar status públicos ou não-listados"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/activity_draft.ex:126
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "You can't quote a status that doesn't exist"
|
||||||
|
msgstr "Você não pode citar um status que não existe"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/embed_controller.ex:35
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Federated posts cannot be embedded"
|
||||||
|
msgstr "Publicações federadas não podem ser embutidas"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/embed_controller.ex:38
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Not authorized to view this post"
|
||||||
|
msgstr "Não autorizado a ver essa publicação"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/embed_controller.ex:32
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Post not found"
|
||||||
|
msgstr "Publicação não encontrada"
|
163
priv/gettext/pt/LC_MESSAGES/posix_errors.po
Normal file
163
priv/gettext/pt/LC_MESSAGES/posix_errors.po
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2024-03-17 22:50+0000\n"
|
||||||
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
|
"Last-Translator: Automatically generated\n"
|
||||||
|
"Language-Team: none\n"
|
||||||
|
"Language: pt\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"X-Generator: Translate Toolkit 3.9.2\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.
|
||||||
|
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 ""
|
616
priv/gettext/pt/LC_MESSAGES/static_pages.po
Normal file
616
priv/gettext/pt/LC_MESSAGES/static_pages.po
Normal file
|
@ -0,0 +1,616 @@
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2024-03-17 22:51+0000\n"
|
||||||
|
"PO-Revision-Date: 2024-03-19 01:10+0000\n"
|
||||||
|
"Last-Translator: Jammer Lammer <akHarINlMYExpSmVPDRT@proton.me>\n"
|
||||||
|
"Language-Team: Portuguese <http://translate.akkoma.dev/projects/akkoma/"
|
||||||
|
"akkoma-backend-static-pages/pt/>\n"
|
||||||
|
"Language: pt\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=n > 1;\n"
|
||||||
|
"X-Generator: Weblate 4.18.2\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.
|
||||||
|
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:9
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "remote follow authorization button"
|
||||||
|
msgid "Authorize"
|
||||||
|
msgstr "Autorizar"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:2
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "remote follow error"
|
||||||
|
msgid "Error fetching user"
|
||||||
|
msgstr "Erro ao buscar usuário"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:4
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "remote follow header"
|
||||||
|
msgid "Remote follow"
|
||||||
|
msgstr "Seguimento remoto"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:8
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "placeholder text for auth code entry"
|
||||||
|
msgid "Authentication code"
|
||||||
|
msgstr "Código de autenticação"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:10
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "placeholder text for password entry"
|
||||||
|
msgid "Password"
|
||||||
|
msgstr "Senha"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:8
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "placeholder text for username entry"
|
||||||
|
msgid "Username"
|
||||||
|
msgstr "Nome de usuário"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:13
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "remote follow authorization button for login"
|
||||||
|
msgid "Authorize"
|
||||||
|
msgstr "Autorizar"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:12
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "remote follow authorization button for mfa"
|
||||||
|
msgid "Authorize"
|
||||||
|
msgstr "Autorizar"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:2
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "remote follow error"
|
||||||
|
msgid "Error following account"
|
||||||
|
msgstr "Erro ao seguir conta"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:4
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "remote follow header, need login"
|
||||||
|
msgid "Log in to follow"
|
||||||
|
msgstr "Inicia a sessão para seguir"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:4
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "remote follow mfa header"
|
||||||
|
msgid "Two-factor authentication"
|
||||||
|
msgstr "Autenticação de dois-fatores"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:4
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "remote follow success"
|
||||||
|
msgid "Account followed!"
|
||||||
|
msgstr "Conta seguida!"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:7
|
||||||
|
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:7
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "placeholder text for account id"
|
||||||
|
msgid "Your account ID, e.g. lain@quitter.se"
|
||||||
|
msgstr "Sua ID de conta, ex. lain@quitter.se"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:8
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "remote follow authorization button for following with a remote account"
|
||||||
|
msgid "Follow"
|
||||||
|
msgstr "Seguir"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:2
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "remote follow error"
|
||||||
|
msgid "Error: %{error}"
|
||||||
|
msgstr "Erro: %{error}"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:4
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "remote follow header"
|
||||||
|
msgid "Remotely follow %{nickname}"
|
||||||
|
msgstr "Seguir remotamente %{nickname}"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:12
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "password reset button"
|
||||||
|
msgid "Reset"
|
||||||
|
msgstr "Resetar"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:4
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "password reset failed homepage link"
|
||||||
|
msgid "Homepage"
|
||||||
|
msgstr "Página inicial"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:1
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "password reset failed message"
|
||||||
|
msgid "Password reset failed"
|
||||||
|
msgstr "Falha ao redefinir senha"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:8
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "password reset form confirm password prompt"
|
||||||
|
msgid "Confirmation"
|
||||||
|
msgstr "Confirmação"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:4
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "password reset form password prompt"
|
||||||
|
msgid "Password"
|
||||||
|
msgstr "Senha"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex:1
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "password reset invalid token message"
|
||||||
|
msgid "Invalid Token"
|
||||||
|
msgstr "Token Inválido"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:2
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "password reset successful homepage link"
|
||||||
|
msgid "Homepage"
|
||||||
|
msgstr "Página inicial"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:1
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "password reset successful message"
|
||||||
|
msgid "Password changed!"
|
||||||
|
msgstr "Senha alterada!"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/feed/feed/tag.atom.eex:15
|
||||||
|
#: lib/pleroma/web/templates/feed/feed/tag.rss.eex:7
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "tag feed description"
|
||||||
|
msgid "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse."
|
||||||
|
msgstr ""
|
||||||
|
"Estas são notas públicas marcadas com #%{tag}. Você pode interagir com elas "
|
||||||
|
"se você tem uma conta em qualquer lugar no fediverse."
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:3
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "oauth authorization exists page title"
|
||||||
|
msgid "Authorization exists"
|
||||||
|
msgstr "Existe autorização"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:37
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "oauth authorize approve button"
|
||||||
|
msgid "Approve"
|
||||||
|
msgstr "Aprovar"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:35
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "oauth authorize cancel button"
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr "Cancelar"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:26
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "oauth authorize message"
|
||||||
|
msgid "Application <strong>%{client_name}</strong> is requesting access to your account."
|
||||||
|
msgstr ""
|
||||||
|
"A aplicação <strong>%{client_name}</strong> está requisitando acesso à sua "
|
||||||
|
"conta."
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:3
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "oauth authorized page title"
|
||||||
|
msgid "Successfully authorized"
|
||||||
|
msgstr "Autorização feita com sucesso"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:1
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "oauth external provider page title"
|
||||||
|
msgid "Sign in with external provider"
|
||||||
|
msgstr "Iniciar sessão com provedor externo"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:13
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "oauth external provider sign in button"
|
||||||
|
msgid "Sign in with %{strategy}"
|
||||||
|
msgstr "Iniciar sessão com %{strategy}"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:59
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "oauth login button"
|
||||||
|
msgid "Log In"
|
||||||
|
msgstr "Iniciar sessão"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:56
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "oauth login password prompt"
|
||||||
|
msgid "Password"
|
||||||
|
msgstr "Senha"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:52
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "oauth login username prompt"
|
||||||
|
msgid "Username"
|
||||||
|
msgstr "Usuário"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:44
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "oauth register nickname prompt"
|
||||||
|
msgid "Pleroma Handle"
|
||||||
|
msgstr "Usuário Pleroma"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:42
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "oauth register nickname unchangeable warning"
|
||||||
|
msgid "Choose carefully! You won't be able to change this later. You will be able to change your display name, though."
|
||||||
|
msgstr ""
|
||||||
|
"Escolha com cautela! Você não será capaz de mudar isso depois. No entanto, "
|
||||||
|
"você será capaz de mudar o seu nome à mostra."
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:18
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "oauth register page email prompt"
|
||||||
|
msgid "Email"
|
||||||
|
msgstr "Email"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:10
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "oauth register page fill form prompt"
|
||||||
|
msgid "If you'd like to register a new account, please provide the details below."
|
||||||
|
msgstr ""
|
||||||
|
"Se você deseja registrar uma nova conta, por favor, provenha os detalhes "
|
||||||
|
"abaixo."
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:35
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "oauth register page login button"
|
||||||
|
msgid "Proceed as existing user"
|
||||||
|
msgstr "Proceder como um usuário já existente"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:31
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "oauth register page login password prompt"
|
||||||
|
msgid "Password"
|
||||||
|
msgstr "Senha"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:24
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "oauth register page login prompt"
|
||||||
|
msgid "Alternatively, sign in to connect to existing account."
|
||||||
|
msgstr "Alternativamente, inicia uma sessão em uma conta existente."
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:27
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "oauth register page login username prompt"
|
||||||
|
msgid "Name or email"
|
||||||
|
msgstr "Nome ou email"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:14
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "oauth register page nickname prompt"
|
||||||
|
msgid "Nickname"
|
||||||
|
msgstr "Apelido"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:22
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "oauth register page register button"
|
||||||
|
msgid "Proceed as new user"
|
||||||
|
msgstr "Proceder como um novo usuário"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:8
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "oauth register page title"
|
||||||
|
msgid "Registration Details"
|
||||||
|
msgstr "Detalhes do registro"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex:2
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "oauth scopes message"
|
||||||
|
msgid "The following permissions will be granted"
|
||||||
|
msgstr "As seguintes permissões serão garantidas"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:6
|
||||||
|
#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:6
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "oauth token code message"
|
||||||
|
msgid "Token code is <br>%{token}"
|
||||||
|
msgstr "O código do token é <br>%{token}"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:14
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "mfa auth code prompt"
|
||||||
|
msgid "Authentication code"
|
||||||
|
msgstr "Código de autenticação"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:9
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "mfa auth page title"
|
||||||
|
msgid "Two-factor authentication"
|
||||||
|
msgstr "Autenticação de dois-fatores"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:25
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "mfa auth page use recovery code link"
|
||||||
|
msgid "Enter a two-factor recovery code"
|
||||||
|
msgstr "Insira um código de recuperação de dois-fatores"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:22
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "mfa auth verify code button"
|
||||||
|
msgid "Verify"
|
||||||
|
msgstr "Verificar"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:9
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "mfa recover page title"
|
||||||
|
msgid "Two-factor recovery"
|
||||||
|
msgstr "Recuperação de dois-fatores"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:14
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "mfa recover recovery code prompt"
|
||||||
|
msgid "Recovery code"
|
||||||
|
msgstr "Código de recuperação"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:25
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "mfa recover use 2fa code link"
|
||||||
|
msgid "Enter a two-factor code"
|
||||||
|
msgstr "Insira um código de dois-fatores"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:22
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "mfa recover verify recovery code button"
|
||||||
|
msgid "Verify"
|
||||||
|
msgstr "Verificar"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex:42
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "static fe profile page remote follow button"
|
||||||
|
msgid "Remote follow"
|
||||||
|
msgstr "Seguir remotamente"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/email/digest.html.eex:163
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "digest email header line"
|
||||||
|
msgid "Hey %{nickname}, here is what you've missed!"
|
||||||
|
msgstr "Ei, %{nickname}, veja o que você perdeu!"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/email/digest.html.eex:544
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "digest email receiver address"
|
||||||
|
msgid "The email address you are subscribed as is <a href='mailto:%{@user.email}' style='color: %{color};text-decoration: none;'>%{email}</a>. "
|
||||||
|
msgstr ""
|
||||||
|
"O email ao qual você está inscrito é <a href='mailto:%{@user.email}' "
|
||||||
|
"style='color:%{color};text-decoration:none;'>%{email}</a>. "
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/email/digest.html.eex:538
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "digest email sending reason"
|
||||||
|
msgid "You have received this email because you have signed up to receive digest emails from <b>%{instance}</b> Pleroma instance."
|
||||||
|
msgstr ""
|
||||||
|
"Você recebeu esse email porque você se inscreveu para receber resumos por "
|
||||||
|
"email da instância Akkoma <b>%{instance}</b>."
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/email/digest.html.eex:547
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "digest email unsubscribe action"
|
||||||
|
msgid "To unsubscribe, please go %{here}."
|
||||||
|
msgstr "Para se desinscrever, por favor vá %{here}."
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/email/digest.html.eex:547
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "digest email unsubscribe action link text"
|
||||||
|
msgid "here"
|
||||||
|
msgstr "aqui"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_failure.html.eex:1
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "mailer unsubscribe failed message"
|
||||||
|
msgid "UNSUBSCRIBE FAILURE"
|
||||||
|
msgstr "FALHA AO SE DESINSCREVER"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_success.html.eex:1
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "mailer unsubscribe successful message"
|
||||||
|
msgid "UNSUBSCRIBE SUCCESSFUL"
|
||||||
|
msgstr "INSCRIÇÃO CANCELADA COM SUCESSO"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/email/digest.html.eex:385
|
||||||
|
#, elixir-format
|
||||||
|
msgctxt "new followers count header"
|
||||||
|
msgid "%{count} New Follower"
|
||||||
|
msgid_plural "%{count} New Followers"
|
||||||
|
msgstr[0] "%{count} Novo Seguidor"
|
||||||
|
msgstr[1] "%{count} Novos Seguidores"
|
||||||
|
|
||||||
|
#: lib/pleroma/emails/user_email.ex:384
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "account archive email subject"
|
||||||
|
msgid "Your account archive is ready"
|
||||||
|
msgstr "O arquivamento da sua conta está pronto"
|
||||||
|
|
||||||
|
#: lib/pleroma/emails/user_email.ex:188
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "approval pending email body"
|
||||||
|
msgid "<h3>Awaiting Approval</h3>\n<p>Your account at %{instance_name} is being reviewed by staff. You will receive another email once your account is approved.</p>\n"
|
||||||
|
msgstr ""
|
||||||
|
"<h3> Aguardando Aprovação </h3>\n"
|
||||||
|
"<p>Sua conta na instância %{instance_name} está sendo revisada pela staff. "
|
||||||
|
"Você receberá um novo email assim que a conta for aprovada. </p>\n"
|
||||||
|
|
||||||
|
#: lib/pleroma/emails/user_email.ex:202
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "approval pending email subject"
|
||||||
|
msgid "Your account is awaiting approval"
|
||||||
|
msgstr "Sua conta está aguardando aprovação"
|
||||||
|
|
||||||
|
#: lib/pleroma/emails/user_email.ex:158
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "confirmation email body"
|
||||||
|
msgid "<h3>Thank you for registering on %{instance_name}</h3>\n<p>Email confirmation is required to activate the account.</p>\n<p>Please click the following link to <a href=\"%{confirmation_url}\">activate your account</a>.</p>\n"
|
||||||
|
msgstr ""
|
||||||
|
"<h3> Obrigado por se registrar na instância %{instance_name}</h3>\n"
|
||||||
|
"<p> Uma confirmação de email é necessário para ativar a sua conta </p>\n"
|
||||||
|
"<p>Por favor, clique no seguinte link para <a href=\"%{confirmation_url}\""
|
||||||
|
">ativar a sua conta</a>.</p>\n"
|
||||||
|
|
||||||
|
#: lib/pleroma/emails/user_email.ex:174
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "confirmation email subject"
|
||||||
|
msgid "%{instance_name} account confirmation"
|
||||||
|
msgstr "Confirmação de conta de %{instance_name}"
|
||||||
|
|
||||||
|
#: lib/pleroma/emails/user_email.ex:310
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "digest email subject"
|
||||||
|
msgid "Your digest from %{instance_name}"
|
||||||
|
msgstr "O seu resumo de %{instance_name}"
|
||||||
|
|
||||||
|
#: lib/pleroma/emails/user_email.ex:81
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "password reset email body"
|
||||||
|
msgid "<h3>Reset your password at %{instance_name}</h3>\n<p>Someone has requested password change for your account at %{instance_name}.</p>\n<p>If it was you, visit the following link to proceed: <a href=\"%{password_reset_url}\">reset password</a>.</p>\n<p>If it was someone else, nothing to worry about: your data is secure and your password has not been changed.</p>\n"
|
||||||
|
msgstr ""
|
||||||
|
"<h3>Redefina a sua senha em %{instance_name}</h3>\n"
|
||||||
|
"<p>Alguém requisitou uma redefinição de senha para a sua conta em "
|
||||||
|
"%{instance_name}.</p>\n"
|
||||||
|
"<p>Caso tenha sido você, visite o seguinte link para proceder: <a href=\""
|
||||||
|
"%{passoword_reset_url}\">redefinir senha</a>.</p>\n"
|
||||||
|
"<p>Se por acaso tenha sido outra pessoa, não há com o que se preocupar: seus "
|
||||||
|
"dados estão seguros e a sua senha não foi alterada.</p>\n"
|
||||||
|
|
||||||
|
#: lib/pleroma/emails/user_email.ex:98
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "password reset email subject"
|
||||||
|
msgid "Password reset"
|
||||||
|
msgstr "Senha redefinida"
|
||||||
|
|
||||||
|
#: lib/pleroma/emails/user_email.ex:215
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "successful registration email body"
|
||||||
|
msgid "<h3>Hello @%{nickname},</h3>\n<p>Your account at %{instance_name} has been registered successfully.</p>\n<p>No further action is required to activate your account.</p>\n"
|
||||||
|
msgstr ""
|
||||||
|
"<h3>Olá, @%{nickname},</h3>\n"
|
||||||
|
"<p>Sua conta em %{instance_name} foi criada com sucesso.</p>\n"
|
||||||
|
"<p>Nenhuma ação extra é necessária para ativar a sua conta.</p>\n"
|
||||||
|
|
||||||
|
#: lib/pleroma/emails/user_email.ex:231
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "successful registration email subject"
|
||||||
|
msgid "Account registered on %{instance_name}"
|
||||||
|
msgstr "Conta registrada em %{instance_name}"
|
||||||
|
|
||||||
|
#: lib/pleroma/emails/user_email.ex:136
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "user invitation email subject"
|
||||||
|
msgid "Invitation to %{instance_name}"
|
||||||
|
msgstr "Convite para %{instance_name}"
|
||||||
|
|
||||||
|
#: lib/pleroma/emails/user_email.ex:53
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "welcome email html body"
|
||||||
|
msgid "Welcome to %{instance_name}!"
|
||||||
|
msgstr "Bem-vindo(a) à instância %{instance_name}!"
|
||||||
|
|
||||||
|
#: lib/pleroma/emails/user_email.ex:41
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "welcome email subject"
|
||||||
|
msgid "Welcome to %{instance_name}!"
|
||||||
|
msgstr "Bem-vindo(a) à instância %{instance_name}!"
|
||||||
|
|
||||||
|
#: lib/pleroma/emails/user_email.ex:65
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "welcome email text body"
|
||||||
|
msgid "Welcome to %{instance_name}!"
|
||||||
|
msgstr "Bem-vindo(a) à instância %{instance_name}!"
|
||||||
|
|
||||||
|
#: lib/pleroma/emails/user_email.ex:368
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "account archive email body - admin requested"
|
||||||
|
msgid "<p>Admin @%{admin_nickname} requested a full backup of your Akkoma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
|
||||||
|
msgstr ""
|
||||||
|
"<p>O administrador @%{admin_nickname} requisitou um backup completo da sua "
|
||||||
|
"conta Akkoma. Ele está pronto para download:</p>\n"
|
||||||
|
"<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
|
||||||
|
|
||||||
|
#: lib/pleroma/emails/user_email.ex:356
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "account archive email body - self-requested"
|
||||||
|
msgid "<p>You requested a full backup of your Akkoma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
|
||||||
|
msgstr ""
|
||||||
|
"<p> Foi requisitou uma backup completo da sua conta Akkoma. Está pronto para "
|
||||||
|
"download:</p>\n"
|
||||||
|
"<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:41
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "oauth register page title"
|
||||||
|
msgid "This is your first visit! Please enter your Akkoma handle."
|
||||||
|
msgstr ""
|
||||||
|
"Esta é a sua primeira visita! Por favor, entre com o seu sobrenome Akkoma."
|
||||||
|
|
||||||
|
#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:123
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "remote follow error message - unknown error"
|
||||||
|
msgid "Something went wrong."
|
||||||
|
msgstr "Algo deu errado."
|
||||||
|
|
||||||
|
#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:67
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "remote follow error message - user not found"
|
||||||
|
msgid "Could not find user"
|
||||||
|
msgstr "Não foi possível encontrar o usuário"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:8
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "status interact authorization button"
|
||||||
|
msgid "Interact"
|
||||||
|
msgstr "Interagir"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:2
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "status interact error"
|
||||||
|
msgid "Error: %{error}"
|
||||||
|
msgstr "Erro: %{error}"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:95
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "status interact error message - status not found"
|
||||||
|
msgid "Could not find status"
|
||||||
|
msgstr "Não foi possível achar o status"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:144
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "status interact error message - unknown error"
|
||||||
|
msgid "Something went wrong."
|
||||||
|
msgstr "Algo deu errado."
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:4
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "status interact header"
|
||||||
|
msgid "Interacting with %{nickname}'s %{status_link}"
|
||||||
|
msgstr "Interagindo com o %{status_link} de %{nickname}"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:4
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "status interact header - status link text"
|
||||||
|
msgid "status"
|
||||||
|
msgstr "status"
|
||||||
|
|
||||||
|
#: lib/pleroma/emails/user_email.ex:119
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgctxt "user invitation email body"
|
||||||
|
msgid "<h3>You are invited to %{instance_name}</h3>\n<p>%{inviter_name} invites you to join %{instance_name}, an instance of Akkoma federated social networking platform.</p>\n<p>Click the following link to register: <a href=\"%{registration_url}\">accept invitation</a>.</p>\n"
|
||||||
|
msgstr ""
|
||||||
|
"<h3> Você foi convidado(a) para a instância %{instance_name}</h3>\n"
|
||||||
|
"<p>%{inviter_name} lhe convidou a se juntar à instância %{instance_name}, "
|
||||||
|
"uma instância Akkoma da plataforma de redes sociais federadas.</p>\n"
|
||||||
|
"<p>Clique no seguinte link para se registrar: <a href=\"%{registration_url}\""
|
||||||
|
">aceitar convite</a>.</p>\n"
|
|
@ -8,555 +8,589 @@
|
||||||
### to merge POT files into PO files.
|
### to merge POT files into PO files.
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"PO-Revision-Date: 2024-04-22 23:54+0000\n"
|
||||||
|
"Last-Translator: Toot <toothpicker@users.noreply.translate.akkoma.dev>\n"
|
||||||
|
"Language-Team: Chinese (Traditional) <http://translate.akkoma.dev/projects/"
|
||||||
|
"akkoma/akkoma-backend-static-pages/zh_Hant/>\n"
|
||||||
"Language: zh_Hant\n"
|
"Language: zh_Hant\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||||
|
"X-Generator: Weblate 4.18.2\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:9
|
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:9
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "remote follow authorization button"
|
msgctxt "remote follow authorization button"
|
||||||
msgid "Authorize"
|
msgid "Authorize"
|
||||||
msgstr ""
|
msgstr "批准"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:2
|
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:2
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "remote follow error"
|
msgctxt "remote follow error"
|
||||||
msgid "Error fetching user"
|
msgid "Error fetching user"
|
||||||
msgstr ""
|
msgstr "無法獲取用戶信息"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:4
|
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:4
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "remote follow header"
|
msgctxt "remote follow header"
|
||||||
msgid "Remote follow"
|
msgid "Remote follow"
|
||||||
msgstr ""
|
msgstr "跨站關注"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:8
|
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:8
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "placeholder text for auth code entry"
|
msgctxt "placeholder text for auth code entry"
|
||||||
msgid "Authentication code"
|
msgid "Authentication code"
|
||||||
msgstr ""
|
msgstr "驗證碼"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:10
|
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:10
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "placeholder text for password entry"
|
msgctxt "placeholder text for password entry"
|
||||||
msgid "Password"
|
msgid "Password"
|
||||||
msgstr ""
|
msgstr "密碼"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:8
|
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:8
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "placeholder text for username entry"
|
msgctxt "placeholder text for username entry"
|
||||||
msgid "Username"
|
msgid "Username"
|
||||||
msgstr ""
|
msgstr "用戶名"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:13
|
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:13
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "remote follow authorization button for login"
|
msgctxt "remote follow authorization button for login"
|
||||||
msgid "Authorize"
|
msgid "Authorize"
|
||||||
msgstr ""
|
msgstr "授權"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:12
|
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:12
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "remote follow authorization button for mfa"
|
msgctxt "remote follow authorization button for mfa"
|
||||||
msgid "Authorize"
|
msgid "Authorize"
|
||||||
msgstr ""
|
msgstr "授權"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:2
|
#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:2
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "remote follow error"
|
msgctxt "remote follow error"
|
||||||
msgid "Error following account"
|
msgid "Error following account"
|
||||||
msgstr ""
|
msgstr "無法關注帳戶"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:4
|
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:4
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "remote follow header, need login"
|
msgctxt "remote follow header, need login"
|
||||||
msgid "Log in to follow"
|
msgid "Log in to follow"
|
||||||
msgstr ""
|
msgstr "登錄以關注"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:4
|
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:4
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "remote follow mfa header"
|
msgctxt "remote follow mfa header"
|
||||||
msgid "Two-factor authentication"
|
msgid "Two-factor authentication"
|
||||||
msgstr ""
|
msgstr "雙因素身份驗證"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:4
|
#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:4
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "remote follow success"
|
msgctxt "remote follow success"
|
||||||
msgid "Account followed!"
|
msgid "Account followed!"
|
||||||
msgstr ""
|
msgstr "已關注該賬號!"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:7
|
#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:7
|
||||||
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:7
|
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:7
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "placeholder text for account id"
|
msgctxt "placeholder text for account id"
|
||||||
msgid "Your account ID, e.g. lain@quitter.se"
|
msgid "Your account ID, e.g. lain@quitter.se"
|
||||||
msgstr ""
|
msgstr "你的帳號ID,比如:lain@quitter.se"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:8
|
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:8
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "remote follow authorization button for following with a remote account"
|
msgctxt "remote follow authorization button for following with a remote account"
|
||||||
msgid "Follow"
|
msgid "Follow"
|
||||||
msgstr ""
|
msgstr "關注"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:2
|
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:2
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "remote follow error"
|
msgctxt "remote follow error"
|
||||||
msgid "Error: %{error}"
|
msgid "Error: %{error}"
|
||||||
msgstr ""
|
msgstr "錯誤:%{error}"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:4
|
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:4
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "remote follow header"
|
msgctxt "remote follow header"
|
||||||
msgid "Remotely follow %{nickname}"
|
msgid "Remotely follow %{nickname}"
|
||||||
msgstr ""
|
msgstr "遠程關注 %{nickname}"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:12
|
#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:12
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "password reset button"
|
msgctxt "password reset button"
|
||||||
msgid "Reset"
|
msgid "Reset"
|
||||||
msgstr ""
|
msgstr "重設"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:4
|
#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:4
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "password reset failed homepage link"
|
msgctxt "password reset failed homepage link"
|
||||||
msgid "Homepage"
|
msgid "Homepage"
|
||||||
msgstr ""
|
msgstr "主頁"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:1
|
#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:1
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "password reset failed message"
|
msgctxt "password reset failed message"
|
||||||
msgid "Password reset failed"
|
msgid "Password reset failed"
|
||||||
msgstr ""
|
msgstr "密碼重置失敗"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:8
|
#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:8
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "password reset form confirm password prompt"
|
msgctxt "password reset form confirm password prompt"
|
||||||
msgid "Confirmation"
|
msgid "Confirmation"
|
||||||
msgstr ""
|
msgstr "確認"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:4
|
#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:4
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "password reset form password prompt"
|
msgctxt "password reset form password prompt"
|
||||||
msgid "Password"
|
msgid "Password"
|
||||||
msgstr ""
|
msgstr "密碼"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex:1
|
#: lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex:1
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "password reset invalid token message"
|
msgctxt "password reset invalid token message"
|
||||||
msgid "Invalid Token"
|
msgid "Invalid Token"
|
||||||
msgstr ""
|
msgstr "無效 Token"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:2
|
#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:2
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "password reset successful homepage link"
|
msgctxt "password reset successful homepage link"
|
||||||
msgid "Homepage"
|
msgid "Homepage"
|
||||||
msgstr ""
|
msgstr "主頁"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:1
|
#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:1
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "password reset successful message"
|
msgctxt "password reset successful message"
|
||||||
msgid "Password changed!"
|
msgid "Password changed!"
|
||||||
msgstr ""
|
msgstr "密碼已修改!"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/feed/feed/tag.atom.eex:15
|
#: lib/pleroma/web/templates/feed/feed/tag.atom.eex:15
|
||||||
#: lib/pleroma/web/templates/feed/feed/tag.rss.eex:7
|
#: lib/pleroma/web/templates/feed/feed/tag.rss.eex:7
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "tag feed description"
|
msgctxt "tag feed description"
|
||||||
msgid "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse."
|
msgid "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse."
|
||||||
msgstr ""
|
msgstr "這些是帶有 #%{tag} "
|
||||||
|
"標籤的公開貼文。如果你在聯邦宇宙的任何地方有帳號,你可以與它們進行互動。"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:3
|
#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:3
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "oauth authorization exists page title"
|
msgctxt "oauth authorization exists page title"
|
||||||
msgid "Authorization exists"
|
msgid "Authorization exists"
|
||||||
msgstr ""
|
msgstr "已存在授權"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:37
|
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:37
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "oauth authorize approve button"
|
msgctxt "oauth authorize approve button"
|
||||||
msgid "Approve"
|
msgid "Approve"
|
||||||
msgstr ""
|
msgstr "允許"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:35
|
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:35
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "oauth authorize cancel button"
|
msgctxt "oauth authorize cancel button"
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr ""
|
msgstr "取消"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:26
|
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:26
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "oauth authorize message"
|
msgctxt "oauth authorize message"
|
||||||
msgid "Application <strong>%{client_name}</strong> is requesting access to your account."
|
msgid "Application <strong>%{client_name}</strong> is requesting access to your account."
|
||||||
msgstr ""
|
msgstr "應用程序 <strong>%{client_name}</strong> 正在請求訪問您的帳戶。"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:3
|
#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:3
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "oauth authorized page title"
|
msgctxt "oauth authorized page title"
|
||||||
msgid "Successfully authorized"
|
msgid "Successfully authorized"
|
||||||
msgstr ""
|
msgstr "授權成功"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:1
|
#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:1
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "oauth external provider page title"
|
msgctxt "oauth external provider page title"
|
||||||
msgid "Sign in with external provider"
|
msgid "Sign in with external provider"
|
||||||
msgstr ""
|
msgstr "使用外部服務進行登錄"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:13
|
#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:13
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "oauth external provider sign in button"
|
msgctxt "oauth external provider sign in button"
|
||||||
msgid "Sign in with %{strategy}"
|
msgid "Sign in with %{strategy}"
|
||||||
msgstr ""
|
msgstr "用 %{strategy} 登錄"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:59
|
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:59
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "oauth login button"
|
msgctxt "oauth login button"
|
||||||
msgid "Log In"
|
msgid "Log In"
|
||||||
msgstr ""
|
msgstr "登錄"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:56
|
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:56
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "oauth login password prompt"
|
msgctxt "oauth login password prompt"
|
||||||
msgid "Password"
|
msgid "Password"
|
||||||
msgstr ""
|
msgstr "密碼"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:52
|
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:52
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "oauth login username prompt"
|
msgctxt "oauth login username prompt"
|
||||||
msgid "Username"
|
msgid "Username"
|
||||||
msgstr ""
|
msgstr "用戶名"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:44
|
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:44
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "oauth register nickname prompt"
|
msgctxt "oauth register nickname prompt"
|
||||||
msgid "Pleroma Handle"
|
msgid "Pleroma Handle"
|
||||||
msgstr ""
|
msgstr "Pleroma 帳號"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:42
|
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:42
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "oauth register nickname unchangeable warning"
|
msgctxt "oauth register nickname unchangeable warning"
|
||||||
msgid "Choose carefully! You won't be able to change this later. You will be able to change your display name, though."
|
msgid "Choose carefully! You won't be able to change this later. You will be able to change your display name, though."
|
||||||
msgstr ""
|
msgstr "選擇時要慎重!您以後將無法更改此項。不過您可以更改您的顯示名稱。"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:18
|
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:18
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "oauth register page email prompt"
|
msgctxt "oauth register page email prompt"
|
||||||
msgid "Email"
|
msgid "Email"
|
||||||
msgstr ""
|
msgstr "郵箱"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:10
|
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:10
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "oauth register page fill form prompt"
|
msgctxt "oauth register page fill form prompt"
|
||||||
msgid "If you'd like to register a new account, please provide the details below."
|
msgid "If you'd like to register a new account, please provide the details below."
|
||||||
msgstr ""
|
msgstr "如果您想註冊一個新帳戶,請提供以下細節。"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:35
|
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:35
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "oauth register page login button"
|
msgctxt "oauth register page login button"
|
||||||
msgid "Proceed as existing user"
|
msgid "Proceed as existing user"
|
||||||
msgstr ""
|
msgstr "以現有用戶身份進行"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:31
|
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:31
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "oauth register page login password prompt"
|
msgctxt "oauth register page login password prompt"
|
||||||
msgid "Password"
|
msgid "Password"
|
||||||
msgstr ""
|
msgstr "密碼"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:24
|
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:24
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "oauth register page login prompt"
|
msgctxt "oauth register page login prompt"
|
||||||
msgid "Alternatively, sign in to connect to existing account."
|
msgid "Alternatively, sign in to connect to existing account."
|
||||||
msgstr ""
|
msgstr "或者登錄後連接到現有賬戶。"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:27
|
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:27
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "oauth register page login username prompt"
|
msgctxt "oauth register page login username prompt"
|
||||||
msgid "Name or email"
|
msgid "Name or email"
|
||||||
msgstr ""
|
msgstr "名字或者郵箱"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:14
|
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:14
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "oauth register page nickname prompt"
|
msgctxt "oauth register page nickname prompt"
|
||||||
msgid "Nickname"
|
msgid "Nickname"
|
||||||
msgstr ""
|
msgstr "暱稱"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:22
|
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:22
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "oauth register page register button"
|
msgctxt "oauth register page register button"
|
||||||
msgid "Proceed as new user"
|
msgid "Proceed as new user"
|
||||||
msgstr ""
|
msgstr "以新用戶身份進行"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:8
|
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:8
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "oauth register page title"
|
msgctxt "oauth register page title"
|
||||||
msgid "Registration Details"
|
msgid "Registration Details"
|
||||||
msgstr ""
|
msgstr "註冊細節"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex:2
|
#: lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex:2
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "oauth scopes message"
|
msgctxt "oauth scopes message"
|
||||||
msgid "The following permissions will be granted"
|
msgid "The following permissions will be granted"
|
||||||
msgstr ""
|
msgstr "將授予以下權限"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:6
|
#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:6
|
||||||
#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:6
|
#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:6
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "oauth token code message"
|
msgctxt "oauth token code message"
|
||||||
msgid "Token code is <br>%{token}"
|
msgid "Token code is <br>%{token}"
|
||||||
msgstr ""
|
msgstr "Token 碼是 <br>%{token}"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:14
|
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:14
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "mfa auth code prompt"
|
msgctxt "mfa auth code prompt"
|
||||||
msgid "Authentication code"
|
msgid "Authentication code"
|
||||||
msgstr ""
|
msgstr "授權碼"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:9
|
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:9
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "mfa auth page title"
|
msgctxt "mfa auth page title"
|
||||||
msgid "Two-factor authentication"
|
msgid "Two-factor authentication"
|
||||||
msgstr ""
|
msgstr "雙因素身份驗證"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:25
|
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:25
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "mfa auth page use recovery code link"
|
msgctxt "mfa auth page use recovery code link"
|
||||||
msgid "Enter a two-factor recovery code"
|
msgid "Enter a two-factor recovery code"
|
||||||
msgstr ""
|
msgstr "輸入一個雙因素恢復的恢復代碼"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:22
|
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:22
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "mfa auth verify code button"
|
msgctxt "mfa auth verify code button"
|
||||||
msgid "Verify"
|
msgid "Verify"
|
||||||
msgstr ""
|
msgstr "認證"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:9
|
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:9
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "mfa recover page title"
|
msgctxt "mfa recover page title"
|
||||||
msgid "Two-factor recovery"
|
msgid "Two-factor recovery"
|
||||||
msgstr ""
|
msgstr "雙因素恢復"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:14
|
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:14
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "mfa recover recovery code prompt"
|
msgctxt "mfa recover recovery code prompt"
|
||||||
msgid "Recovery code"
|
msgid "Recovery code"
|
||||||
msgstr ""
|
msgstr "恢復碼"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:25
|
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:25
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "mfa recover use 2fa code link"
|
msgctxt "mfa recover use 2fa code link"
|
||||||
msgid "Enter a two-factor code"
|
msgid "Enter a two-factor code"
|
||||||
msgstr ""
|
msgstr "輸入一個雙重因素驗證碼"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:22
|
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:22
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "mfa recover verify recovery code button"
|
msgctxt "mfa recover verify recovery code button"
|
||||||
msgid "Verify"
|
msgid "Verify"
|
||||||
msgstr ""
|
msgstr "驗證"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex:42
|
#: lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex:42
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "static fe profile page remote follow button"
|
msgctxt "static fe profile page remote follow button"
|
||||||
msgid "Remote follow"
|
msgid "Remote follow"
|
||||||
msgstr ""
|
msgstr "跨站關注"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/email/digest.html.eex:163
|
#: lib/pleroma/web/templates/email/digest.html.eex:163
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "digest email header line"
|
msgctxt "digest email header line"
|
||||||
msgid "Hey %{nickname}, here is what you've missed!"
|
msgid "Hey %{nickname}, here is what you've missed!"
|
||||||
msgstr ""
|
msgstr "嗨 %{nickname},這是你錯過了的一些東西!"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/email/digest.html.eex:544
|
#: lib/pleroma/web/templates/email/digest.html.eex:544
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "digest email receiver address"
|
msgctxt "digest email receiver address"
|
||||||
msgid "The email address you are subscribed as is <a href='mailto:%{@user.email}' style='color: %{color};text-decoration: none;'>%{email}</a>. "
|
msgid "The email address you are subscribed as is <a href='mailto:%{@user.email}' style='color: %{color};text-decoration: none;'>%{email}</a>. "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"您訂閱的電子郵件地址是 <a href='mailto:%{@user.email}' style='color: %{color"
|
||||||
|
"};text-decoration: none;'>%{email}</a>。 "
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/email/digest.html.eex:538
|
#: lib/pleroma/web/templates/email/digest.html.eex:538
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "digest email sending reason"
|
msgctxt "digest email sending reason"
|
||||||
msgid "You have received this email because you have signed up to receive digest emails from <b>%{instance}</b> Pleroma instance."
|
msgid "You have received this email because you have signed up to receive digest emails from <b>%{instance}</b> Pleroma instance."
|
||||||
msgstr ""
|
msgstr "您之所以會收到來自 <b>%{instance}</b> Akkoma "
|
||||||
|
"實例的郵件摘要,是因爲您已經註冊了該服務實例。"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/email/digest.html.eex:547
|
#: lib/pleroma/web/templates/email/digest.html.eex:547
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "digest email unsubscribe action"
|
msgctxt "digest email unsubscribe action"
|
||||||
msgid "To unsubscribe, please go %{here}."
|
msgid "To unsubscribe, please go %{here}."
|
||||||
msgstr ""
|
msgstr "取消訂閱,請點擊 %{here}."
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/email/digest.html.eex:547
|
#: lib/pleroma/web/templates/email/digest.html.eex:547
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "digest email unsubscribe action link text"
|
msgctxt "digest email unsubscribe action link text"
|
||||||
msgid "here"
|
msgid "here"
|
||||||
msgstr ""
|
msgstr "這里"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_failure.html.eex:1
|
#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_failure.html.eex:1
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "mailer unsubscribe failed message"
|
msgctxt "mailer unsubscribe failed message"
|
||||||
msgid "UNSUBSCRIBE FAILURE"
|
msgid "UNSUBSCRIBE FAILURE"
|
||||||
msgstr ""
|
msgstr "取消訂閱失敗"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_success.html.eex:1
|
#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_success.html.eex:1
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "mailer unsubscribe successful message"
|
msgctxt "mailer unsubscribe successful message"
|
||||||
msgid "UNSUBSCRIBE SUCCESSFUL"
|
msgid "UNSUBSCRIBE SUCCESSFUL"
|
||||||
msgstr ""
|
msgstr "成功取消訂閱"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/email/digest.html.eex:385
|
#: lib/pleroma/web/templates/email/digest.html.eex:385
|
||||||
#, elixir-format
|
#, elixir-format
|
||||||
msgctxt "new followers count header"
|
msgctxt "new followers count header"
|
||||||
msgid "%{count} New Follower"
|
msgid "%{count} New Follower"
|
||||||
msgid_plural "%{count} New Followers"
|
msgid_plural "%{count} New Followers"
|
||||||
msgstr[0] ""
|
msgstr[0] "%{count} 個新關注者"
|
||||||
|
|
||||||
#: lib/pleroma/emails/user_email.ex:384
|
#: lib/pleroma/emails/user_email.ex:384
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "account archive email subject"
|
msgctxt "account archive email subject"
|
||||||
msgid "Your account archive is ready"
|
msgid "Your account archive is ready"
|
||||||
msgstr ""
|
msgstr "您的帳戶檔案已經準備好了"
|
||||||
|
|
||||||
#: lib/pleroma/emails/user_email.ex:188
|
#: lib/pleroma/emails/user_email.ex:188
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "approval pending email body"
|
msgctxt "approval pending email body"
|
||||||
msgid "<h3>Awaiting Approval</h3>\n<p>Your account at %{instance_name} is being reviewed by staff. You will receive another email once your account is approved.</p>\n"
|
msgid "<h3>Awaiting Approval</h3>\n<p>Your account at %{instance_name} is being reviewed by staff. You will receive another email once your account is approved.</p>\n"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"<h3>正在等待批准</h3>\n"
|
||||||
|
"<p> 您在 %{instance_name} 的帳戶正在被工作人員審查。一旦您的帳戶被批准通過,"
|
||||||
|
"您將收到另一封電子郵件。</p>\n"
|
||||||
|
|
||||||
#: lib/pleroma/emails/user_email.ex:202
|
#: lib/pleroma/emails/user_email.ex:202
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "approval pending email subject"
|
msgctxt "approval pending email subject"
|
||||||
msgid "Your account is awaiting approval"
|
msgid "Your account is awaiting approval"
|
||||||
msgstr ""
|
msgstr "您的帳戶正在等待審批"
|
||||||
|
|
||||||
#: lib/pleroma/emails/user_email.ex:158
|
#: lib/pleroma/emails/user_email.ex:158
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "confirmation email body"
|
msgctxt "confirmation email body"
|
||||||
msgid "<h3>Thank you for registering on %{instance_name}</h3>\n<p>Email confirmation is required to activate the account.</p>\n<p>Please click the following link to <a href=\"%{confirmation_url}\">activate your account</a>.</p>\n"
|
msgid "<h3>Thank you for registering on %{instance_name}</h3>\n<p>Email confirmation is required to activate the account.</p>\n<p>Please click the following link to <a href=\"%{confirmation_url}\">activate your account</a>.</p>\n"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"<h3>感謝註冊 %{instance_name}</h3>\n"
|
||||||
|
"<p>需要電子郵件確認才能激活該帳戶</p>\n"
|
||||||
|
"<p>請點擊以下鏈結以 <a href=\"%{confirmation_url}\">確認您的帳戶</a></p>\n"
|
||||||
|
|
||||||
#: lib/pleroma/emails/user_email.ex:174
|
#: lib/pleroma/emails/user_email.ex:174
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "confirmation email subject"
|
msgctxt "confirmation email subject"
|
||||||
msgid "%{instance_name} account confirmation"
|
msgid "%{instance_name} account confirmation"
|
||||||
msgstr ""
|
msgstr "%{instance_name} 帳戶確認"
|
||||||
|
|
||||||
#: lib/pleroma/emails/user_email.ex:310
|
#: lib/pleroma/emails/user_email.ex:310
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "digest email subject"
|
msgctxt "digest email subject"
|
||||||
msgid "Your digest from %{instance_name}"
|
msgid "Your digest from %{instance_name}"
|
||||||
msgstr ""
|
msgstr "您來自 %{instance_name} 的摘要郵件"
|
||||||
|
|
||||||
#: lib/pleroma/emails/user_email.ex:81
|
#: lib/pleroma/emails/user_email.ex:81
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "password reset email body"
|
msgctxt "password reset email body"
|
||||||
msgid "<h3>Reset your password at %{instance_name}</h3>\n<p>Someone has requested password change for your account at %{instance_name}.</p>\n<p>If it was you, visit the following link to proceed: <a href=\"%{password_reset_url}\">reset password</a>.</p>\n<p>If it was someone else, nothing to worry about: your data is secure and your password has not been changed.</p>\n"
|
msgid "<h3>Reset your password at %{instance_name}</h3>\n<p>Someone has requested password change for your account at %{instance_name}.</p>\n<p>If it was you, visit the following link to proceed: <a href=\"%{password_reset_url}\">reset password</a>.</p>\n<p>If it was someone else, nothing to worry about: your data is secure and your password has not been changed.</p>\n"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"<h3>在 %{instance_name} 重置您的密碼</h3>\n"
|
||||||
|
"<p>有人請求更改您在 %{instance_name} 的帳戶密碼。</p>\n"
|
||||||
|
"<p>如果這是您的操作,請點擊以下鏈結繼續重置密碼:<a href=\""
|
||||||
|
"%{password_reset_url}\">重置密碼</a>。</p>\n"
|
||||||
|
"<p>如果這不是您的操作,請不用擔心:您的數據是安全的,您的密碼未被更改。</p>\n"
|
||||||
|
|
||||||
#: lib/pleroma/emails/user_email.ex:98
|
#: lib/pleroma/emails/user_email.ex:98
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "password reset email subject"
|
msgctxt "password reset email subject"
|
||||||
msgid "Password reset"
|
msgid "Password reset"
|
||||||
msgstr ""
|
msgstr "重置密碼"
|
||||||
|
|
||||||
#: lib/pleroma/emails/user_email.ex:215
|
#: lib/pleroma/emails/user_email.ex:215
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "successful registration email body"
|
msgctxt "successful registration email body"
|
||||||
msgid "<h3>Hello @%{nickname},</h3>\n<p>Your account at %{instance_name} has been registered successfully.</p>\n<p>No further action is required to activate your account.</p>\n"
|
msgid "<h3>Hello @%{nickname},</h3>\n<p>Your account at %{instance_name} has been registered successfully.</p>\n<p>No further action is required to activate your account.</p>\n"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"<h3>你好,@%{nickname},</h3>\n"
|
||||||
|
"<p>你在 %{instance_name} 的帳戶已經成功註冊。</p>\n"
|
||||||
|
"<p>無需進行其他操作即可激活你的帳戶。</p>\n"
|
||||||
|
|
||||||
#: lib/pleroma/emails/user_email.ex:231
|
#: lib/pleroma/emails/user_email.ex:231
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "successful registration email subject"
|
msgctxt "successful registration email subject"
|
||||||
msgid "Account registered on %{instance_name}"
|
msgid "Account registered on %{instance_name}"
|
||||||
msgstr ""
|
msgstr "帳號註冊在 %{instance_name}"
|
||||||
|
|
||||||
#: lib/pleroma/emails/user_email.ex:136
|
#: lib/pleroma/emails/user_email.ex:136
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "user invitation email subject"
|
msgctxt "user invitation email subject"
|
||||||
msgid "Invitation to %{instance_name}"
|
msgid "Invitation to %{instance_name}"
|
||||||
msgstr ""
|
msgstr "邀請加入 %{instance_name}"
|
||||||
|
|
||||||
#: lib/pleroma/emails/user_email.ex:53
|
#: lib/pleroma/emails/user_email.ex:53
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "welcome email html body"
|
msgctxt "welcome email html body"
|
||||||
msgid "Welcome to %{instance_name}!"
|
msgid "Welcome to %{instance_name}!"
|
||||||
msgstr ""
|
msgstr "歡迎來到 %{instance_name}!"
|
||||||
|
|
||||||
#: lib/pleroma/emails/user_email.ex:41
|
#: lib/pleroma/emails/user_email.ex:41
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "welcome email subject"
|
msgctxt "welcome email subject"
|
||||||
msgid "Welcome to %{instance_name}!"
|
msgid "Welcome to %{instance_name}!"
|
||||||
msgstr ""
|
msgstr "歡迎來到 %{instance_name}!"
|
||||||
|
|
||||||
#: lib/pleroma/emails/user_email.ex:65
|
#: lib/pleroma/emails/user_email.ex:65
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "welcome email text body"
|
msgctxt "welcome email text body"
|
||||||
msgid "Welcome to %{instance_name}!"
|
msgid "Welcome to %{instance_name}!"
|
||||||
msgstr ""
|
msgstr "歡迎來到 %{instance_name}!"
|
||||||
|
|
||||||
#: lib/pleroma/emails/user_email.ex:368
|
#: lib/pleroma/emails/user_email.ex:368
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "account archive email body - admin requested"
|
msgctxt "account archive email body - admin requested"
|
||||||
msgid "<p>Admin @%{admin_nickname} requested a full backup of your Akkoma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
|
msgid "<p>Admin @%{admin_nickname} requested a full backup of your Akkoma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"<p>管理員 @%{admin_nickname} 請求對你的 Akkoma "
|
||||||
|
"帳戶進行完整備份,備份已準備好可供下載:</p>\n"
|
||||||
|
"<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
|
||||||
|
|
||||||
#: lib/pleroma/emails/user_email.ex:356
|
#: lib/pleroma/emails/user_email.ex:356
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "account archive email body - self-requested"
|
msgctxt "account archive email body - self-requested"
|
||||||
msgid "<p>You requested a full backup of your Akkoma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
|
msgid "<p>You requested a full backup of your Akkoma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"<p>您請求了 Akkoma 帳戶的完整備份。備份已準備就緒,可以下載:</p>\n"
|
||||||
|
"<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:41
|
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:41
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "oauth register page title"
|
msgctxt "oauth register page title"
|
||||||
msgid "This is your first visit! Please enter your Akkoma handle."
|
msgid "This is your first visit! Please enter your Akkoma handle."
|
||||||
msgstr ""
|
msgstr "這是您的第一次訪問!請填寫您的 Akkoma 帳號。"
|
||||||
|
|
||||||
#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:123
|
#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:123
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "remote follow error message - unknown error"
|
msgctxt "remote follow error message - unknown error"
|
||||||
msgid "Something went wrong."
|
msgid "Something went wrong."
|
||||||
msgstr ""
|
msgstr "發生了一些錯誤。"
|
||||||
|
|
||||||
#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:67
|
#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:67
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "remote follow error message - user not found"
|
msgctxt "remote follow error message - user not found"
|
||||||
msgid "Could not find user"
|
msgid "Could not find user"
|
||||||
msgstr ""
|
msgstr "無法找到相應用戶"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:8
|
#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:8
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "status interact authorization button"
|
msgctxt "status interact authorization button"
|
||||||
msgid "Interact"
|
msgid "Interact"
|
||||||
msgstr ""
|
msgstr "互動"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:2
|
#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:2
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "status interact error"
|
msgctxt "status interact error"
|
||||||
msgid "Error: %{error}"
|
msgid "Error: %{error}"
|
||||||
msgstr ""
|
msgstr "錯誤:%{error}"
|
||||||
|
|
||||||
#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:95
|
#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:95
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "status interact error message - status not found"
|
msgctxt "status interact error message - status not found"
|
||||||
msgid "Could not find status"
|
msgid "Could not find status"
|
||||||
msgstr ""
|
msgstr "無法找到貼文"
|
||||||
|
|
||||||
#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:144
|
#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:144
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "status interact error message - unknown error"
|
msgctxt "status interact error message - unknown error"
|
||||||
msgid "Something went wrong."
|
msgid "Something went wrong."
|
||||||
msgstr ""
|
msgstr "發生了一些錯誤。"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:4
|
#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:4
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "status interact header"
|
msgctxt "status interact header"
|
||||||
msgid "Interacting with %{nickname}'s %{status_link}"
|
msgid "Interacting with %{nickname}'s %{status_link}"
|
||||||
msgstr ""
|
msgstr "與 %{nickname} 的 %{status_link} 進行交互"
|
||||||
|
|
||||||
#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:4
|
#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:4
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "status interact header - status link text"
|
msgctxt "status interact header - status link text"
|
||||||
msgid "status"
|
msgid "status"
|
||||||
msgstr ""
|
msgstr "貼文"
|
||||||
|
|
||||||
#: lib/pleroma/emails/user_email.ex:119
|
#: lib/pleroma/emails/user_email.ex:119
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgctxt "user invitation email body"
|
msgctxt "user invitation email body"
|
||||||
msgid "<h3>You are invited to %{instance_name}</h3>\n<p>%{inviter_name} invites you to join %{instance_name}, an instance of Akkoma federated social networking platform.</p>\n<p>Click the following link to register: <a href=\"%{registration_url}\">accept invitation</a>.</p>\n"
|
msgid "<h3>You are invited to %{instance_name}</h3>\n<p>%{inviter_name} invites you to join %{instance_name}, an instance of Akkoma federated social networking platform.</p>\n<p>Click the following link to register: <a href=\"%{registration_url}\">accept invitation</a>.</p>\n"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"<h3>您被邀請加入 %{instance_name}</h3>\n"
|
||||||
|
"<p>%{inviter_name} 邀請您加入 %{instance_name},這是一個使用 Akkoma "
|
||||||
|
"聯邦社交網絡平臺的實例。</p>\n"
|
||||||
|
"<p>點擊以下鏈結註冊: <a href=\"%{registration_url}\">接受邀請</a>.</p>\n"
|
||||||
|
|
|
@ -1,37 +1,10 @@
|
||||||
defmodule Pleroma.Repo.Migrations.UploadFilterExiftoolToExiftoolStripMetadata do
|
defmodule Pleroma.Repo.Migrations.UploadFilterExiftoolToExiftoolStripMetadata do
|
||||||
use Ecto.Migration
|
use Ecto.Migration
|
||||||
|
|
||||||
alias Pleroma.ConfigDB
|
# 20240425120000_upload_filter_exiftool_to_exiftool_strip_location.exs
|
||||||
|
# was originally committed with the id used in this file, but this breaks
|
||||||
def up,
|
# rollback order. Thus it was moved to 20240425120000 and this stub just prevents
|
||||||
do:
|
# errors during large-scale rollbacks for anyone who already applied the old id
|
||||||
ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.Upload})
|
def up, do: :ok
|
||||||
|> update_filtername(
|
def down, do: :ok
|
||||||
Pleroma.Upload.Filter.Exiftool,
|
|
||||||
Pleroma.Upload.Filter.Exiftool.StripMetadata
|
|
||||||
)
|
|
||||||
|
|
||||||
def down,
|
|
||||||
do:
|
|
||||||
ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.Upload})
|
|
||||||
|> update_filtername(
|
|
||||||
Pleroma.Upload.Filter.Exiftool.StripMetadata,
|
|
||||||
Pleroma.Upload.Filter.Exiftool
|
|
||||||
)
|
|
||||||
|
|
||||||
defp update_filtername(%{value: value}, from_filtername, to_filtername) do
|
|
||||||
new_value =
|
|
||||||
value
|
|
||||||
|> Keyword.update(:filters, [], fn filters ->
|
|
||||||
filters
|
|
||||||
|> Enum.map(fn
|
|
||||||
^from_filtername -> to_filtername
|
|
||||||
filter -> filter
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
ConfigDB.update_or_create(%{group: :pleroma, key: Pleroma.Upload, value: new_value})
|
|
||||||
end
|
|
||||||
|
|
||||||
defp update_filtername(_, _, _), do: nil
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
defmodule Pleroma.Repo.Migrations.UploadFilterExiftoolToExiftoolStripMetadataReal do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
alias Pleroma.ConfigDB
|
||||||
|
|
||||||
|
def up,
|
||||||
|
do:
|
||||||
|
ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.Upload})
|
||||||
|
|> update_filtername(
|
||||||
|
Pleroma.Upload.Filter.Exiftool,
|
||||||
|
Pleroma.Upload.Filter.Exiftool.StripMetadata
|
||||||
|
)
|
||||||
|
|
||||||
|
def down,
|
||||||
|
do:
|
||||||
|
ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.Upload})
|
||||||
|
|> update_filtername(
|
||||||
|
Pleroma.Upload.Filter.Exiftool.StripMetadata,
|
||||||
|
Pleroma.Upload.Filter.Exiftool
|
||||||
|
)
|
||||||
|
|
||||||
|
defp update_filtername(%{value: value}, from_filtername, to_filtername) do
|
||||||
|
new_value =
|
||||||
|
value
|
||||||
|
|> Keyword.update(:filters, [], fn filters ->
|
||||||
|
filters
|
||||||
|
|> Enum.map(fn
|
||||||
|
^from_filtername -> to_filtername
|
||||||
|
filter -> filter
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
ConfigDB.update_or_create(%{group: :pleroma, key: Pleroma.Upload, value: new_value})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp update_filtername(_, _, _), do: nil
|
||||||
|
end
|
64
priv/repo/migrations/20240501190000_drop_unused_indexes.exs
Normal file
64
priv/repo/migrations/20240501190000_drop_unused_indexes.exs
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
defmodule Pleroma.Repo.Migrations.DropUnusedIndexes do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def up do
|
||||||
|
# Leftovers from a late Pleroma migration (will not be restored on rollback)
|
||||||
|
drop_i(:users, [:show_birthday], :users_show_birthday_index)
|
||||||
|
|
||||||
|
drop_i(
|
||||||
|
:users,
|
||||||
|
["date_part('month', birthday)", "date_part('day', birthday)"],
|
||||||
|
:users_birthday_month_day_index
|
||||||
|
)
|
||||||
|
|
||||||
|
# Unused
|
||||||
|
drop_i(:activities, ["(data->'cc')"], :activities_cc_index)
|
||||||
|
drop_i(:activities, ["(data->'object'->>'inReplyTo')"], :activities_in_reply_to)
|
||||||
|
drop_i(:activities, ["(data #> '{\"object\",\"likes\"}')"], :activities_likes)
|
||||||
|
drop_i(:activities, ["(data->'to')"], :activities_to_index)
|
||||||
|
|
||||||
|
drop_i(:objects, ["(data->'likes')"], :objects_likes)
|
||||||
|
|
||||||
|
drop_i(:users, [:featured_address], :users_featured_address_index)
|
||||||
|
drop_i(:users, [:following_address], :users_following_address_index)
|
||||||
|
drop_i(:users, [:invisible], :users_invisible_index)
|
||||||
|
drop_i(:users, [:last_status_at], :users_last_status_at_index)
|
||||||
|
drop_i(:users, [:tags], :users_tags_index)
|
||||||
|
|
||||||
|
drop_i(:apps, [:client_id, :client_secret], :apps_client_id_client_secret_index)
|
||||||
|
drop_i(:apps, [:user_id], :apps_user_id_index)
|
||||||
|
|
||||||
|
# Duplicate of primary key index (will not be restored on rollback)
|
||||||
|
drop_i(
|
||||||
|
:user_frontend_setting_profiles,
|
||||||
|
[:user_id, :frontend_name, :profile_name],
|
||||||
|
:user_frontend_setting_profiles_user_id_frontend_name_profile_name_index
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
create_i(:activities, ["(data->'cc')"], :activities_cc_index, :gin)
|
||||||
|
create_i(:activities, ["(data->'object'->>'inReplyTo')"], :activities_in_reply_to)
|
||||||
|
create_i(:activities, ["(data #> '{\"object\",\"likes\"}')"], :activities_likes, :gin)
|
||||||
|
create_i(:activities, ["(data->'to')"], :activities_to_index, :gin)
|
||||||
|
|
||||||
|
create_i(:objects, ["(data->'likes')"], :objects_likes, :gin)
|
||||||
|
|
||||||
|
create_i(:users, [:featured_address], :users_featured_address_index)
|
||||||
|
create_i(:users, [:following_address], :users_following_address_index)
|
||||||
|
create_i(:users, [:invisible], :users_invisible_index)
|
||||||
|
create_i(:users, [:last_status_at], :users_last_status_at_index)
|
||||||
|
create_i(:users, [:tags], :users_tags_index, :gin)
|
||||||
|
|
||||||
|
create_i(:apps, [:client_id, :client_secret], :apps_client_id_client_secret_index)
|
||||||
|
create_i(:apps, [:user_id], :apps_user_id_index)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp drop_i(table, fields, name) do
|
||||||
|
drop_if_exists(index(table, fields, name: name))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_i(table, fields, name, type \\ :btree) do
|
||||||
|
create_if_not_exists(index(table, fields, name: name, using: type))
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,74 +0,0 @@
|
||||||
defmodule Pleroma.Repo.Migrations.DropUnusedIndexes do
|
|
||||||
use Ecto.Migration
|
|
||||||
|
|
||||||
@disable_ddl_transaction true
|
|
||||||
|
|
||||||
@disable_migration_lock true
|
|
||||||
|
|
||||||
def up do
|
|
||||||
drop_if_exists(
|
|
||||||
index(:activities, ["(data->>'actor')", "inserted_at desc"], name: :activities_actor_index)
|
|
||||||
)
|
|
||||||
|
|
||||||
drop_if_exists(index(:activities, ["(data->'to')"], name: :activities_to_index))
|
|
||||||
|
|
||||||
drop_if_exists(index(:activities, ["(data->'cc')"], name: :activities_cc_index))
|
|
||||||
|
|
||||||
drop_if_exists(index(:activities, ["(split_part(actor, '/', 3))"], name: :activities_hosts))
|
|
||||||
|
|
||||||
drop_if_exists(
|
|
||||||
index(:activities, ["(data->'object'->>'inReplyTo')"], name: :activities_in_reply_to)
|
|
||||||
)
|
|
||||||
|
|
||||||
drop_if_exists(
|
|
||||||
index(:activities, ["((data #> '{\"object\",\"likes\"}'))"], name: :activities_likes)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def down do
|
|
||||||
create_if_not_exists(
|
|
||||||
index(:activities, ["(data->>'actor')", "inserted_at desc"],
|
|
||||||
name: :activities_actor_index,
|
|
||||||
concurrently: true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
create_if_not_exists(
|
|
||||||
index(:activities, ["(data->'to')"],
|
|
||||||
name: :activities_to_index,
|
|
||||||
using: :gin,
|
|
||||||
concurrently: true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
create_if_not_exists(
|
|
||||||
index(:activities, ["(data->'cc')"],
|
|
||||||
name: :activities_cc_index,
|
|
||||||
using: :gin,
|
|
||||||
concurrently: true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
create_if_not_exists(
|
|
||||||
index(:activities, ["(split_part(actor, '/', 3))"],
|
|
||||||
name: :activities_hosts,
|
|
||||||
concurrently: true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
create_if_not_exists(
|
|
||||||
index(:activities, ["(data->'object'->>'inReplyTo')"],
|
|
||||||
name: :activities_in_reply_to,
|
|
||||||
concurrently: true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
create_if_not_exists(
|
|
||||||
index(:activities, ["((data #> '{\"object\",\"likes\"}'))"],
|
|
||||||
name: :activities_likes,
|
|
||||||
using: :gin,
|
|
||||||
concurrently: true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
3
test/fixtures/tesla_mock/bad.com_host_meta
vendored
Normal file
3
test/fixtures/tesla_mock/bad.com_host_meta
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
|
||||||
|
<Link rel="lrdd" template="https://bad.com/.well-known/webfinger?resource={uri}" type="application/xrd+xml" />
|
||||||
|
</XRD>
|
28
test/fixtures/tesla_mock/webfinger_spoof.json
vendored
Normal file
28
test/fixtures/tesla_mock/webfinger_spoof.json
vendored
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"aliases": [
|
||||||
|
"https://bad.com/users/meanie",
|
||||||
|
"https://anotherbad.social/users/meanie"
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"href": "https://bad.com/users/meanie",
|
||||||
|
"rel": "http://webfinger.net/rel/profile-page",
|
||||||
|
"type": "text/html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "https://bad.com/users/meanie",
|
||||||
|
"rel": "self",
|
||||||
|
"type": "application/activity+json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "https://bad.com/users/meanie",
|
||||||
|
"rel": "self",
|
||||||
|
"type": "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rel": "http://ostatus.org/schema/1.0/subscribe",
|
||||||
|
"template": "https://bad.com/ostatus_subscribe?acct={uri}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subject": "acct:oopsie@notwhereitshouldbe.org"
|
||||||
|
}
|
51
test/fixtures/users_mock/nonsense_attachment_user.json
vendored
Normal file
51
test/fixtures/users_mock/nonsense_attachment_user.json
vendored
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
{
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
{
|
||||||
|
"blurhash": "toot:blurhash",
|
||||||
|
"Emoji": "toot:Emoji",
|
||||||
|
"focalPoint": {
|
||||||
|
"@container": "@list",
|
||||||
|
"@id": "toot:focalPoint"
|
||||||
|
},
|
||||||
|
"Hashtag": "as:Hashtag",
|
||||||
|
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
||||||
|
"sensitive": "as:sensitive",
|
||||||
|
"toot": "http://joinmastodon.org/ns#",
|
||||||
|
"votersCount": "toot:votersCount",
|
||||||
|
"featured": {
|
||||||
|
"@id": "toot:featured",
|
||||||
|
"@type": "@id"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"https://w3id.org/security/v1"
|
||||||
|
],
|
||||||
|
"id": "https://fedi.vision/@vote@fedi.vision/",
|
||||||
|
"type": "Person",
|
||||||
|
"toot:discoverable": true,
|
||||||
|
"inbox": "https://fedi.vision/@vote@fedi.vision/inbox/",
|
||||||
|
"publicKey": {
|
||||||
|
"id": "https://fedi.vision/@vote@fedi.vision/#main-key",
|
||||||
|
"owner": "https://fedi.vision/@vote@fedi.vision/",
|
||||||
|
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAj2f+uQtdoBO9X/u2Qso4\nxHYdfy8zB24m9Gg982/ts88DAMLxZUzX0JsBWT7coL0Ipf4NSbVaqS6nKrr2P8Qs\nf97wMhowyuYxK22BMPcbpfZkFj3tVT/JkDx2iujBJJ5ZBO5KRlupjDTqV4rOAY7F\n58ad0jK9PsJNJMsJ/b8+0t3Q/K+RqCGVmtK+iPSigOYoiKoquyRzHLTfP+mpOlDa\n3f+uyAbFya7CpcgBx1zz0PALWA+oh/zhZK4yT6719Esa8SDcoJ0ws70zMxWekq1A\n3ia88/Io6SY2qFNBpzzXGO3JK8OFRFtmPV8ZfAh5Pv6y52iuTJ21kxjAG7ZTP/fY\nBQIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||||
|
},
|
||||||
|
"attachment": {
|
||||||
|
"haha": "you expected a proper object, but it was me, random nonsense"
|
||||||
|
},
|
||||||
|
"endpoints": {
|
||||||
|
"sharedInbox": "https://fedi.vision/inbox/"
|
||||||
|
},
|
||||||
|
"followers": "https://fedi.vision/@vote@fedi.vision/followers/",
|
||||||
|
"following": "https://fedi.vision/@vote@fedi.vision/following/",
|
||||||
|
"icon": {
|
||||||
|
"type": "Image",
|
||||||
|
"mediaType": "image/webp",
|
||||||
|
"url": "https://eu-central-1.linodeobjects.com:443/st4/profile_images/2024/5/9/RwqTbeYx16gauXPXvt-CaysOnGw.webp"
|
||||||
|
},
|
||||||
|
"name": "FediVision Vote Bot",
|
||||||
|
"outbox": "https://fedi.vision/@vote@fedi.vision/outbox/",
|
||||||
|
"preferredUsername": "vote",
|
||||||
|
"published": "2024-05-09T09:04:04Z",
|
||||||
|
"summary": "<p>New in 2024, this is the bot that will count your #Fedivision vote! Accept no substitutes!</p><p>Send this account a toot in the form<br> vote ABCD EFGH IJKL<br>substituting the (up to) three codes for the songs you want to win. Punctuation ignored, case insensitive, order is unimportant. Only your latest toot counts, so change your vote with a new toot.</p>",
|
||||||
|
"url": "https://fedi.vision/@vote/"
|
||||||
|
}
|
53
test/fixtures/users_mock/takahe_user.json
vendored
Normal file
53
test/fixtures/users_mock/takahe_user.json
vendored
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
{
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
{
|
||||||
|
"blurhash": "toot:blurhash",
|
||||||
|
"Emoji": "toot:Emoji",
|
||||||
|
"focalPoint": {
|
||||||
|
"@container": "@list",
|
||||||
|
"@id": "toot:focalPoint"
|
||||||
|
},
|
||||||
|
"Hashtag": "as:Hashtag",
|
||||||
|
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
||||||
|
"sensitive": "as:sensitive",
|
||||||
|
"toot": "http://joinmastodon.org/ns#",
|
||||||
|
"votersCount": "toot:votersCount",
|
||||||
|
"featured": {
|
||||||
|
"@id": "toot:featured",
|
||||||
|
"@type": "@id"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"https://w3id.org/security/v1"
|
||||||
|
],
|
||||||
|
"id": "https://fedi.vision/@vote@fedi.vision/",
|
||||||
|
"type": "Person",
|
||||||
|
"toot:discoverable": true,
|
||||||
|
"inbox": "https://fedi.vision/@vote@fedi.vision/inbox/",
|
||||||
|
"publicKey": {
|
||||||
|
"id": "https://fedi.vision/@vote@fedi.vision/#main-key",
|
||||||
|
"owner": "https://fedi.vision/@vote@fedi.vision/",
|
||||||
|
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAj2f+uQtdoBO9X/u2Qso4\nxHYdfy8zB24m9Gg982/ts88DAMLxZUzX0JsBWT7coL0Ipf4NSbVaqS6nKrr2P8Qs\nf97wMhowyuYxK22BMPcbpfZkFj3tVT/JkDx2iujBJJ5ZBO5KRlupjDTqV4rOAY7F\n58ad0jK9PsJNJMsJ/b8+0t3Q/K+RqCGVmtK+iPSigOYoiKoquyRzHLTfP+mpOlDa\n3f+uyAbFya7CpcgBx1zz0PALWA+oh/zhZK4yT6719Esa8SDcoJ0ws70zMxWekq1A\n3ia88/Io6SY2qFNBpzzXGO3JK8OFRFtmPV8ZfAh5Pv6y52iuTJ21kxjAG7ZTP/fY\nBQIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||||
|
},
|
||||||
|
"attachment": {
|
||||||
|
"type": "PropertyValue",
|
||||||
|
"value": "<a href=\"https://fedivision.party/vote\" rel=\"nofollow\"><span class=\"invisible\">https://</span>fedivision.party/vote</a>",
|
||||||
|
"name": "More details"
|
||||||
|
},
|
||||||
|
"endpoints": {
|
||||||
|
"sharedInbox": "https://fedi.vision/inbox/"
|
||||||
|
},
|
||||||
|
"followers": "https://fedi.vision/@vote@fedi.vision/followers/",
|
||||||
|
"following": "https://fedi.vision/@vote@fedi.vision/following/",
|
||||||
|
"icon": {
|
||||||
|
"type": "Image",
|
||||||
|
"mediaType": "image/webp",
|
||||||
|
"url": "https://eu-central-1.linodeobjects.com:443/st4/profile_images/2024/5/9/RwqTbeYx16gauXPXvt-CaysOnGw.webp"
|
||||||
|
},
|
||||||
|
"name": "FediVision Vote Bot",
|
||||||
|
"outbox": "https://fedi.vision/@vote@fedi.vision/outbox/",
|
||||||
|
"preferredUsername": "vote",
|
||||||
|
"published": "2024-05-09T09:04:04Z",
|
||||||
|
"summary": "<p>New in 2024, this is the bot that will count your #Fedivision vote! Accept no substitutes!</p><p>Send this account a toot in the form<br> vote ABCD EFGH IJKL<br>substituting the (up to) three codes for the songs you want to win. Punctuation ignored, case insensitive, order is unimportant. Only your latest toot counts, so change your vote with a new toot.</p>",
|
||||||
|
"url": "https://fedi.vision/@vote/"
|
||||||
|
}
|
41
test/fixtures/webfinger/imposter-webfinger.json
vendored
Normal file
41
test/fixtures/webfinger/imposter-webfinger.json
vendored
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"subject": "acct:oopsie@notwhereitshouldbe.com",
|
||||||
|
"aliases": [
|
||||||
|
"https://bad.com/webfingertest"
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"rel": "http://webfinger.net/rel/profile-page",
|
||||||
|
"type": "text/html",
|
||||||
|
"href": "https://bad.com/webfingertest"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rel": "self",
|
||||||
|
"type": "application/activity+json",
|
||||||
|
"href": "https://bad.com/webfingertest"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rel": "http://ostatus.org/schema/1.0/subscribe",
|
||||||
|
"template": "https://bad.com/contact/follow?url={uri}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rel": "http://schemas.google.com/g/2010#updates-from",
|
||||||
|
"type": "application/atom+xml",
|
||||||
|
"href": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rel": "salmon",
|
||||||
|
"href": "https://bad.com/salmon/friendica"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rel": "http://microformats.org/profile/hcard",
|
||||||
|
"type": "text/html",
|
||||||
|
"href": "https://bad.com/hcard/friendica"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rel": "http://joindiaspora.com/seed_location",
|
||||||
|
"type": "text/html",
|
||||||
|
"href": "https://bad.com"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -3,7 +3,7 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Mix.Tasks.Pleroma.AppTest do
|
defmodule Mix.Tasks.Pleroma.AppTest do
|
||||||
use Pleroma.DataCase, async: true
|
use Pleroma.DataCase, async: false
|
||||||
|
|
||||||
setup_all do
|
setup_all do
|
||||||
Mix.shell(Mix.Shell.Process)
|
Mix.shell(Mix.Shell.Process)
|
||||||
|
@ -50,13 +50,13 @@ defmodule Mix.Tasks.Pleroma.AppTest do
|
||||||
defp assert_app(name, redirect, scopes) do
|
defp assert_app(name, redirect, scopes) do
|
||||||
app = Repo.get_by(Pleroma.Web.OAuth.App, client_name: name)
|
app = Repo.get_by(Pleroma.Web.OAuth.App, client_name: name)
|
||||||
|
|
||||||
assert_receive {:mix_shell, :info, [message]}, 1_000
|
assert_receive {:mix_shell, :info, [message]}, 5_000
|
||||||
assert message == "#{name} successfully created:"
|
assert message == "#{name} successfully created:"
|
||||||
|
|
||||||
assert_receive {:mix_shell, :info, [message]}, 1_000
|
assert_receive {:mix_shell, :info, [message]}, 5_000
|
||||||
assert message == "App client_id: #{app.client_id}"
|
assert message == "App client_id: #{app.client_id}"
|
||||||
|
|
||||||
assert_receive {:mix_shell, :info, [message]}, 1_000
|
assert_receive {:mix_shell, :info, [message]}, 5_000
|
||||||
assert message == "App client_secret: #{app.client_secret}"
|
assert message == "App client_secret: #{app.client_secret}"
|
||||||
|
|
||||||
assert app.scopes == scopes
|
assert app.scopes == scopes
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Mix.Tasks.Pleroma.DatabaseTest do
|
defmodule Mix.Tasks.Pleroma.DatabaseTest do
|
||||||
use Pleroma.DataCase, async: true
|
use Pleroma.DataCase, async: false
|
||||||
use Oban.Testing, repo: Pleroma.Repo
|
use Oban.Testing, repo: Pleroma.Repo
|
||||||
|
|
||||||
alias Pleroma.Activity
|
alias Pleroma.Activity
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Mix.Tasks.Pleroma.Ecto.RollbackTest do
|
defmodule Mix.Tasks.Pleroma.Ecto.RollbackTest do
|
||||||
use Pleroma.DataCase, async: true
|
use Pleroma.DataCase, async: false
|
||||||
import ExUnit.CaptureLog
|
import ExUnit.CaptureLog
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Mix.Tasks.Pleroma.EctoTest do
|
defmodule Mix.Tasks.Pleroma.EctoTest do
|
||||||
use ExUnit.Case, async: true
|
use ExUnit.Case, async: false
|
||||||
|
|
||||||
test "raise on bad path" do
|
test "raise on bad path" do
|
||||||
assert_raise RuntimeError, ~r/Could not find migrations directory/, fn ->
|
assert_raise RuntimeError, ~r/Could not find migrations directory/, fn ->
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Mix.Tasks.Pleroma.EmojiTest do
|
defmodule Mix.Tasks.Pleroma.EmojiTest do
|
||||||
use ExUnit.Case, async: true
|
use ExUnit.Case, async: false
|
||||||
|
|
||||||
import ExUnit.CaptureIO
|
import ExUnit.CaptureIO
|
||||||
import Tesla.Mock
|
import Tesla.Mock
|
||||||
|
|
|
@ -41,6 +41,26 @@ defmodule Pleroma.ActivityTest do
|
||||||
assert activity == found_activity
|
assert activity == found_activity
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "returns activities by object's AP id in requested presorted order" do
|
||||||
|
a1 = insert(:note_activity)
|
||||||
|
o1 = Object.normalize(a1, fetch: false).data["id"]
|
||||||
|
|
||||||
|
a2 = insert(:note_activity)
|
||||||
|
o2 = Object.normalize(a2, fetch: false).data["id"]
|
||||||
|
|
||||||
|
a3 = insert(:note_activity)
|
||||||
|
o3 = Object.normalize(a3, fetch: false).data["id"]
|
||||||
|
|
||||||
|
a4 = insert(:note_activity)
|
||||||
|
o4 = Object.normalize(a4, fetch: false).data["id"]
|
||||||
|
|
||||||
|
found_activities =
|
||||||
|
Activity.get_presorted_create_by_object_ap_id([o3, o2, o4, o1])
|
||||||
|
|> Repo.all()
|
||||||
|
|
||||||
|
assert found_activities == [a3, a2, a4, a1]
|
||||||
|
end
|
||||||
|
|
||||||
test "preloading a bookmark" do
|
test "preloading a bookmark" do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
user2 = insert(:user)
|
user2 = insert(:user)
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
defmodule Pleroma.Emoji.FormatterTest do
|
defmodule Pleroma.Emoji.FormatterTest do
|
||||||
alias Pleroma.Emoji.Formatter
|
alias Pleroma.Emoji.Formatter
|
||||||
use Pleroma.DataCase, async: true
|
use Pleroma.DataCase, async: false
|
||||||
|
|
||||||
describe "emojify" do
|
describe "emojify" do
|
||||||
test "it adds cool emoji" do
|
test "it adds cool emoji" do
|
||||||
|
|
105
test/pleroma/http/backoff_test.exs
Normal file
105
test/pleroma/http/backoff_test.exs
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
defmodule Pleroma.HTTP.BackoffTest do
|
||||||
|
@backoff_cache :http_backoff_cache
|
||||||
|
use Pleroma.DataCase, async: false
|
||||||
|
alias Pleroma.HTTP.Backoff
|
||||||
|
|
||||||
|
defp within_tolerance?(ttl, expected) do
|
||||||
|
ttl > expected - 15 and ttl < expected + 15
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "get/3" do
|
||||||
|
test "should return {:ok, env} when not rate limited" do
|
||||||
|
Tesla.Mock.mock_global(fn
|
||||||
|
%Tesla.Env{url: "https://akkoma.dev/api/v1/instance"} ->
|
||||||
|
{:ok, %Tesla.Env{status: 200, body: "ok"}}
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert {:ok, env} = Backoff.get("https://akkoma.dev/api/v1/instance")
|
||||||
|
assert env.status == 200
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should return {:error, env} when rate limited" do
|
||||||
|
# Shove a value into the cache to simulate a rate limit
|
||||||
|
Cachex.put(@backoff_cache, "akkoma.dev", true)
|
||||||
|
assert {:error, :ratelimit} = Backoff.get("https://akkoma.dev/api/v1/instance")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should insert a value into the cache when rate limited" do
|
||||||
|
Tesla.Mock.mock_global(fn
|
||||||
|
%Tesla.Env{url: "https://ratelimited.dev/api/v1/instance"} ->
|
||||||
|
{:ok, %Tesla.Env{status: 429, body: "Rate limited"}}
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert {:error, :ratelimit} = Backoff.get("https://ratelimited.dev/api/v1/instance")
|
||||||
|
assert {:ok, true} = Cachex.get(@backoff_cache, "ratelimited.dev")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should insert a value into the cache when rate limited with a 503 response" do
|
||||||
|
Tesla.Mock.mock_global(fn
|
||||||
|
%Tesla.Env{url: "https://ratelimited.dev/api/v1/instance"} ->
|
||||||
|
{:ok, %Tesla.Env{status: 503, body: "Rate limited"}}
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert {:error, :ratelimit} = Backoff.get("https://ratelimited.dev/api/v1/instance")
|
||||||
|
assert {:ok, true} = Cachex.get(@backoff_cache, "ratelimited.dev")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should parse the value of x-ratelimit-reset, if present" do
|
||||||
|
ten_minutes_from_now =
|
||||||
|
DateTime.utc_now() |> Timex.shift(minutes: 10) |> DateTime.to_iso8601()
|
||||||
|
|
||||||
|
Tesla.Mock.mock_global(fn
|
||||||
|
%Tesla.Env{url: "https://ratelimited.dev/api/v1/instance"} ->
|
||||||
|
{:ok,
|
||||||
|
%Tesla.Env{
|
||||||
|
status: 429,
|
||||||
|
body: "Rate limited",
|
||||||
|
headers: [{"x-ratelimit-reset", ten_minutes_from_now}]
|
||||||
|
}}
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert {:error, :ratelimit} = Backoff.get("https://ratelimited.dev/api/v1/instance")
|
||||||
|
assert {:ok, true} = Cachex.get(@backoff_cache, "ratelimited.dev")
|
||||||
|
{:ok, ttl} = Cachex.ttl(@backoff_cache, "ratelimited.dev")
|
||||||
|
assert within_tolerance?(ttl, 600)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should parse the value of retry-after when it's a timestamp" do
|
||||||
|
ten_minutes_from_now =
|
||||||
|
DateTime.utc_now() |> Timex.shift(minutes: 10) |> DateTime.to_iso8601()
|
||||||
|
|
||||||
|
Tesla.Mock.mock_global(fn
|
||||||
|
%Tesla.Env{url: "https://ratelimited.dev/api/v1/instance"} ->
|
||||||
|
{:ok,
|
||||||
|
%Tesla.Env{
|
||||||
|
status: 429,
|
||||||
|
body: "Rate limited",
|
||||||
|
headers: [{"retry-after", ten_minutes_from_now}]
|
||||||
|
}}
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert {:error, :ratelimit} = Backoff.get("https://ratelimited.dev/api/v1/instance")
|
||||||
|
assert {:ok, true} = Cachex.get(@backoff_cache, "ratelimited.dev")
|
||||||
|
{:ok, ttl} = Cachex.ttl(@backoff_cache, "ratelimited.dev")
|
||||||
|
assert within_tolerance?(ttl, 600)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should parse the value of retry-after when it's a number of seconds" do
|
||||||
|
Tesla.Mock.mock_global(fn
|
||||||
|
%Tesla.Env{url: "https://ratelimited.dev/api/v1/instance"} ->
|
||||||
|
{:ok,
|
||||||
|
%Tesla.Env{
|
||||||
|
status: 429,
|
||||||
|
body: "Rate limited",
|
||||||
|
headers: [{"retry-after", "600"}]
|
||||||
|
}}
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert {:error, :ratelimit} = Backoff.get("https://ratelimited.dev/api/v1/instance")
|
||||||
|
assert {:ok, true} = Cachex.get(@backoff_cache, "ratelimited.dev")
|
||||||
|
# assert that the value is 10 minutes from now
|
||||||
|
{:ok, ttl} = Cachex.ttl(@backoff_cache, "ratelimited.dev")
|
||||||
|
assert within_tolerance?(ttl, 600)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -153,6 +153,11 @@ defmodule Pleroma.SignatureTest do
|
||||||
{:ok, "https://example.com/users/1234"}
|
{:ok, "https://example.com/users/1234"}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it deduces the actor ID for bridgy" do
|
||||||
|
assert Signature.key_id_to_actor_id("https://example.com/1234#key") ==
|
||||||
|
{:ok, "https://example.com/1234"}
|
||||||
|
end
|
||||||
|
|
||||||
test "it calls webfinger for 'acct:' accounts" do
|
test "it calls webfinger for 'acct:' accounts" do
|
||||||
with_mock(Pleroma.Web.WebFinger,
|
with_mock(Pleroma.Web.WebFinger,
|
||||||
finger: fn _ -> {:ok, %{"ap_id" => "https://gensokyo.2hu/users/raymoo"}} end
|
finger: fn _ -> {:ok, %{"ap_id" => "https://gensokyo.2hu/users/raymoo"}} end
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Pleroma.Upload.Filter.Exiftool.ReadDescriptionTest do
|
defmodule Pleroma.Upload.Filter.Exiftool.ReadDescriptionTest do
|
||||||
use Pleroma.DataCase, async: true
|
use Pleroma.DataCase, async: false
|
||||||
alias Pleroma.Upload.Filter
|
alias Pleroma.Upload.Filter
|
||||||
|
|
||||||
@uploads %Pleroma.Upload{
|
@uploads %Pleroma.Upload{
|
||||||
|
|
|
@ -765,109 +765,19 @@ defmodule Pleroma.UserTest do
|
||||||
setup do: clear_config([Pleroma.Web.WebFinger, :update_nickname_on_user_fetch], true)
|
setup do: clear_config([Pleroma.Web.WebFinger, :update_nickname_on_user_fetch], true)
|
||||||
|
|
||||||
test "for mastodon" do
|
test "for mastodon" do
|
||||||
Tesla.Mock.mock(fn
|
ap_id = "a@mastodon.example"
|
||||||
%{url: "https://example.com/.well-known/host-meta"} ->
|
|
||||||
%Tesla.Env{
|
|
||||||
status: 302,
|
|
||||||
headers: [{"location", "https://sub.example.com/.well-known/host-meta"}]
|
|
||||||
}
|
|
||||||
|
|
||||||
%{url: "https://sub.example.com/.well-known/host-meta"} ->
|
|
||||||
%Tesla.Env{
|
|
||||||
status: 200,
|
|
||||||
body:
|
|
||||||
"test/fixtures/webfinger/masto-host-meta.xml"
|
|
||||||
|> File.read!()
|
|
||||||
|> String.replace("{{domain}}", "sub.example.com")
|
|
||||||
}
|
|
||||||
|
|
||||||
%{url: "https://sub.example.com/.well-known/webfinger?resource=acct:a@example.com"} ->
|
|
||||||
%Tesla.Env{
|
|
||||||
status: 200,
|
|
||||||
body:
|
|
||||||
"test/fixtures/webfinger/masto-webfinger.json"
|
|
||||||
|> File.read!()
|
|
||||||
|> String.replace("{{nickname}}", "a")
|
|
||||||
|> String.replace("{{domain}}", "example.com")
|
|
||||||
|> String.replace("{{subdomain}}", "sub.example.com"),
|
|
||||||
headers: [{"content-type", "application/jrd+json"}]
|
|
||||||
}
|
|
||||||
|
|
||||||
%{url: "https://sub.example.com/users/a"} ->
|
|
||||||
%Tesla.Env{
|
|
||||||
status: 200,
|
|
||||||
body:
|
|
||||||
"test/fixtures/webfinger/masto-user.json"
|
|
||||||
|> File.read!()
|
|
||||||
|> String.replace("{{nickname}}", "a")
|
|
||||||
|> String.replace("{{domain}}", "sub.example.com"),
|
|
||||||
headers: [{"content-type", "application/activity+json"}]
|
|
||||||
}
|
|
||||||
|
|
||||||
%{url: "https://sub.example.com/users/a/collections/featured"} ->
|
|
||||||
%Tesla.Env{
|
|
||||||
status: 200,
|
|
||||||
body:
|
|
||||||
File.read!("test/fixtures/users_mock/masto_featured.json")
|
|
||||||
|> String.replace("{{domain}}", "sub.example.com")
|
|
||||||
|> String.replace("{{nickname}}", "a"),
|
|
||||||
headers: [{"content-type", "application/activity+json"}]
|
|
||||||
}
|
|
||||||
end)
|
|
||||||
|
|
||||||
ap_id = "a@example.com"
|
|
||||||
{:ok, fetched_user} = User.get_or_fetch(ap_id)
|
{:ok, fetched_user} = User.get_or_fetch(ap_id)
|
||||||
|
|
||||||
assert fetched_user.ap_id == "https://sub.example.com/users/a"
|
assert fetched_user.ap_id == "https://sub.mastodon.example/users/a"
|
||||||
assert fetched_user.nickname == "a@example.com"
|
assert fetched_user.nickname == "a@mastodon.example"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "for pleroma" do
|
test "for pleroma" do
|
||||||
Tesla.Mock.mock(fn
|
ap_id = "a@pleroma.example"
|
||||||
%{url: "https://example.com/.well-known/host-meta"} ->
|
|
||||||
%Tesla.Env{
|
|
||||||
status: 302,
|
|
||||||
headers: [{"location", "https://sub.example.com/.well-known/host-meta"}]
|
|
||||||
}
|
|
||||||
|
|
||||||
%{url: "https://sub.example.com/.well-known/host-meta"} ->
|
|
||||||
%Tesla.Env{
|
|
||||||
status: 200,
|
|
||||||
body:
|
|
||||||
"test/fixtures/webfinger/pleroma-host-meta.xml"
|
|
||||||
|> File.read!()
|
|
||||||
|> String.replace("{{domain}}", "sub.example.com")
|
|
||||||
}
|
|
||||||
|
|
||||||
%{url: "https://sub.example.com/.well-known/webfinger?resource=acct:a@example.com"} ->
|
|
||||||
%Tesla.Env{
|
|
||||||
status: 200,
|
|
||||||
body:
|
|
||||||
"test/fixtures/webfinger/pleroma-webfinger.json"
|
|
||||||
|> File.read!()
|
|
||||||
|> String.replace("{{nickname}}", "a")
|
|
||||||
|> String.replace("{{domain}}", "example.com")
|
|
||||||
|> String.replace("{{subdomain}}", "sub.example.com"),
|
|
||||||
headers: [{"content-type", "application/jrd+json"}]
|
|
||||||
}
|
|
||||||
|
|
||||||
%{url: "https://sub.example.com/users/a"} ->
|
|
||||||
%Tesla.Env{
|
|
||||||
status: 200,
|
|
||||||
body:
|
|
||||||
"test/fixtures/webfinger/pleroma-user.json"
|
|
||||||
|> File.read!()
|
|
||||||
|> String.replace("{{nickname}}", "a")
|
|
||||||
|> String.replace("{{domain}}", "sub.example.com"),
|
|
||||||
headers: [{"content-type", "application/activity+json"}]
|
|
||||||
}
|
|
||||||
end)
|
|
||||||
|
|
||||||
ap_id = "a@example.com"
|
|
||||||
{:ok, fetched_user} = User.get_or_fetch(ap_id)
|
{:ok, fetched_user} = User.get_or_fetch(ap_id)
|
||||||
|
|
||||||
assert fetched_user.ap_id == "https://sub.example.com/users/a"
|
assert fetched_user.ap_id == "https://sub.pleroma.example/users/a"
|
||||||
assert fetched_user.nickname == "a@example.com"
|
assert fetched_user.nickname == "a@pleroma.example"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -233,6 +233,48 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "works for takahe actors" do
|
||||||
|
user_id = "https://fedi.vision/@vote@fedi.vision/"
|
||||||
|
|
||||||
|
Tesla.Mock.mock(fn
|
||||||
|
%{method: :get, url: ^user_id} ->
|
||||||
|
%Tesla.Env{
|
||||||
|
status: 200,
|
||||||
|
body: File.read!("test/fixtures/users_mock/takahe_user.json"),
|
||||||
|
headers: [{"content-type", "application/activity+json"}]
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
|
{:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
|
||||||
|
|
||||||
|
assert user.actor_type == "Person"
|
||||||
|
|
||||||
|
assert [
|
||||||
|
%{
|
||||||
|
"name" => "More details"
|
||||||
|
}
|
||||||
|
] = user.fields
|
||||||
|
end
|
||||||
|
|
||||||
|
test "works for actors with malformed attachment fields" do
|
||||||
|
user_id = "https://fedi.vision/@vote@fedi.vision/"
|
||||||
|
|
||||||
|
Tesla.Mock.mock(fn
|
||||||
|
%{method: :get, url: ^user_id} ->
|
||||||
|
%Tesla.Env{
|
||||||
|
status: 200,
|
||||||
|
body: File.read!("test/fixtures/users_mock/nonsense_attachment_user.json"),
|
||||||
|
headers: [{"content-type", "application/activity+json"}]
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
|
{:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
|
||||||
|
|
||||||
|
assert user.actor_type == "Person"
|
||||||
|
|
||||||
|
assert [] = user.fields
|
||||||
|
end
|
||||||
|
|
||||||
test "fetches user featured collection" do
|
test "fetches user featured collection" do
|
||||||
ap_id = "https://example.com/users/lain"
|
ap_id = "https://example.com/users/lain"
|
||||||
|
|
||||||
|
@ -283,9 +325,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
||||||
body: featured_data,
|
body: featured_data,
|
||||||
headers: [{"content-type", "application/activity+json"}]
|
headers: [{"content-type", "application/activity+json"}]
|
||||||
}
|
}
|
||||||
end)
|
|
||||||
|
|
||||||
Tesla.Mock.mock_global(fn
|
|
||||||
%{
|
%{
|
||||||
method: :get,
|
method: :get,
|
||||||
url: ^object_url
|
url: ^object_url
|
||||||
|
@ -298,7 +338,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
||||||
end)
|
end)
|
||||||
|
|
||||||
{:ok, user} = ActivityPub.make_user_from_ap_id(ap_id)
|
{:ok, user} = ActivityPub.make_user_from_ap_id(ap_id)
|
||||||
Process.sleep(50)
|
# wait for oban
|
||||||
|
Pleroma.Tests.ObanHelpers.perform_all()
|
||||||
|
|
||||||
assert user.featured_address == featured_url
|
assert user.featured_address == featured_url
|
||||||
assert Map.has_key?(user.pinned_objects, object_url)
|
assert Map.has_key?(user.pinned_objects, object_url)
|
||||||
|
|
|
@ -590,41 +590,22 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "attachments_from_ids_descs/2" do
|
|
||||||
test "returns [] when attachment ids is empty" do
|
|
||||||
assert Utils.attachments_from_ids_descs([], "{}") == []
|
|
||||||
end
|
|
||||||
|
|
||||||
test "returns list attachments with desc" do
|
|
||||||
object = insert(:note)
|
|
||||||
desc = Jason.encode!(%{object.id => "test-desc"})
|
|
||||||
|
|
||||||
assert Utils.attachments_from_ids_descs(["#{object.id}", "34"], desc) == [
|
|
||||||
Map.merge(object.data, %{"name" => "test-desc"})
|
|
||||||
]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "attachments_from_ids/1" do
|
describe "attachments_from_ids/1" do
|
||||||
test "returns attachments with descs" do
|
test "returns attachments without descs" do
|
||||||
object = insert(:note)
|
user = insert(:user)
|
||||||
desc = Jason.encode!(%{object.id => "test-desc"})
|
object = insert(:attachment, user: user)
|
||||||
|
assert Utils.attachments_from_ids(user, %{media_ids: ["#{object.id}"]}) == [object.data]
|
||||||
assert Utils.attachments_from_ids(%{
|
|
||||||
media_ids: ["#{object.id}"],
|
|
||||||
descriptions: desc
|
|
||||||
}) == [
|
|
||||||
Map.merge(object.data, %{"name" => "test-desc"})
|
|
||||||
]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns attachments without descs" do
|
test "returns [] when passed non-media object ids" do
|
||||||
object = insert(:note)
|
user = insert(:user)
|
||||||
assert Utils.attachments_from_ids(%{media_ids: ["#{object.id}"]}) == [object.data]
|
object = insert(:note, user: user)
|
||||||
|
assert Utils.attachments_from_ids(user, %{media_ids: ["#{object.id}"]}) == []
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns [] when not pass media_ids" do
|
test "returns [] when not pass media_ids" do
|
||||||
assert Utils.attachments_from_ids(%{}) == []
|
user = insert(:user)
|
||||||
|
assert Utils.attachments_from_ids(user, %{}) == []
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.MediaControllerTest do
|
||||||
use Pleroma.Web.ConnCase, async: false
|
use Pleroma.Web.ConnCase, async: false
|
||||||
|
|
||||||
import ExUnit.CaptureLog
|
import ExUnit.CaptureLog
|
||||||
|
import Pleroma.Factory
|
||||||
|
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
|
@ -174,6 +175,18 @@ defmodule Pleroma.Web.MastodonAPI.MediaControllerTest do
|
||||||
assert media["description"] == "test-media"
|
assert media["description"] == "test-media"
|
||||||
assert refresh_record(object).data["name"] == "test-media"
|
assert refresh_record(object).data["name"] == "test-media"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "won't update non-media", %{conn: conn, user: user} do
|
||||||
|
object = insert(:note, user: user)
|
||||||
|
|
||||||
|
response =
|
||||||
|
conn
|
||||||
|
|> put_req_header("content-type", "multipart/form-data")
|
||||||
|
|> put("/api/v1/media/#{object.id}", %{"description" => "test-media"})
|
||||||
|
|> json_response(404)
|
||||||
|
|
||||||
|
assert response == %{"error" => "Record not found"}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "Get media by id (/api/v1/media/:id)" do
|
describe "Get media by id (/api/v1/media/:id)" do
|
||||||
|
@ -207,6 +220,17 @@ defmodule Pleroma.Web.MastodonAPI.MediaControllerTest do
|
||||||
assert media["id"]
|
assert media["id"]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it returns 404 when requesting non-media object", %{conn: conn, user: user} do
|
||||||
|
object = insert(:note, user: user)
|
||||||
|
|
||||||
|
response =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/media/#{object.id}")
|
||||||
|
|> json_response(404)
|
||||||
|
|
||||||
|
assert response == %{"error" => "Record not found"}
|
||||||
|
end
|
||||||
|
|
||||||
test "it returns 403 if media object requested by non-owner", %{object: object, user: user} do
|
test "it returns 403 if media object requested by non-owner", %{object: object, user: user} do
|
||||||
%{conn: conn, user: other_user} = oauth_access(["read:media"])
|
%{conn: conn, user: other_user} = oauth_access(["read:media"])
|
||||||
|
|
||||||
|
|
|
@ -220,6 +220,28 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
|
||||||
assert json_response_and_validate_schema(conn, 200)
|
assert json_response_and_validate_schema(conn, 200)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "refuses to post non-owned media", %{conn: conn} do
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
file = %Plug.Upload{
|
||||||
|
content_type: "image/jpeg",
|
||||||
|
path: Path.absname("test/fixtures/image.jpg"),
|
||||||
|
filename: "an_image.jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
{:ok, upload} = ActivityPub.upload(file, actor: other_user.ap_id)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> put_req_header("content-type", "application/json")
|
||||||
|
|> post("/api/v1/statuses", %{
|
||||||
|
"status" => "mew",
|
||||||
|
"media_ids" => [to_string(upload.id)]
|
||||||
|
})
|
||||||
|
|
||||||
|
assert json_response(conn, 422) == %{"error" => "forbidden"}
|
||||||
|
end
|
||||||
|
|
||||||
test "posting a status with an invalid language", %{conn: conn} do
|
test "posting a status with an invalid language", %{conn: conn} do
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|
@ -569,6 +591,29 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
|
||||||
assert %{"type" => "image"} = media_attachment
|
assert %{"type" => "image"} = media_attachment
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "refuses to schedule post with non-owned media", %{conn: conn} do
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
file = %Plug.Upload{
|
||||||
|
content_type: "image/jpeg",
|
||||||
|
path: Path.absname("test/fixtures/image.jpg"),
|
||||||
|
filename: "an_image.jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
{:ok, upload} = ActivityPub.upload(file, actor: other_user.ap_id)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> put_req_header("content-type", "application/json")
|
||||||
|
|> post("/api/v1/statuses", %{
|
||||||
|
"status" => "mew",
|
||||||
|
"scheduled_at" => DateTime.add(DateTime.utc_now(), 6, :minute),
|
||||||
|
"media_ids" => [to_string(upload.id)]
|
||||||
|
})
|
||||||
|
|
||||||
|
assert json_response(conn, 403) == %{"error" => "Access denied"}
|
||||||
|
end
|
||||||
|
|
||||||
test "skips the scheduling and creates the activity if scheduled_at is earlier than 5 minutes from now",
|
test "skips the scheduling and creates the activity if scheduled_at is earlier than 5 minutes from now",
|
||||||
%{conn: conn} do
|
%{conn: conn} do
|
||||||
scheduled_at =
|
scheduled_at =
|
||||||
|
@ -2406,6 +2451,25 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
|
||||||
assert [%{"id" => ^attachment_id}] = response["media_attachments"]
|
assert [%{"id" => ^attachment_id}] = response["media_attachments"]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it does not update to non-owned attachments", %{conn: conn, user: user} do
|
||||||
|
other_user = insert(:user)
|
||||||
|
attachment = insert(:attachment, user: other_user)
|
||||||
|
attachment_id = to_string(attachment.id)
|
||||||
|
|
||||||
|
{:ok, activity} = CommonAPI.post(user, %{status: "mew mew #abc", spoiler_text: "#def"})
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> put_req_header("content-type", "application/json")
|
||||||
|
|> put("/api/v1/statuses/#{activity.id}", %{
|
||||||
|
"status" => "mew mew #abc",
|
||||||
|
"spoiler_text" => "#def",
|
||||||
|
"media_ids" => [attachment_id]
|
||||||
|
})
|
||||||
|
|
||||||
|
assert json_response(conn, 400) == %{"error" => "internal_server_error"}
|
||||||
|
end
|
||||||
|
|
||||||
test "it does not update visibility", %{conn: conn, user: user} do
|
test "it does not update visibility", %{conn: conn, user: user} do
|
||||||
{:ok, activity} =
|
{:ok, activity} =
|
||||||
CommonAPI.post(user, %{
|
CommonAPI.post(user, %{
|
||||||
|
|
|
@ -47,8 +47,7 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityViewTest do
|
||||||
expected = %{
|
expected = %{
|
||||||
id: to_string(scheduled_activity.id),
|
id: to_string(scheduled_activity.id),
|
||||||
media_attachments:
|
media_attachments:
|
||||||
%{media_ids: [upload.id]}
|
Utils.attachments_from_ids(user, %{media_ids: [upload.id]})
|
||||||
|> Utils.attachments_from_ids()
|
|
||||||
|> Enum.map(&StatusView.render("attachment.json", %{attachment: &1})),
|
|> Enum.map(&StatusView.render("attachment.json", %{attachment: &1})),
|
||||||
params: %{
|
params: %{
|
||||||
in_reply_to_id: to_string(activity.id),
|
in_reply_to_id: to_string(activity.id),
|
||||||
|
|
|
@ -50,12 +50,7 @@ defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "reach user on tld, while pleroma is runned on subdomain" do
|
test "reach user on tld, while pleroma is running on subdomain" do
|
||||||
Pleroma.Web.Endpoint.config_change(
|
|
||||||
[{Pleroma.Web.Endpoint, url: [host: "sub.example.com"]}],
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
clear_config([Pleroma.Web.Endpoint, :url, :host], "sub.example.com")
|
clear_config([Pleroma.Web.Endpoint, :url, :host], "sub.example.com")
|
||||||
|
|
||||||
clear_config([Pleroma.Web.WebFinger, :domain], "example.com")
|
clear_config([Pleroma.Web.WebFinger, :domain], "example.com")
|
||||||
|
|
|
@ -9,7 +9,7 @@ defmodule Pleroma.Web.WebFingerTest do
|
||||||
import Tesla.Mock
|
import Tesla.Mock
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
|
mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -76,15 +76,6 @@ defmodule Pleroma.Web.WebFingerTest do
|
||||||
{:ok, _data} = WebFinger.finger(user)
|
{:ok, _data} = WebFinger.finger(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns the ActivityPub actor URI and subscribe address for an ActivityPub user with the ld+json mimetype" do
|
|
||||||
user = "kaniini@gerzilla.de"
|
|
||||||
|
|
||||||
{:ok, data} = WebFinger.finger(user)
|
|
||||||
|
|
||||||
assert data["ap_id"] == "https://gerzilla.de/channel/kaniini"
|
|
||||||
assert data["subscribe_address"] == "https://gerzilla.de/follow?f=&url={uri}"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "it work for AP-only user" do
|
test "it work for AP-only user" do
|
||||||
user = "kpherox@mstdn.jp"
|
user = "kpherox@mstdn.jp"
|
||||||
|
|
||||||
|
@ -99,12 +90,6 @@ defmodule Pleroma.Web.WebFingerTest do
|
||||||
assert data["subscribe_address"] == "https://mstdn.jp/authorize_interaction?acct={uri}"
|
assert data["subscribe_address"] == "https://mstdn.jp/authorize_interaction?acct={uri}"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it works for friendica" do
|
|
||||||
user = "lain@squeet.me"
|
|
||||||
|
|
||||||
{:ok, _data} = WebFinger.finger(user)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "it gets the xrd endpoint" do
|
test "it gets the xrd endpoint" do
|
||||||
{:ok, template} = WebFinger.find_lrdd_template("social.heldscal.la")
|
{:ok, template} = WebFinger.find_lrdd_template("social.heldscal.la")
|
||||||
|
|
||||||
|
@ -180,5 +165,44 @@ defmodule Pleroma.Web.WebFingerTest do
|
||||||
|
|
||||||
{:ok, _data} = WebFinger.finger("pekorino@pawoo.net")
|
{:ok, _data} = WebFinger.finger("pekorino@pawoo.net")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "prevents spoofing" do
|
||||||
|
Tesla.Mock.mock(fn
|
||||||
|
%{
|
||||||
|
url: "https://bad.com/.well-known/webfinger?resource=acct:meanie@bad.com"
|
||||||
|
} ->
|
||||||
|
{:ok,
|
||||||
|
%Tesla.Env{
|
||||||
|
status: 200,
|
||||||
|
body: File.read!("test/fixtures/tesla_mock/webfinger_spoof.json"),
|
||||||
|
headers: [{"content-type", "application/jrd+json"}]
|
||||||
|
}}
|
||||||
|
|
||||||
|
%{url: "https://bad.com/.well-known/host-meta"} ->
|
||||||
|
{:ok,
|
||||||
|
%Tesla.Env{
|
||||||
|
status: 200,
|
||||||
|
body: File.read!("test/fixtures/tesla_mock/bad.com_host_meta")
|
||||||
|
}}
|
||||||
|
end)
|
||||||
|
|
||||||
|
{:error, _data} = WebFinger.finger("meanie@bad.com")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag capture_log: true
|
||||||
|
test "prevents forgeries" do
|
||||||
|
Tesla.Mock.mock(fn
|
||||||
|
%{url: "https://bad.com/.well-known/webfinger?resource=acct:meanie@bad.com"} ->
|
||||||
|
fake_webfinger =
|
||||||
|
File.read!("test/fixtures/webfinger/imposter-webfinger.json") |> Jason.decode!()
|
||||||
|
|
||||||
|
Tesla.Mock.json(fake_webfinger)
|
||||||
|
|
||||||
|
%{url: "https://bad.com/.well-known/host-meta"} ->
|
||||||
|
{:ok, %Tesla.Env{status: 404}}
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert {:error, {:webfinger_invalid, _, _}} = WebFinger.finger("meanie@bad.com")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1464,6 +1464,177 @@ defmodule HttpRequestMock do
|
||||||
}}
|
}}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get("https://google.com/", _, _, _) do
|
||||||
|
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/google.html")}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def get("https://yahoo.com/", _, _, _) do
|
||||||
|
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/yahoo.html")}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def get("https://example.com/error", _, _, _), do: {:error, :overload}
|
||||||
|
|
||||||
|
def get("https://example.com/ogp-missing-title", _, _, _) do
|
||||||
|
{:ok,
|
||||||
|
%Tesla.Env{
|
||||||
|
status: 200,
|
||||||
|
body: File.read!("test/fixtures/rich_media/ogp-missing-title.html")
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def get("https://example.com/oembed", _, _, _) do
|
||||||
|
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/oembed.html")}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def get("https://example.com/oembed.json", _, _, _) do
|
||||||
|
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/oembed.json")}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def get("https://example.com/twitter-card", _, _, _) do
|
||||||
|
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/twitter_card.html")}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def get("https://example.com/non-ogp", _, _, _) do
|
||||||
|
{:ok,
|
||||||
|
%Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/non_ogp_embed.html")}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def get("https://example.com/empty", _, _, _) do
|
||||||
|
{:ok, %Tesla.Env{status: 200, body: "hello"}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def get("https://friends.grishka.me/posts/54642", _, _, _) do
|
||||||
|
{:ok,
|
||||||
|
%Tesla.Env{
|
||||||
|
status: 200,
|
||||||
|
body: File.read!("test/fixtures/tesla_mock/smithereen_non_anonymous_poll.json"),
|
||||||
|
headers: activitypub_object_headers()
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def get("https://friends.grishka.me/users/1", _, _, _) do
|
||||||
|
{:ok,
|
||||||
|
%Tesla.Env{
|
||||||
|
status: 200,
|
||||||
|
body: File.read!("test/fixtures/tesla_mock/smithereen_user.json"),
|
||||||
|
headers: activitypub_object_headers()
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def get("https://mastodon.example/.well-known/host-meta", _, _, _) do
|
||||||
|
{:ok,
|
||||||
|
%Tesla.Env{
|
||||||
|
status: 302,
|
||||||
|
headers: [{"location", "https://sub.mastodon.example/.well-known/host-meta"}]
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def get("https://sub.mastodon.example/.well-known/host-meta", _, _, _) do
|
||||||
|
{:ok,
|
||||||
|
%Tesla.Env{
|
||||||
|
status: 200,
|
||||||
|
body:
|
||||||
|
"test/fixtures/webfinger/masto-host-meta.xml"
|
||||||
|
|> File.read!()
|
||||||
|
|> String.replace("{{domain}}", "sub.mastodon.example")
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def get(
|
||||||
|
"https://sub.mastodon.example/.well-known/webfinger?resource=acct:a@mastodon.example",
|
||||||
|
_,
|
||||||
|
_,
|
||||||
|
_
|
||||||
|
) do
|
||||||
|
{:ok,
|
||||||
|
%Tesla.Env{
|
||||||
|
status: 200,
|
||||||
|
body:
|
||||||
|
"test/fixtures/webfinger/masto-webfinger.json"
|
||||||
|
|> File.read!()
|
||||||
|
|> String.replace("{{nickname}}", "a")
|
||||||
|
|> String.replace("{{domain}}", "mastodon.example")
|
||||||
|
|> String.replace("{{subdomain}}", "sub.mastodon.example"),
|
||||||
|
headers: [{"content-type", "application/jrd+json"}]
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def get("https://sub.mastodon.example/users/a", _, _, _) do
|
||||||
|
{:ok,
|
||||||
|
%Tesla.Env{
|
||||||
|
status: 200,
|
||||||
|
body:
|
||||||
|
"test/fixtures/webfinger/masto-user.json"
|
||||||
|
|> File.read!()
|
||||||
|
|> String.replace("{{nickname}}", "a")
|
||||||
|
|> String.replace("{{domain}}", "sub.mastodon.example"),
|
||||||
|
headers: [{"content-type", "application/activity+json"}]
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def get("https://sub.mastodon.example/users/a/collections/featured", _, _, _) do
|
||||||
|
{:ok,
|
||||||
|
%Tesla.Env{
|
||||||
|
status: 200,
|
||||||
|
body:
|
||||||
|
File.read!("test/fixtures/users_mock/masto_featured.json")
|
||||||
|
|> String.replace("{{domain}}", "sub.mastodon.example")
|
||||||
|
|> String.replace("{{nickname}}", "a"),
|
||||||
|
headers: [{"content-type", "application/activity+json"}]
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def get("https://pleroma.example/.well-known/host-meta", _, _, _) do
|
||||||
|
{:ok,
|
||||||
|
%Tesla.Env{
|
||||||
|
status: 302,
|
||||||
|
headers: [{"location", "https://sub.pleroma.example/.well-known/host-meta"}]
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def get("https://sub.pleroma.example/.well-known/host-meta", _, _, _) do
|
||||||
|
{:ok,
|
||||||
|
%Tesla.Env{
|
||||||
|
status: 200,
|
||||||
|
body:
|
||||||
|
"test/fixtures/webfinger/pleroma-host-meta.xml"
|
||||||
|
|> File.read!()
|
||||||
|
|> String.replace("{{domain}}", "sub.pleroma.example")
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def get(
|
||||||
|
"https://sub.pleroma.example/.well-known/webfinger?resource=acct:a@pleroma.example",
|
||||||
|
_,
|
||||||
|
_,
|
||||||
|
_
|
||||||
|
) do
|
||||||
|
{:ok,
|
||||||
|
%Tesla.Env{
|
||||||
|
status: 200,
|
||||||
|
body:
|
||||||
|
"test/fixtures/webfinger/pleroma-webfinger.json"
|
||||||
|
|> File.read!()
|
||||||
|
|> String.replace("{{nickname}}", "a")
|
||||||
|
|> String.replace("{{domain}}", "pleroma.example")
|
||||||
|
|> String.replace("{{subdomain}}", "sub.pleroma.example"),
|
||||||
|
headers: [{"content-type", "application/jrd+json"}]
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def get("https://sub.pleroma.example/users/a", _, _, _) do
|
||||||
|
{:ok,
|
||||||
|
%Tesla.Env{
|
||||||
|
status: 200,
|
||||||
|
body:
|
||||||
|
"test/fixtures/webfinger/pleroma-user.json"
|
||||||
|
|> File.read!()
|
||||||
|
|> String.replace("{{nickname}}", "a")
|
||||||
|
|> String.replace("{{domain}}", "sub.pleroma.example"),
|
||||||
|
headers: [{"content-type", "application/activity+json"}]
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
|
||||||
def get(url, query, body, headers) do
|
def get(url, query, body, headers) do
|
||||||
{:error,
|
{:error,
|
||||||
"Mock response not implemented for GET #{inspect(url)}, #{query}, #{inspect(body)}, #{inspect(headers)}"}
|
"Mock response not implemented for GET #{inspect(url)}, #{query}, #{inspect(body)}, #{inspect(headers)}"}
|
||||||
|
|
Loading…
Reference in a new issue