forked from AkkomaGang/akkoma
Merge branch 'develop' into feature/expire-mutes
This commit is contained in:
commit
4987ee6256
113 changed files with 4510 additions and 348 deletions
35
CHANGELOG.md
35
CHANGELOG.md
|
@ -5,12 +5,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Experimental websocket-based federation between Pleroma instances.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Renamed `:await_up_timeout` in `:connections_pool` namespace to `:connect_timeout`, old name is deprecated.
|
- Renamed `:await_up_timeout` in `:connections_pool` namespace to `:connect_timeout`, old name is deprecated.
|
||||||
- Renamed `:timeout` in `pools` namespace to `:recv_timeout`, old name is deprecated.
|
- Renamed `:timeout` in `pools` namespace to `:recv_timeout`, old name is deprecated.
|
||||||
|
- The `discoverable` field in the `User` struct will now add a NOINDEX metatag to profile pages when false.
|
||||||
|
- Users with the `discoverable` field set to false will not show up in searches.
|
||||||
- Minimum lifetime for ephmeral activities changed to 10 minutes and made configurable (`:min_lifetime` option).
|
- Minimum lifetime for ephmeral activities changed to 10 minutes and made configurable (`:min_lifetime` option).
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Media preview proxy (requires media proxy be enabled; see `:media_preview_proxy` config for more details).
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
- **Breaking:** `Pleroma.Workers.Cron.StatsWorker` setting from Oban `:crontab` (moved to a simpler implementation).
|
- **Breaking:** `Pleroma.Workers.Cron.StatsWorker` setting from Oban `:crontab` (moved to a simpler implementation).
|
||||||
|
@ -19,12 +27,27 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Removed `:managed_config` option. In practice, it was accidentally removed with 2.0.0 release when frontends were
|
- Removed `:managed_config` option. In practice, it was accidentally removed with 2.0.0 release when frontends were
|
||||||
switched to a new configuration mechanism, however it was not officially removed until now.
|
switched to a new configuration mechanism, however it was not officially removed until now.
|
||||||
|
|
||||||
## unreleased-patch - ???
|
## [2.1.2] - 2020-09-17
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Fix most MRF rules either crashing or not being applied to objects passed into the Common Pipeline (ChatMessage, Question, Answer, Audio, Event).
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Welcome Chat messages preventing user registration with MRF Simple Policy applied to the local instance
|
- Welcome Chat messages preventing user registration with MRF Simple Policy applied to the local instance.
|
||||||
- Mastodon API: the public timeline returning an error when the `reply_visibility` parameter is set to `self` for an unauthenticated user
|
- Mastodon API: the public timeline returning an error when the `reply_visibility` parameter is set to `self` for an unauthenticated user.
|
||||||
|
- Mastodon Streaming API: Handler crashes on authentication failures, resulting in error logs.
|
||||||
|
- Mastodon Streaming API: Error logs on client pings.
|
||||||
|
- Rich media: Log spam on failures. Now the error is only logged once per attempt.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Rich Media: A HEAD request is now done to the url, to ensure it has the appropriate content type and size before proceeding with a GET.
|
||||||
|
|
||||||
|
### Upgrade notes
|
||||||
|
|
||||||
|
1. Restart Pleroma
|
||||||
|
|
||||||
## [2.1.1] - 2020-09-08
|
## [2.1.1] - 2020-09-08
|
||||||
|
|
||||||
|
@ -41,6 +64,12 @@ switched to a new configuration mechanism, however it was not officially removed
|
||||||
### Added
|
### Added
|
||||||
- Rich media failure tracking (along with `:failure_backoff` option).
|
- Rich media failure tracking (along with `:failure_backoff` option).
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Admin API Changes</summary>
|
||||||
|
|
||||||
|
- Add `PATCH /api/pleroma/admin/instance_document/:document_name` to modify the Terms of Service and Instance Panel HTML pages via Admin API
|
||||||
|
</details>
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Default HTTP adapter not respecting pool setting, leading to possible OOM.
|
- Default HTTP adapter not respecting pool setting, leading to possible OOM.
|
||||||
- Fixed uploading webp images when the Exiftool Upload Filter is enabled by skipping them
|
- Fixed uploading webp images when the Exiftool Upload Filter is enabled by skipping them
|
||||||
|
|
|
@ -130,6 +130,7 @@
|
||||||
dispatch: [
|
dispatch: [
|
||||||
{:_,
|
{:_,
|
||||||
[
|
[
|
||||||
|
{"/api/fedsocket/v1", Pleroma.Web.FedSockets.IncomingHandler, []},
|
||||||
{"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
|
{"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
|
||||||
{"/websocket", Phoenix.Endpoint.CowboyWebSocket,
|
{"/websocket", Phoenix.Endpoint.CowboyWebSocket,
|
||||||
{Phoenix.Transports.WebSocket,
|
{Phoenix.Transports.WebSocket,
|
||||||
|
@ -148,6 +149,16 @@
|
||||||
"SameSite=Lax"
|
"SameSite=Lax"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
config :pleroma, :fed_sockets,
|
||||||
|
enabled: false,
|
||||||
|
connection_duration: :timer.hours(8),
|
||||||
|
rejection_duration: :timer.minutes(15),
|
||||||
|
fed_socket_fetches: [
|
||||||
|
default: 12_000,
|
||||||
|
interval: 3_000,
|
||||||
|
lazy: false
|
||||||
|
]
|
||||||
|
|
||||||
# Configures Elixir's Logger
|
# Configures Elixir's Logger
|
||||||
config :logger, :console,
|
config :logger, :console,
|
||||||
level: :debug,
|
level: :debug,
|
||||||
|
@ -423,6 +434,8 @@
|
||||||
proxy_opts: [
|
proxy_opts: [
|
||||||
redirect_on_failure: false,
|
redirect_on_failure: false,
|
||||||
max_body_length: 25 * 1_048_576,
|
max_body_length: 25 * 1_048_576,
|
||||||
|
# Note: max_read_duration defaults to Pleroma.ReverseProxy.max_read_duration_default/1
|
||||||
|
max_read_duration: 30_000,
|
||||||
http: [
|
http: [
|
||||||
follow_redirect: true,
|
follow_redirect: true,
|
||||||
pool: :media
|
pool: :media
|
||||||
|
@ -437,6 +450,14 @@
|
||||||
|
|
||||||
config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Script, script_path: nil
|
config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Script, script_path: nil
|
||||||
|
|
||||||
|
# Note: media preview proxy depends on media proxy to be enabled
|
||||||
|
config :pleroma, :media_preview_proxy,
|
||||||
|
enabled: false,
|
||||||
|
thumbnail_max_width: 600,
|
||||||
|
thumbnail_max_height: 600,
|
||||||
|
image_quality: 85,
|
||||||
|
min_content_length: 100 * 1024
|
||||||
|
|
||||||
config :pleroma, :chat, enabled: true
|
config :pleroma, :chat, enabled: true
|
||||||
|
|
||||||
config :phoenix, :format_encoders, json: Jason
|
config :phoenix, :format_encoders, json: Jason
|
||||||
|
@ -532,6 +553,7 @@
|
||||||
token_expiration: 5,
|
token_expiration: 5,
|
||||||
federator_incoming: 50,
|
federator_incoming: 50,
|
||||||
federator_outgoing: 50,
|
federator_outgoing: 50,
|
||||||
|
ingestion_queue: 50,
|
||||||
web_push: 50,
|
web_push: 50,
|
||||||
mailer: 10,
|
mailer: 10,
|
||||||
transmogrifier: 20,
|
transmogrifier: 20,
|
||||||
|
@ -742,8 +764,8 @@
|
||||||
],
|
],
|
||||||
media: [
|
media: [
|
||||||
size: 50,
|
size: 50,
|
||||||
max_waiting: 10,
|
max_waiting: 20,
|
||||||
recv_timeout: 10_000
|
recv_timeout: 15_000
|
||||||
],
|
],
|
||||||
upload: [
|
upload: [
|
||||||
size: 25,
|
size: 25,
|
||||||
|
@ -788,6 +810,8 @@
|
||||||
|
|
||||||
config :ex_aws, http_client: Pleroma.HTTP.ExAws
|
config :ex_aws, http_client: Pleroma.HTTP.ExAws
|
||||||
|
|
||||||
|
config :web_push_encryption, http_client: Pleroma.HTTP
|
||||||
|
|
||||||
config :pleroma, :instances_favicons, enabled: false
|
config :pleroma, :instances_favicons, enabled: false
|
||||||
|
|
||||||
config :floki, :html_parser, Floki.HTMLParser.FastHtml
|
config :floki, :html_parser, Floki.HTMLParser.FastHtml
|
||||||
|
|
|
@ -270,6 +270,19 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
%{
|
||||||
|
group: :pleroma,
|
||||||
|
key: :fed_sockets,
|
||||||
|
type: :group,
|
||||||
|
description: "Websocket based federation",
|
||||||
|
children: [
|
||||||
|
%{
|
||||||
|
key: :enabled,
|
||||||
|
type: :boolean,
|
||||||
|
description: "Enable FedSockets"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
%{
|
%{
|
||||||
group: :pleroma,
|
group: :pleroma,
|
||||||
key: Pleroma.Emails.Mailer,
|
key: Pleroma.Emails.Mailer,
|
||||||
|
@ -1874,6 +1887,7 @@
|
||||||
suggestions: [
|
suggestions: [
|
||||||
redirect_on_failure: false,
|
redirect_on_failure: false,
|
||||||
max_body_length: 25 * 1_048_576,
|
max_body_length: 25 * 1_048_576,
|
||||||
|
max_read_duration: 30_000,
|
||||||
http: [
|
http: [
|
||||||
follow_redirect: true,
|
follow_redirect: true,
|
||||||
pool: :media
|
pool: :media
|
||||||
|
@ -1894,6 +1908,11 @@
|
||||||
"Limits the content length to be approximately the " <>
|
"Limits the content length to be approximately the " <>
|
||||||
"specified length. It is validated with the `content-length` header and also verified when proxying."
|
"specified length. It is validated with the `content-length` header and also verified when proxying."
|
||||||
},
|
},
|
||||||
|
%{
|
||||||
|
key: :max_read_duration,
|
||||||
|
type: :integer,
|
||||||
|
description: "Timeout (in milliseconds) of GET request to remote URI."
|
||||||
|
},
|
||||||
%{
|
%{
|
||||||
key: :http,
|
key: :http,
|
||||||
label: "HTTP",
|
label: "HTTP",
|
||||||
|
@ -1940,6 +1959,43 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
%{
|
||||||
|
group: :pleroma,
|
||||||
|
key: :media_preview_proxy,
|
||||||
|
type: :group,
|
||||||
|
description: "Media preview proxy",
|
||||||
|
children: [
|
||||||
|
%{
|
||||||
|
key: :enabled,
|
||||||
|
type: :boolean,
|
||||||
|
description:
|
||||||
|
"Enables proxying of remote media preview to the instance's proxy. Requires enabled media proxy."
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :thumbnail_max_width,
|
||||||
|
type: :integer,
|
||||||
|
description:
|
||||||
|
"Max width of preview thumbnail for images (video preview always has original dimensions)."
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :thumbnail_max_height,
|
||||||
|
type: :integer,
|
||||||
|
description:
|
||||||
|
"Max height of preview thumbnail for images (video preview always has original dimensions)."
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :image_quality,
|
||||||
|
type: :integer,
|
||||||
|
description: "Quality of the output. Ranges from 0 (min quality) to 100 (max quality)."
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :min_content_length,
|
||||||
|
type: :integer,
|
||||||
|
description:
|
||||||
|
"Min content length to perform preview, in bytes. If greater than 0, media smaller in size will be served as is, without thumbnailing."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
%{
|
%{
|
||||||
group: :pleroma,
|
group: :pleroma,
|
||||||
key: Pleroma.Web.MediaProxy.Invalidation.Http,
|
key: Pleroma.Web.MediaProxy.Invalidation.Http,
|
||||||
|
|
|
@ -19,6 +19,11 @@
|
||||||
level: :warn,
|
level: :warn,
|
||||||
format: "\n[$level] $message\n"
|
format: "\n[$level] $message\n"
|
||||||
|
|
||||||
|
config :pleroma, :fed_sockets,
|
||||||
|
enabled: false,
|
||||||
|
connection_duration: 5,
|
||||||
|
rejection_duration: 5
|
||||||
|
|
||||||
config :pleroma, :auth, oauth_consumer_strategies: []
|
config :pleroma, :auth, oauth_consumer_strategies: []
|
||||||
|
|
||||||
config :pleroma, Pleroma.Upload,
|
config :pleroma, Pleroma.Upload,
|
||||||
|
|
|
@ -1334,3 +1334,166 @@ Loads json generated from `config/descriptions.exs`.
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## GET /api/pleroma/admin/users/:nickname/chats
|
||||||
|
|
||||||
|
### List a user's chats
|
||||||
|
|
||||||
|
- Params: None
|
||||||
|
|
||||||
|
- Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"sender": {
|
||||||
|
"id": "someflakeid",
|
||||||
|
"username": "somenick",
|
||||||
|
...
|
||||||
|
},
|
||||||
|
"receiver": {
|
||||||
|
"id": "someflakeid",
|
||||||
|
"username": "somenick",
|
||||||
|
...
|
||||||
|
},
|
||||||
|
"id" : "1",
|
||||||
|
"unread" : 2,
|
||||||
|
"last_message" : {...}, // The last message in that chat
|
||||||
|
"updated_at": "2020-04-21T15:11:46.000Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## GET /api/pleroma/admin/chats/:chat_id
|
||||||
|
|
||||||
|
### View a single chat
|
||||||
|
|
||||||
|
- Params: None
|
||||||
|
|
||||||
|
- Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sender": {
|
||||||
|
"id": "someflakeid",
|
||||||
|
"username": "somenick",
|
||||||
|
...
|
||||||
|
},
|
||||||
|
"receiver": {
|
||||||
|
"id": "someflakeid",
|
||||||
|
"username": "somenick",
|
||||||
|
...
|
||||||
|
},
|
||||||
|
"id" : "1",
|
||||||
|
"unread" : 2,
|
||||||
|
"last_message" : {...}, // The last message in that chat
|
||||||
|
"updated_at": "2020-04-21T15:11:46.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## GET /api/pleroma/admin/chats/:chat_id/messages
|
||||||
|
|
||||||
|
### List the messages in a chat
|
||||||
|
|
||||||
|
- Params: `max_id`, `min_id`
|
||||||
|
|
||||||
|
- Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"account_id": "someflakeid",
|
||||||
|
"chat_id": "1",
|
||||||
|
"content": "Check this out :firefox:",
|
||||||
|
"created_at": "2020-04-21T15:11:46.000Z",
|
||||||
|
"emojis": [
|
||||||
|
{
|
||||||
|
"shortcode": "firefox",
|
||||||
|
"static_url": "https://dontbulling.me/emoji/Firefox.gif",
|
||||||
|
"url": "https://dontbulling.me/emoji/Firefox.gif",
|
||||||
|
"visible_in_picker": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"id": "13",
|
||||||
|
"unread": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account_id": "someflakeid",
|
||||||
|
"chat_id": "1",
|
||||||
|
"content": "Whats' up?",
|
||||||
|
"created_at": "2020-04-21T15:06:45.000Z",
|
||||||
|
"emojis": [],
|
||||||
|
"id": "12",
|
||||||
|
"unread": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## DELETE /api/pleroma/admin/chats/:chat_id/messages/:message_id
|
||||||
|
|
||||||
|
### Delete a single message
|
||||||
|
|
||||||
|
- Params: None
|
||||||
|
|
||||||
|
- Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"account_id": "someflakeid",
|
||||||
|
"chat_id": "1",
|
||||||
|
"content": "Check this out :firefox:",
|
||||||
|
"created_at": "2020-04-21T15:11:46.000Z",
|
||||||
|
"emojis": [
|
||||||
|
{
|
||||||
|
"shortcode": "firefox",
|
||||||
|
"static_url": "https://dontbulling.me/emoji/Firefox.gif",
|
||||||
|
"url": "https://dontbulling.me/emoji/Firefox.gif",
|
||||||
|
"visible_in_picker": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"id": "13",
|
||||||
|
"unread": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `GET /api/pleroma/admin/instance_document/:document_name`
|
||||||
|
|
||||||
|
### Get an instance document
|
||||||
|
|
||||||
|
- Authentication: required
|
||||||
|
|
||||||
|
- Response:
|
||||||
|
|
||||||
|
Returns the content of the document
|
||||||
|
|
||||||
|
```html
|
||||||
|
<h1>Instance panel</h1>
|
||||||
|
```
|
||||||
|
|
||||||
|
## `PATCH /api/pleroma/admin/instance_document/:document_name`
|
||||||
|
- Params:
|
||||||
|
- `file` (the file to be uploaded, using multipart form data.)
|
||||||
|
|
||||||
|
### Update an instance document
|
||||||
|
|
||||||
|
- Authentication: required
|
||||||
|
|
||||||
|
- Response:
|
||||||
|
|
||||||
|
``` json
|
||||||
|
{
|
||||||
|
"url": "https://example.com/instance/panel.html"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `DELETE /api/pleroma/admin/instance_document/:document_name`
|
||||||
|
|
||||||
|
### Delete an instance document
|
||||||
|
|
||||||
|
- Response:
|
||||||
|
|
||||||
|
``` json
|
||||||
|
{
|
||||||
|
"url": "https://example.com/instance/panel.html"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
|
@ -225,6 +225,16 @@ Enables the worker which processes posts scheduled for deletion. Pinned posts ar
|
||||||
|
|
||||||
* `enabled`: whether expired activities will be sent to the job queue to be deleted
|
* `enabled`: whether expired activities will be sent to the job queue to be deleted
|
||||||
|
|
||||||
|
## FedSockets
|
||||||
|
FedSockets is an experimental feature allowing for Pleroma backends to federate using a persistant websocket connection as opposed to making each federation a seperate http connection. This feature is currently off by default. It is configurable throught he following options.
|
||||||
|
|
||||||
|
### :fedsockets
|
||||||
|
* `enabled`: Enables FedSockets for this instance. `false` by default.
|
||||||
|
* `connection_duration`: Time an idle websocket is kept open.
|
||||||
|
* `rejection_duration`: Failures to connect via FedSockets will not be retried for this period of time.
|
||||||
|
* `fed_socket_fetches` and `fed_socket_rejections`: Settings passed to `cachex` for the fetch registry, and rejection stacks. See `Pleroma.Web.FedSockets` for more details.
|
||||||
|
|
||||||
|
|
||||||
## Frontends
|
## Frontends
|
||||||
|
|
||||||
### :frontend_configurations
|
### :frontend_configurations
|
||||||
|
@ -314,6 +324,14 @@ This section describe PWA manifest instance-specific values. Currently this opti
|
||||||
* `enabled`: Enables purge cache
|
* `enabled`: Enables purge cache
|
||||||
* `provider`: Which one of the [purge cache strategy](#purge-cache-strategy) to use.
|
* `provider`: Which one of the [purge cache strategy](#purge-cache-strategy) to use.
|
||||||
|
|
||||||
|
## :media_preview_proxy
|
||||||
|
|
||||||
|
* `enabled`: Enables proxying of remote media preview to the instance’s proxy. Requires enabled media proxy (`media_proxy/enabled`).
|
||||||
|
* `thumbnail_max_width`: Max width of preview thumbnail for images (video preview always has original dimensions).
|
||||||
|
* `thumbnail_max_height`: Max height of preview thumbnail for images (video preview always has original dimensions).
|
||||||
|
* `image_quality`: Quality of the output. Ranges from 0 (min quality) to 100 (max quality).
|
||||||
|
* `min_content_length`: Min content length to perform preview, in bytes. If greater than 0, media smaller in size will be served as is, without thumbnailing.
|
||||||
|
|
||||||
### Purge cache strategy
|
### Purge cache strategy
|
||||||
|
|
||||||
#### Pleroma.Web.MediaProxy.Invalidation.Script
|
#### Pleroma.Web.MediaProxy.Invalidation.Script
|
||||||
|
|
|
@ -32,7 +32,8 @@ def run(["migrate_from_db" | options]) do
|
||||||
|
|
||||||
@spec migrate_to_db(Path.t() | nil) :: any()
|
@spec migrate_to_db(Path.t() | nil) :: any()
|
||||||
def migrate_to_db(file_path \\ nil) do
|
def migrate_to_db(file_path \\ nil) do
|
||||||
if Pleroma.Config.get([:configurable_from_database]) do
|
with true <- Pleroma.Config.get([:configurable_from_database]),
|
||||||
|
:ok <- Pleroma.Config.DeprecationWarnings.warn() do
|
||||||
config_file =
|
config_file =
|
||||||
if file_path do
|
if file_path do
|
||||||
file_path
|
file_path
|
||||||
|
@ -46,7 +47,8 @@ def migrate_to_db(file_path \\ nil) do
|
||||||
|
|
||||||
do_migrate_to_db(config_file)
|
do_migrate_to_db(config_file)
|
||||||
else
|
else
|
||||||
migration_error()
|
:error -> deprecation_error()
|
||||||
|
_ -> migration_error()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -120,6 +122,10 @@ defp migration_error do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp deprecation_error do
|
||||||
|
shell_error("Migration is not allowed until all deprecation warnings have been resolved.")
|
||||||
|
end
|
||||||
|
|
||||||
if Code.ensure_loaded?(Config.Reader) do
|
if Code.ensure_loaded?(Config.Reader) do
|
||||||
defp config_header, do: "import Config\r\n\r\n"
|
defp config_header, do: "import Config\r\n\r\n"
|
||||||
defp read_file(config_file), do: Config.Reader.read_imports!(config_file)
|
defp read_file(config_file), do: Config.Reader.read_imports!(config_file)
|
||||||
|
|
|
@ -99,7 +99,7 @@ def start(_type, _args) do
|
||||||
{Oban, Config.get(Oban)}
|
{Oban, Config.get(Oban)}
|
||||||
] ++
|
] ++
|
||||||
task_children(@env) ++
|
task_children(@env) ++
|
||||||
streamer_child(@env) ++
|
dont_run_in_test(@env) ++
|
||||||
chat_child(@env, chat_enabled?()) ++
|
chat_child(@env, chat_enabled?()) ++
|
||||||
[
|
[
|
||||||
Pleroma.Web.Endpoint,
|
Pleroma.Web.Endpoint,
|
||||||
|
@ -188,16 +188,17 @@ def build_cachex(type, opts),
|
||||||
|
|
||||||
defp chat_enabled?, do: Config.get([:chat, :enabled])
|
defp chat_enabled?, do: Config.get([:chat, :enabled])
|
||||||
|
|
||||||
defp streamer_child(env) when env in [:test, :benchmark], do: []
|
defp dont_run_in_test(env) when env in [:test, :benchmark], do: []
|
||||||
|
|
||||||
defp streamer_child(_) do
|
defp dont_run_in_test(_) do
|
||||||
[
|
[
|
||||||
{Registry,
|
{Registry,
|
||||||
[
|
[
|
||||||
name: Pleroma.Web.Streamer.registry(),
|
name: Pleroma.Web.Streamer.registry(),
|
||||||
keys: :duplicate,
|
keys: :duplicate,
|
||||||
partitions: System.schedulers_online()
|
partitions: System.schedulers_online()
|
||||||
]}
|
]},
|
||||||
|
Pleroma.Web.FedSockets.Supervisor
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,9 @@ defmodule Pleroma.Chat do
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
|
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
|
import Ecto.Query
|
||||||
|
|
||||||
|
alias Pleroma.Chat
|
||||||
alias Pleroma.Repo
|
alias Pleroma.Repo
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
|
|
||||||
|
@ -69,4 +71,12 @@ def bump_or_create(user_id, recipient) do
|
||||||
conflict_target: [:user_id, :recipient]
|
conflict_target: [:user_id, :recipient]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec for_user_query(FlakeId.Ecto.CompatType.t()) :: Ecto.Query.t()
|
||||||
|
def for_user_query(user_id) do
|
||||||
|
from(c in Chat,
|
||||||
|
where: c.user_id == ^user_id,
|
||||||
|
order_by: [desc: c.updated_at]
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -26,6 +26,10 @@ def check_hellthread_threshold do
|
||||||
!!!DEPRECATION WARNING!!!
|
!!!DEPRECATION WARNING!!!
|
||||||
You are using the old configuration mechanism for the hellthread filter. Please check config.md.
|
You are using the old configuration mechanism for the hellthread filter. Please check config.md.
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
:error
|
||||||
|
else
|
||||||
|
:ok
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -47,17 +51,26 @@ def mrf_user_allowlist do
|
||||||
|
|
||||||
config :pleroma, :mrf_user_allowlist, #{inspect(rewritten, pretty: true)}
|
config :pleroma, :mrf_user_allowlist, #{inspect(rewritten, pretty: true)}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
:error
|
||||||
|
else
|
||||||
|
:ok
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def warn do
|
def warn do
|
||||||
check_hellthread_threshold()
|
with :ok <- check_hellthread_threshold(),
|
||||||
mrf_user_allowlist()
|
:ok <- mrf_user_allowlist(),
|
||||||
check_old_mrf_config()
|
:ok <- check_old_mrf_config(),
|
||||||
check_media_proxy_whitelist_config()
|
:ok <- check_media_proxy_whitelist_config(),
|
||||||
check_welcome_message_config()
|
:ok <- check_welcome_message_config(),
|
||||||
check_gun_pool_options()
|
:ok <- check_gun_pool_options(),
|
||||||
check_activity_expiration_config()
|
:ok <- check_activity_expiration_config() do
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
_ ->
|
||||||
|
:error
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_welcome_message_config do
|
def check_welcome_message_config do
|
||||||
|
@ -74,6 +87,10 @@ def check_welcome_message_config do
|
||||||
\n* `config :pleroma, :instance, welcome_user_nickname` is now `config :pleroma, :welcome, :direct_message, :sender_nickname`
|
\n* `config :pleroma, :instance, welcome_user_nickname` is now `config :pleroma, :welcome, :direct_message, :sender_nickname`
|
||||||
\n* `config :pleroma, :instance, welcome_message` is now `config :pleroma, :welcome, :direct_message, :message`
|
\n* `config :pleroma, :instance, welcome_message` is now `config :pleroma, :welcome, :direct_message, :message`
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
:error
|
||||||
|
else
|
||||||
|
:ok
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -101,8 +118,11 @@ def move_namespace_and_warn(config_map, warning_preface) do
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
if warning != "" do
|
if warning == "" do
|
||||||
|
:ok
|
||||||
|
else
|
||||||
Logger.warn(warning_preface <> warning)
|
Logger.warn(warning_preface <> warning)
|
||||||
|
:error
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -115,6 +135,10 @@ def check_media_proxy_whitelist_config do
|
||||||
!!!DEPRECATION WARNING!!!
|
!!!DEPRECATION WARNING!!!
|
||||||
Your config is using old format (only domain) for MediaProxy whitelist option. Setting should work for now, but you are advised to change format to scheme with port to prevent possible issues later.
|
Your config is using old format (only domain) for MediaProxy whitelist option. Setting should work for now, but you are advised to change format to scheme with port to prevent possible issues later.
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
:error
|
||||||
|
else
|
||||||
|
:ok
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -157,6 +181,9 @@ def check_gun_pool_options do
|
||||||
Logger.warn(Enum.join([warning_preface | pool_warnings]))
|
Logger.warn(Enum.join([warning_preface | pool_warnings]))
|
||||||
|
|
||||||
Config.put(:pools, updated_config)
|
Config.put(:pools, updated_config)
|
||||||
|
:error
|
||||||
|
else
|
||||||
|
:ok
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
150
lib/pleroma/helpers/media_helper.ex
Normal file
150
lib/pleroma/helpers/media_helper.ex
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Helpers.MediaHelper do
|
||||||
|
@moduledoc """
|
||||||
|
Handles common media-related operations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias Pleroma.HTTP
|
||||||
|
|
||||||
|
def image_resize(url, options) do
|
||||||
|
with executable when is_binary(executable) <- System.find_executable("convert"),
|
||||||
|
{:ok, args} <- prepare_image_resize_args(options),
|
||||||
|
{:ok, env} <- HTTP.get(url, [], pool: :media),
|
||||||
|
{:ok, fifo_path} <- mkfifo() do
|
||||||
|
args = List.flatten([fifo_path, args])
|
||||||
|
run_fifo(fifo_path, env, executable, args)
|
||||||
|
else
|
||||||
|
nil -> {:error, {:convert, :command_not_found}}
|
||||||
|
{:error, _} = error -> error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp prepare_image_resize_args(
|
||||||
|
%{max_width: max_width, max_height: max_height, format: "png"} = options
|
||||||
|
) do
|
||||||
|
quality = options[:quality] || 85
|
||||||
|
resize = Enum.join([max_width, "x", max_height, ">"])
|
||||||
|
|
||||||
|
args = [
|
||||||
|
"-resize",
|
||||||
|
resize,
|
||||||
|
"-quality",
|
||||||
|
to_string(quality),
|
||||||
|
"png:-"
|
||||||
|
]
|
||||||
|
|
||||||
|
{:ok, args}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp prepare_image_resize_args(%{max_width: max_width, max_height: max_height} = options) do
|
||||||
|
quality = options[:quality] || 85
|
||||||
|
resize = Enum.join([max_width, "x", max_height, ">"])
|
||||||
|
|
||||||
|
args = [
|
||||||
|
"-interlace",
|
||||||
|
"Plane",
|
||||||
|
"-resize",
|
||||||
|
resize,
|
||||||
|
"-quality",
|
||||||
|
to_string(quality),
|
||||||
|
"jpg:-"
|
||||||
|
]
|
||||||
|
|
||||||
|
{:ok, args}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp prepare_image_resize_args(_), do: {:error, :missing_options}
|
||||||
|
|
||||||
|
# Note: video thumbnail is intentionally not resized (always has original dimensions)
|
||||||
|
def video_framegrab(url) do
|
||||||
|
with executable when is_binary(executable) <- System.find_executable("ffmpeg"),
|
||||||
|
{:ok, env} <- HTTP.get(url, [], pool: :media),
|
||||||
|
{:ok, fifo_path} <- mkfifo(),
|
||||||
|
args = [
|
||||||
|
"-y",
|
||||||
|
"-i",
|
||||||
|
fifo_path,
|
||||||
|
"-vframes",
|
||||||
|
"1",
|
||||||
|
"-f",
|
||||||
|
"mjpeg",
|
||||||
|
"-loglevel",
|
||||||
|
"error",
|
||||||
|
"-"
|
||||||
|
] do
|
||||||
|
run_fifo(fifo_path, env, executable, args)
|
||||||
|
else
|
||||||
|
nil -> {:error, {:ffmpeg, :command_not_found}}
|
||||||
|
{:error, _} = error -> error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp run_fifo(fifo_path, env, executable, args) do
|
||||||
|
pid =
|
||||||
|
Port.open({:spawn_executable, executable}, [
|
||||||
|
:use_stdio,
|
||||||
|
:stream,
|
||||||
|
:exit_status,
|
||||||
|
:binary,
|
||||||
|
args: args
|
||||||
|
])
|
||||||
|
|
||||||
|
fifo = Port.open(to_charlist(fifo_path), [:eof, :binary, :stream, :out])
|
||||||
|
fix = Pleroma.Helpers.QtFastStart.fix(env.body)
|
||||||
|
true = Port.command(fifo, fix)
|
||||||
|
:erlang.port_close(fifo)
|
||||||
|
loop_recv(pid)
|
||||||
|
after
|
||||||
|
File.rm(fifo_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp mkfifo do
|
||||||
|
path = Path.join(System.tmp_dir!(), "pleroma-media-preview-pipe-#{Ecto.UUID.generate()}")
|
||||||
|
|
||||||
|
case System.cmd("mkfifo", [path]) do
|
||||||
|
{_, 0} ->
|
||||||
|
spawn(fifo_guard(path))
|
||||||
|
{:ok, path}
|
||||||
|
|
||||||
|
{_, err} ->
|
||||||
|
{:error, {:fifo_failed, err}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fifo_guard(path) do
|
||||||
|
pid = self()
|
||||||
|
|
||||||
|
fn ->
|
||||||
|
ref = Process.monitor(pid)
|
||||||
|
|
||||||
|
receive do
|
||||||
|
{:DOWN, ^ref, :process, ^pid, _} ->
|
||||||
|
File.rm(path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp loop_recv(pid) do
|
||||||
|
loop_recv(pid, <<>>)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp loop_recv(pid, acc) do
|
||||||
|
receive do
|
||||||
|
{^pid, {:data, data}} ->
|
||||||
|
loop_recv(pid, acc <> data)
|
||||||
|
|
||||||
|
{^pid, {:exit_status, 0}} ->
|
||||||
|
{:ok, acc}
|
||||||
|
|
||||||
|
{^pid, {:exit_status, status}} ->
|
||||||
|
{:error, status}
|
||||||
|
after
|
||||||
|
5000 ->
|
||||||
|
:erlang.port_close(pid)
|
||||||
|
{:error, :timeout}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
131
lib/pleroma/helpers/qt_fast_start.ex
Normal file
131
lib/pleroma/helpers/qt_fast_start.ex
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Helpers.QtFastStart do
|
||||||
|
@moduledoc """
|
||||||
|
(WIP) Converts a "slow start" (data before metadatas) mov/mp4 file to a "fast start" one (metadatas before data).
|
||||||
|
"""
|
||||||
|
|
||||||
|
# TODO: Cleanup and optimizations
|
||||||
|
# Inspirations: https://www.ffmpeg.org/doxygen/3.4/qt-faststart_8c_source.html
|
||||||
|
# https://github.com/danielgtaylor/qtfaststart/blob/master/qtfaststart/processor.py
|
||||||
|
# ISO/IEC 14496-12:2015, ISO/IEC 15444-12:2015
|
||||||
|
# Paracetamol
|
||||||
|
|
||||||
|
def fix(<<0x00, 0x00, 0x00, _, 0x66, 0x74, 0x79, 0x70, _::bits>> = binary) do
|
||||||
|
index = fix(binary, 0, nil, nil, [])
|
||||||
|
|
||||||
|
case index do
|
||||||
|
:abort -> binary
|
||||||
|
[{"ftyp", _, _, _, _}, {"mdat", _, _, _, _} | _] -> faststart(index)
|
||||||
|
[{"ftyp", _, _, _, _}, {"free", _, _, _, _}, {"mdat", _, _, _, _} | _] -> faststart(index)
|
||||||
|
_ -> binary
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def fix(binary) do
|
||||||
|
binary
|
||||||
|
end
|
||||||
|
|
||||||
|
# MOOV have been seen before MDAT- abort
|
||||||
|
defp fix(<<_::bits>>, _, true, false, _) do
|
||||||
|
:abort
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fix(
|
||||||
|
<<size::integer-big-size(32), fourcc::bits-size(32), rest::bits>>,
|
||||||
|
pos,
|
||||||
|
got_moov,
|
||||||
|
got_mdat,
|
||||||
|
acc
|
||||||
|
) do
|
||||||
|
full_size = (size - 8) * 8
|
||||||
|
<<data::bits-size(full_size), rest::bits>> = rest
|
||||||
|
|
||||||
|
acc = [
|
||||||
|
{fourcc, pos, pos + size, size,
|
||||||
|
<<size::integer-big-size(32), fourcc::bits-size(32), data::bits>>}
|
||||||
|
| acc
|
||||||
|
]
|
||||||
|
|
||||||
|
fix(rest, pos + size, got_moov || fourcc == "moov", got_mdat || fourcc == "mdat", acc)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fix(<<>>, _pos, _, _, acc) do
|
||||||
|
:lists.reverse(acc)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp faststart(index) do
|
||||||
|
{{_ftyp, _, _, _, ftyp}, index} = List.keytake(index, "ftyp", 0)
|
||||||
|
|
||||||
|
# Skip re-writing the free fourcc as it's kind of useless.
|
||||||
|
# Why stream useless bytes when you can do without?
|
||||||
|
{free_size, index} =
|
||||||
|
case List.keytake(index, "free", 0) do
|
||||||
|
{{_, _, _, size, _}, index} -> {size, index}
|
||||||
|
_ -> {0, index}
|
||||||
|
end
|
||||||
|
|
||||||
|
{{_moov, _, _, moov_size, moov}, index} = List.keytake(index, "moov", 0)
|
||||||
|
offset = -free_size + moov_size
|
||||||
|
rest = for {_, _, _, _, data} <- index, do: data, into: []
|
||||||
|
<<moov_head::bits-size(64), moov_data::bits>> = moov
|
||||||
|
[ftyp, moov_head, fix_moov(moov_data, offset, []), rest]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fix_moov(
|
||||||
|
<<size::integer-big-size(32), fourcc::bits-size(32), rest::bits>>,
|
||||||
|
offset,
|
||||||
|
acc
|
||||||
|
) do
|
||||||
|
full_size = (size - 8) * 8
|
||||||
|
<<data::bits-size(full_size), rest::bits>> = rest
|
||||||
|
|
||||||
|
data =
|
||||||
|
cond do
|
||||||
|
fourcc in ["trak", "mdia", "minf", "stbl"] ->
|
||||||
|
# Theses contains sto or co64 part
|
||||||
|
[<<size::integer-big-size(32), fourcc::bits-size(32)>>, fix_moov(data, offset, [])]
|
||||||
|
|
||||||
|
fourcc in ["stco", "co64"] ->
|
||||||
|
# fix the damn thing
|
||||||
|
<<version::integer-big-size(32), count::integer-big-size(32), rest::bits>> = data
|
||||||
|
|
||||||
|
entry_size =
|
||||||
|
case fourcc do
|
||||||
|
"stco" -> 32
|
||||||
|
"co64" -> 64
|
||||||
|
end
|
||||||
|
|
||||||
|
[
|
||||||
|
<<size::integer-big-size(32), fourcc::bits-size(32), version::integer-big-size(32),
|
||||||
|
count::integer-big-size(32)>>,
|
||||||
|
rewrite_entries(entry_size, offset, rest, [])
|
||||||
|
]
|
||||||
|
|
||||||
|
true ->
|
||||||
|
[<<size::integer-big-size(32), fourcc::bits-size(32)>>, data]
|
||||||
|
end
|
||||||
|
|
||||||
|
acc = [acc | data]
|
||||||
|
fix_moov(rest, offset, acc)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fix_moov(<<>>, _, acc), do: acc
|
||||||
|
|
||||||
|
for size <- [32, 64] do
|
||||||
|
defp rewrite_entries(
|
||||||
|
unquote(size),
|
||||||
|
offset,
|
||||||
|
<<pos::integer-big-size(unquote(size)), rest::bits>>,
|
||||||
|
acc
|
||||||
|
) do
|
||||||
|
rewrite_entries(unquote(size), offset, rest, [
|
||||||
|
acc | <<pos + offset::integer-big-size(unquote(size))>>
|
||||||
|
])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp rewrite_entries(_, _, <<>>, acc), do: acc
|
||||||
|
end
|
|
@ -3,18 +3,22 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Pleroma.Helpers.UriHelper do
|
defmodule Pleroma.Helpers.UriHelper do
|
||||||
def append_uri_params(uri, appended_params) do
|
def modify_uri_params(uri, overridden_params, deleted_params \\ []) do
|
||||||
uri = URI.parse(uri)
|
uri = URI.parse(uri)
|
||||||
appended_params = for {k, v} <- appended_params, into: %{}, do: {to_string(k), v}
|
|
||||||
existing_params = URI.query_decoder(uri.query || "") |> Enum.into(%{})
|
existing_params = URI.query_decoder(uri.query || "") |> Map.new()
|
||||||
updated_params_keys = Enum.uniq(Map.keys(existing_params) ++ Map.keys(appended_params))
|
overridden_params = Map.new(overridden_params, fn {k, v} -> {to_string(k), v} end)
|
||||||
|
deleted_params = Enum.map(deleted_params, &to_string/1)
|
||||||
|
|
||||||
updated_params =
|
updated_params =
|
||||||
for k <- updated_params_keys, do: {k, appended_params[k] || existing_params[k]}
|
existing_params
|
||||||
|
|> Map.merge(overridden_params)
|
||||||
|
|> Map.drop(deleted_params)
|
||||||
|
|
||||||
uri
|
uri
|
||||||
|> Map.put(:query, URI.encode_query(updated_params))
|
|> Map.put(:query, URI.encode_query(updated_params))
|
||||||
|> URI.to_string()
|
|> URI.to_string()
|
||||||
|
|> String.replace_suffix("?", "")
|
||||||
end
|
end
|
||||||
|
|
||||||
def maybe_add_base("/" <> uri, base), do: Path.join([base, uri])
|
def maybe_add_base("/" <> uri, base), do: Path.join([base, uri])
|
||||||
|
|
|
@ -156,9 +156,7 @@ def get_or_update_favicon(%URI{host: host} = instance_uri) do
|
||||||
defp scrape_favicon(%URI{} = instance_uri) do
|
defp scrape_favicon(%URI{} = instance_uri) do
|
||||||
try do
|
try do
|
||||||
with {:ok, %Tesla.Env{body: html}} <-
|
with {:ok, %Tesla.Env{body: html}} <-
|
||||||
Pleroma.HTTP.get(to_string(instance_uri), [{"accept", "text/html"}],
|
Pleroma.HTTP.get(to_string(instance_uri), [{"accept", "text/html"}], pool: :media),
|
||||||
adapter: [pool: :media]
|
|
||||||
),
|
|
||||||
{_, [favicon_rel | _]} when is_binary(favicon_rel) <-
|
{_, [favicon_rel | _]} when is_binary(favicon_rel) <-
|
||||||
{:parse,
|
{:parse,
|
||||||
html |> Floki.parse_document!() |> Floki.attribute("link[rel=icon]", "href")},
|
html |> Floki.parse_document!() |> Floki.attribute("link[rel=icon]", "href")},
|
||||||
|
|
|
@ -320,6 +320,19 @@ def insert_log(%{
|
||||||
|> insert_log_entry_with_message()
|
|> insert_log_entry_with_message()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec insert_log(%{actor: User, action: String.t(), subject_id: String.t()}) ::
|
||||||
|
{:ok, ModerationLog} | {:error, any}
|
||||||
|
def insert_log(%{actor: %User{} = actor, action: "chat_message_delete", subject_id: subject_id}) do
|
||||||
|
%ModerationLog{
|
||||||
|
data: %{
|
||||||
|
"actor" => %{"nickname" => actor.nickname},
|
||||||
|
"action" => "chat_message_delete",
|
||||||
|
"subject_id" => subject_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|> insert_log_entry_with_message()
|
||||||
|
end
|
||||||
|
|
||||||
@spec insert_log_entry_with_message(ModerationLog) :: {:ok, ModerationLog} | {:error, any}
|
@spec insert_log_entry_with_message(ModerationLog) :: {:ok, ModerationLog} | {:error, any}
|
||||||
defp insert_log_entry_with_message(entry) do
|
defp insert_log_entry_with_message(entry) do
|
||||||
entry.data["message"]
|
entry.data["message"]
|
||||||
|
@ -627,6 +640,17 @@ def get_log_entry_message(%ModerationLog{
|
||||||
"@#{actor_nickname} updated users: #{users_to_nicknames_string(subjects)}"
|
"@#{actor_nickname} updated users: #{users_to_nicknames_string(subjects)}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec get_log_entry_message(ModerationLog) :: String.t()
|
||||||
|
def get_log_entry_message(%ModerationLog{
|
||||||
|
data: %{
|
||||||
|
"actor" => %{"nickname" => actor_nickname},
|
||||||
|
"action" => "chat_message_delete",
|
||||||
|
"subject_id" => subject_id
|
||||||
|
}
|
||||||
|
}) do
|
||||||
|
"@#{actor_nickname} deleted chat message ##{subject_id}"
|
||||||
|
end
|
||||||
|
|
||||||
defp nicknames_to_string(nicknames) do
|
defp nicknames_to_string(nicknames) do
|
||||||
nicknames
|
nicknames
|
||||||
|> Enum.map(&"@#{&1}")
|
|> Enum.map(&"@#{&1}")
|
||||||
|
|
|
@ -12,6 +12,7 @@ defmodule Pleroma.Object.Fetcher do
|
||||||
alias Pleroma.Web.ActivityPub.ObjectValidator
|
alias Pleroma.Web.ActivityPub.ObjectValidator
|
||||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||||
alias Pleroma.Web.Federator
|
alias Pleroma.Web.Federator
|
||||||
|
alias Pleroma.Web.FedSockets
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
require Pleroma.Constants
|
require Pleroma.Constants
|
||||||
|
@ -182,27 +183,20 @@ defp maybe_date_fetch(headers, date) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do
|
def fetch_and_contain_remote_object_from_id(prm, opts \\ [])
|
||||||
|
|
||||||
|
def fetch_and_contain_remote_object_from_id(%{"id" => id}, opts),
|
||||||
|
do: fetch_and_contain_remote_object_from_id(id, opts)
|
||||||
|
|
||||||
|
def fetch_and_contain_remote_object_from_id(id, opts) when is_binary(id) do
|
||||||
Logger.debug("Fetching object #{id} via AP")
|
Logger.debug("Fetching object #{id} via AP")
|
||||||
|
|
||||||
date = Pleroma.Signature.signed_date()
|
|
||||||
|
|
||||||
headers =
|
|
||||||
[{"accept", "application/activity+json"}]
|
|
||||||
|> maybe_date_fetch(date)
|
|
||||||
|> sign_fetch(id, date)
|
|
||||||
|
|
||||||
Logger.debug("Fetch headers: #{inspect(headers)}")
|
|
||||||
|
|
||||||
with {:scheme, true} <- {:scheme, String.starts_with?(id, "http")},
|
with {:scheme, true} <- {:scheme, String.starts_with?(id, "http")},
|
||||||
{:ok, %{body: body, status: code}} when code in 200..299 <- HTTP.get(id, headers),
|
{:ok, body} <- get_object(id, opts),
|
||||||
{:ok, data} <- Jason.decode(body),
|
{:ok, data} <- safe_json_decode(body),
|
||||||
:ok <- Containment.contain_origin_from_id(id, data) do
|
:ok <- Containment.contain_origin_from_id(id, data) do
|
||||||
{:ok, data}
|
{:ok, data}
|
||||||
else
|
else
|
||||||
{:ok, %{status: code}} when code in [404, 410] ->
|
|
||||||
{:error, "Object has been deleted"}
|
|
||||||
|
|
||||||
{:scheme, _} ->
|
{:scheme, _} ->
|
||||||
{:error, "Unsupported URI scheme"}
|
{:error, "Unsupported URI scheme"}
|
||||||
|
|
||||||
|
@ -214,8 +208,44 @@ def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_and_contain_remote_object_from_id(%{"id" => id}),
|
def fetch_and_contain_remote_object_from_id(_id, _opts),
|
||||||
do: fetch_and_contain_remote_object_from_id(id)
|
do: {:error, "id must be a string"}
|
||||||
|
|
||||||
def fetch_and_contain_remote_object_from_id(_id), do: {:error, "id must be a string"}
|
defp get_object(id, opts) do
|
||||||
|
with false <- Keyword.get(opts, :force_http, false),
|
||||||
|
{:ok, fedsocket} <- FedSockets.get_or_create_fed_socket(id) do
|
||||||
|
Logger.debug("fetching via fedsocket - #{inspect(id)}")
|
||||||
|
FedSockets.fetch(fedsocket, id)
|
||||||
|
else
|
||||||
|
_other ->
|
||||||
|
Logger.debug("fetching via http - #{inspect(id)}")
|
||||||
|
get_object_http(id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_object_http(id) do
|
||||||
|
date = Pleroma.Signature.signed_date()
|
||||||
|
|
||||||
|
headers =
|
||||||
|
[{"accept", "application/activity+json"}]
|
||||||
|
|> maybe_date_fetch(date)
|
||||||
|
|> sign_fetch(id, date)
|
||||||
|
|
||||||
|
case HTTP.get(id, headers) do
|
||||||
|
{:ok, %{body: body, status: code}} when code in 200..299 ->
|
||||||
|
{:ok, body}
|
||||||
|
|
||||||
|
{:ok, %{status: code}} when code in [404, 410] ->
|
||||||
|
{:error, "Object has been deleted"}
|
||||||
|
|
||||||
|
{:error, e} ->
|
||||||
|
{:error, e}
|
||||||
|
|
||||||
|
e ->
|
||||||
|
{:error, e}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp safe_json_decode(nil), do: {:ok, nil}
|
||||||
|
defp safe_json_decode(json), do: Jason.decode(json)
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,6 +17,9 @@ defmodule Pleroma.ReverseProxy do
|
||||||
@failed_request_ttl :timer.seconds(60)
|
@failed_request_ttl :timer.seconds(60)
|
||||||
@methods ~w(GET HEAD)
|
@methods ~w(GET HEAD)
|
||||||
|
|
||||||
|
def max_read_duration_default, do: @max_read_duration
|
||||||
|
def default_cache_control_header, do: @default_cache_control_header
|
||||||
|
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
A reverse proxy.
|
A reverse proxy.
|
||||||
|
|
||||||
|
@ -391,6 +394,8 @@ defp body_size_constraint(size, limit) when is_integer(limit) and limit > 0 and
|
||||||
|
|
||||||
defp body_size_constraint(_, _), do: :ok
|
defp body_size_constraint(_, _), do: :ok
|
||||||
|
|
||||||
|
defp check_read_duration(nil = _duration, max), do: check_read_duration(@max_read_duration, max)
|
||||||
|
|
||||||
defp check_read_duration(duration, max)
|
defp check_read_duration(duration, max)
|
||||||
when is_integer(duration) and is_integer(max) and max > 0 do
|
when is_integer(duration) and is_integer(max) and max > 0 do
|
||||||
if duration > max do
|
if duration > max do
|
||||||
|
|
|
@ -39,7 +39,7 @@ def key_id_to_actor_id(key_id) do
|
||||||
def fetch_public_key(conn) do
|
def fetch_public_key(conn) do
|
||||||
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
|
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
|
||||||
{:ok, actor_id} <- key_id_to_actor_id(kid),
|
{:ok, actor_id} <- key_id_to_actor_id(kid),
|
||||||
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
|
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id, force_http: true) do
|
||||||
{:ok, public_key}
|
{:ok, public_key}
|
||||||
else
|
else
|
||||||
e ->
|
e ->
|
||||||
|
@ -50,8 +50,8 @@ def fetch_public_key(conn) do
|
||||||
def refetch_public_key(conn) do
|
def refetch_public_key(conn) do
|
||||||
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
|
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
|
||||||
{:ok, actor_id} <- key_id_to_actor_id(kid),
|
{:ok, actor_id} <- key_id_to_actor_id(kid),
|
||||||
{:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id),
|
{:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id, force_http: true),
|
||||||
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
|
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id, force_http: true) do
|
||||||
{:ok, public_key}
|
{:ok, public_key}
|
||||||
else
|
else
|
||||||
e ->
|
e ->
|
||||||
|
|
|
@ -1840,12 +1840,12 @@ def html_filter_policy(%User{no_rich_text: true}) do
|
||||||
|
|
||||||
def html_filter_policy(_), do: Config.get([:markup, :scrub_policy])
|
def html_filter_policy(_), do: Config.get([:markup, :scrub_policy])
|
||||||
|
|
||||||
def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
|
def fetch_by_ap_id(ap_id, opts \\ []), do: ActivityPub.make_user_from_ap_id(ap_id, opts)
|
||||||
|
|
||||||
def get_or_fetch_by_ap_id(ap_id) do
|
def get_or_fetch_by_ap_id(ap_id, opts \\ []) do
|
||||||
cached_user = get_cached_by_ap_id(ap_id)
|
cached_user = get_cached_by_ap_id(ap_id)
|
||||||
|
|
||||||
maybe_fetched_user = needs_update?(cached_user) && fetch_by_ap_id(ap_id)
|
maybe_fetched_user = needs_update?(cached_user) && fetch_by_ap_id(ap_id, opts)
|
||||||
|
|
||||||
case {cached_user, maybe_fetched_user} do
|
case {cached_user, maybe_fetched_user} do
|
||||||
{_, {:ok, %User{} = user}} ->
|
{_, {:ok, %User{} = user}} ->
|
||||||
|
@ -1918,8 +1918,8 @@ def public_key(%{public_key: public_key_pem}) when is_binary(public_key_pem) do
|
||||||
|
|
||||||
def public_key(_), do: {:error, "key not found"}
|
def public_key(_), do: {:error, "key not found"}
|
||||||
|
|
||||||
def get_public_key_for_ap_id(ap_id) do
|
def get_public_key_for_ap_id(ap_id, opts \\ []) do
|
||||||
with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
|
with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id, opts),
|
||||||
{:ok, public_key} <- public_key(user) do
|
{:ok, public_key} <- public_key(user) do
|
||||||
{:ok, public_key}
|
{:ok, public_key}
|
||||||
else
|
else
|
||||||
|
|
|
@ -52,6 +52,7 @@ defp search_query(query_string, for_user, following) do
|
||||||
|> base_query(following)
|
|> base_query(following)
|
||||||
|> filter_blocked_user(for_user)
|
|> filter_blocked_user(for_user)
|
||||||
|> filter_invisible_users()
|
|> filter_invisible_users()
|
||||||
|
|> filter_discoverable_users()
|
||||||
|> filter_internal_users()
|
|> filter_internal_users()
|
||||||
|> filter_blocked_domains(for_user)
|
|> filter_blocked_domains(for_user)
|
||||||
|> fts_search(query_string)
|
|> fts_search(query_string)
|
||||||
|
@ -122,6 +123,10 @@ defp filter_invisible_users(query) do
|
||||||
from(q in query, where: q.invisible == false)
|
from(q in query, where: q.invisible == false)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp filter_discoverable_users(query) do
|
||||||
|
from(q in query, where: q.discoverable == true)
|
||||||
|
end
|
||||||
|
|
||||||
defp filter_internal_users(query) do
|
defp filter_internal_users(query) do
|
||||||
from(q in query, where: q.actor_type != "Application")
|
from(q in query, where: q.actor_type != "Application")
|
||||||
end
|
end
|
||||||
|
|
|
@ -1270,10 +1270,12 @@ defp object_to_user_data(data) do
|
||||||
|
|
||||||
def fetch_follow_information_for_user(user) do
|
def fetch_follow_information_for_user(user) do
|
||||||
with {:ok, following_data} <-
|
with {:ok, following_data} <-
|
||||||
Fetcher.fetch_and_contain_remote_object_from_id(user.following_address),
|
Fetcher.fetch_and_contain_remote_object_from_id(user.following_address,
|
||||||
|
force_http: true
|
||||||
|
),
|
||||||
{:ok, hide_follows} <- collection_private(following_data),
|
{:ok, hide_follows} <- collection_private(following_data),
|
||||||
{:ok, followers_data} <-
|
{:ok, followers_data} <-
|
||||||
Fetcher.fetch_and_contain_remote_object_from_id(user.follower_address),
|
Fetcher.fetch_and_contain_remote_object_from_id(user.follower_address, force_http: true),
|
||||||
{:ok, hide_followers} <- collection_private(followers_data) do
|
{:ok, hide_followers} <- collection_private(followers_data) do
|
||||||
{:ok,
|
{:ok,
|
||||||
%{
|
%{
|
||||||
|
@ -1347,8 +1349,8 @@ def user_data_from_user_object(data) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_and_prepare_user_from_ap_id(ap_id) do
|
def fetch_and_prepare_user_from_ap_id(ap_id, opts \\ []) do
|
||||||
with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id),
|
with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id, opts),
|
||||||
{:ok, data} <- user_data_from_user_object(data) do
|
{:ok, data} <- user_data_from_user_object(data) do
|
||||||
{:ok, maybe_update_follow_information(data)}
|
{:ok, maybe_update_follow_information(data)}
|
||||||
else
|
else
|
||||||
|
@ -1390,13 +1392,13 @@ def maybe_handle_clashing_nickname(data) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def make_user_from_ap_id(ap_id) do
|
def make_user_from_ap_id(ap_id, opts \\ []) do
|
||||||
user = User.get_cached_by_ap_id(ap_id)
|
user = User.get_cached_by_ap_id(ap_id)
|
||||||
|
|
||||||
if user && !User.ap_enabled?(user) do
|
if user && !User.ap_enabled?(user) 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) do
|
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id, opts) do
|
||||||
if user do
|
if user do
|
||||||
user
|
user
|
||||||
|> User.remote_user_changeset(data)
|
|> User.remote_user_changeset(data)
|
||||||
|
|
|
@ -5,16 +5,34 @@
|
||||||
defmodule Pleroma.Web.ActivityPub.MRF do
|
defmodule Pleroma.Web.ActivityPub.MRF do
|
||||||
@callback filter(Map.t()) :: {:ok | :reject, Map.t()}
|
@callback filter(Map.t()) :: {:ok | :reject, Map.t()}
|
||||||
|
|
||||||
def filter(policies, %{} = object) do
|
def filter(policies, %{} = message) do
|
||||||
policies
|
policies
|
||||||
|> Enum.reduce({:ok, object}, fn
|
|> Enum.reduce({:ok, message}, fn
|
||||||
policy, {:ok, object} -> policy.filter(object)
|
policy, {:ok, message} -> policy.filter(message)
|
||||||
_, error -> error
|
_, error -> error
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter(%{} = object), do: get_policies() |> filter(object)
|
def filter(%{} = object), do: get_policies() |> filter(object)
|
||||||
|
|
||||||
|
def pipeline_filter(%{} = message, meta) do
|
||||||
|
object = meta[:object_data]
|
||||||
|
ap_id = message["object"]
|
||||||
|
|
||||||
|
if object && ap_id do
|
||||||
|
with {:ok, message} <- filter(Map.put(message, "object", object)) do
|
||||||
|
meta = Keyword.put(meta, :object_data, message["object"])
|
||||||
|
{:ok, Map.put(message, "object", ap_id), meta}
|
||||||
|
else
|
||||||
|
{err, message} -> {err, message, meta}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
{err, message} = filter(message)
|
||||||
|
|
||||||
|
{err, message, meta}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def get_policies do
|
def get_policies do
|
||||||
Pleroma.Config.get([:mrf, :policies], []) |> get_policies()
|
Pleroma.Config.get([:mrf, :policies], []) |> get_policies()
|
||||||
end
|
end
|
||||||
|
|
|
@ -20,9 +20,17 @@ defp string_matches?(string, pattern) do
|
||||||
String.match?(string, pattern)
|
String.match?(string, pattern)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp check_reject(%{"object" => %{"content" => content, "summary" => summary}} = message) do
|
defp object_payload(%{} = object) do
|
||||||
|
[object["content"], object["summary"], object["name"]]
|
||||||
|
|> Enum.filter(& &1)
|
||||||
|
|> Enum.join("\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
defp check_reject(%{"object" => %{} = object} = message) do
|
||||||
|
payload = object_payload(object)
|
||||||
|
|
||||||
if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern ->
|
if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern ->
|
||||||
string_matches?(content, pattern) or string_matches?(summary, pattern)
|
string_matches?(payload, pattern)
|
||||||
end) do
|
end) do
|
||||||
{:reject, "[KeywordPolicy] Matches with rejected keyword"}
|
{:reject, "[KeywordPolicy] Matches with rejected keyword"}
|
||||||
else
|
else
|
||||||
|
@ -30,12 +38,12 @@ defp check_reject(%{"object" => %{"content" => content, "summary" => summary}} =
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp check_ftl_removal(
|
defp check_ftl_removal(%{"to" => to, "object" => %{} = object} = message) do
|
||||||
%{"to" => to, "object" => %{"content" => content, "summary" => summary}} = message
|
payload = object_payload(object)
|
||||||
) do
|
|
||||||
if Pleroma.Constants.as_public() in to and
|
if Pleroma.Constants.as_public() in to and
|
||||||
Enum.any?(Pleroma.Config.get([:mrf_keyword, :federated_timeline_removal]), fn pattern ->
|
Enum.any?(Pleroma.Config.get([:mrf_keyword, :federated_timeline_removal]), fn pattern ->
|
||||||
string_matches?(content, pattern) or string_matches?(summary, pattern)
|
string_matches?(payload, pattern)
|
||||||
end) do
|
end) do
|
||||||
to = List.delete(to, Pleroma.Constants.as_public())
|
to = List.delete(to, Pleroma.Constants.as_public())
|
||||||
cc = [Pleroma.Constants.as_public() | message["cc"] || []]
|
cc = [Pleroma.Constants.as_public() | message["cc"] || []]
|
||||||
|
@ -51,35 +59,24 @@ defp check_ftl_removal(
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp check_replace(%{"object" => %{"content" => content, "summary" => summary}} = message) do
|
defp check_replace(%{"object" => %{} = object} = message) do
|
||||||
content =
|
object =
|
||||||
if is_binary(content) do
|
["content", "name", "summary"]
|
||||||
content
|
|> Enum.filter(fn field -> Map.has_key?(object, field) && object[field] end)
|
||||||
else
|
|> Enum.reduce(object, fn field, object ->
|
||||||
""
|
data =
|
||||||
end
|
Enum.reduce(
|
||||||
|
Pleroma.Config.get([:mrf_keyword, :replace]),
|
||||||
|
object[field],
|
||||||
|
fn {pat, repl}, acc -> String.replace(acc, pat, repl) end
|
||||||
|
)
|
||||||
|
|
||||||
summary =
|
Map.put(object, field, data)
|
||||||
if is_binary(summary) do
|
end)
|
||||||
summary
|
|
||||||
else
|
|
||||||
""
|
|
||||||
end
|
|
||||||
|
|
||||||
{content, summary} =
|
message = Map.put(message, "object", object)
|
||||||
Enum.reduce(
|
|
||||||
Pleroma.Config.get([:mrf_keyword, :replace]),
|
|
||||||
{content, summary},
|
|
||||||
fn {pattern, replacement}, {content_acc, summary_acc} ->
|
|
||||||
{String.replace(content_acc, pattern, replacement),
|
|
||||||
String.replace(summary_acc, pattern, replacement)}
|
|
||||||
end
|
|
||||||
)
|
|
||||||
|
|
||||||
{:ok,
|
{:ok, message}
|
||||||
message
|
|
||||||
|> put_in(["object", "content"], content)
|
|
||||||
|> put_in(["object", "summary"], summary)}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
|
@ -12,17 +12,21 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
@options [
|
@adapter_options [
|
||||||
pool: :media,
|
pool: :media,
|
||||||
recv_timeout: 10_000
|
recv_timeout: 10_000
|
||||||
]
|
]
|
||||||
|
|
||||||
def perform(:prefetch, url) do
|
def perform(:prefetch, url) do
|
||||||
Logger.debug("Prefetching #{inspect(url)}")
|
# Fetching only proxiable resources
|
||||||
|
if MediaProxy.enabled?() and MediaProxy.url_proxiable?(url) do
|
||||||
|
# If preview proxy is enabled, it'll also hit media proxy (so we're caching both requests)
|
||||||
|
prefetch_url = MediaProxy.preview_url(url)
|
||||||
|
|
||||||
url
|
Logger.debug("Prefetching #{inspect(url)} as #{inspect(prefetch_url)}")
|
||||||
|> MediaProxy.url()
|
|
||||||
|> HTTP.get([], @options)
|
HTTP.get(prefetch_url, [], @adapter_options)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def perform(:preload, %{"object" => %{"attachment" => attachments}} = _message) do
|
def perform(:preload, %{"object" => %{"attachment" => attachments}} = _message) do
|
||||||
|
|
|
@ -28,8 +28,7 @@ def filter(%{"actor" => actor} = message) do
|
||||||
}"
|
}"
|
||||||
)
|
)
|
||||||
|
|
||||||
subchain
|
MRF.filter(subchain, message)
|
||||||
|> MRF.filter(message)
|
|
||||||
else
|
else
|
||||||
_e -> {:ok, message}
|
_e -> {:ok, message}
|
||||||
end
|
end
|
||||||
|
|
|
@ -26,13 +26,17 @@ def common_pipeline(object, meta) do
|
||||||
|
|
||||||
{:error, e} ->
|
{:error, e} ->
|
||||||
{:error, e}
|
{:error, e}
|
||||||
|
|
||||||
|
{:reject, e} ->
|
||||||
|
{:reject, e}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def do_common_pipeline(object, meta) do
|
def do_common_pipeline(object, meta) do
|
||||||
with {_, {:ok, validated_object, meta}} <-
|
with {_, {:ok, validated_object, meta}} <-
|
||||||
{:validate_object, ObjectValidator.validate(object, meta)},
|
{:validate_object, ObjectValidator.validate(object, meta)},
|
||||||
{_, {:ok, mrfd_object}} <- {:mrf_object, MRF.filter(validated_object)},
|
{_, {:ok, mrfd_object, meta}} <-
|
||||||
|
{:mrf_object, MRF.pipeline_filter(validated_object, meta)},
|
||||||
{_, {:ok, activity, meta}} <-
|
{_, {:ok, activity, meta}} <-
|
||||||
{:persist_object, ActivityPub.persist(mrfd_object, meta)},
|
{:persist_object, ActivityPub.persist(mrfd_object, meta)},
|
||||||
{_, {:ok, activity, meta}} <-
|
{_, {:ok, activity, meta}} <-
|
||||||
|
@ -40,7 +44,7 @@ def do_common_pipeline(object, meta) do
|
||||||
{_, {:ok, _}} <- {:federation, maybe_federate(activity, meta)} do
|
{_, {:ok, _}} <- {:federation, maybe_federate(activity, meta)} do
|
||||||
{:ok, activity, meta}
|
{:ok, activity, meta}
|
||||||
else
|
else
|
||||||
{:mrf_object, {:reject, _}} -> {:ok, nil, meta}
|
{:mrf_object, {:reject, message, _}} -> {:reject, message}
|
||||||
e -> {:error, e}
|
e -> {:error, e}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,6 +13,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Web.ActivityPub.Relay
|
alias Pleroma.Web.ActivityPub.Relay
|
||||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||||
|
alias Pleroma.Web.FedSockets
|
||||||
|
|
||||||
require Pleroma.Constants
|
require Pleroma.Constants
|
||||||
|
|
||||||
|
@ -50,15 +51,35 @@ def is_representable?(%Activity{} = activity) do
|
||||||
def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do
|
def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do
|
||||||
Logger.debug("Federating #{id} to #{inbox}")
|
Logger.debug("Federating #{id} to #{inbox}")
|
||||||
|
|
||||||
uri = URI.parse(inbox)
|
case FedSockets.get_or_create_fed_socket(inbox) do
|
||||||
|
{:ok, fedsocket} ->
|
||||||
|
Logger.debug("publishing via fedsockets - #{inspect(inbox)}")
|
||||||
|
FedSockets.publish(fedsocket, json)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
Logger.debug("publishing via http - #{inspect(inbox)}")
|
||||||
|
http_publish(inbox, actor, json, params)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def publish_one(%{actor_id: actor_id} = params) do
|
||||||
|
actor = User.get_cached_by_id(actor_id)
|
||||||
|
|
||||||
|
params
|
||||||
|
|> Map.delete(:actor_id)
|
||||||
|
|> Map.put(:actor, actor)
|
||||||
|
|> publish_one()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp http_publish(inbox, actor, json, params) do
|
||||||
|
uri = %{path: path} = URI.parse(inbox)
|
||||||
digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
|
digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
|
||||||
|
|
||||||
date = Pleroma.Signature.signed_date()
|
date = Pleroma.Signature.signed_date()
|
||||||
|
|
||||||
signature =
|
signature =
|
||||||
Pleroma.Signature.sign(actor, %{
|
Pleroma.Signature.sign(actor, %{
|
||||||
"(request-target)": "post #{uri.path}",
|
"(request-target)": "post #{path}",
|
||||||
host: signature_host(uri),
|
host: signature_host(uri),
|
||||||
"content-length": byte_size(json),
|
"content-length": byte_size(json),
|
||||||
digest: digest,
|
digest: digest,
|
||||||
|
@ -89,15 +110,6 @@ def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = pa
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def publish_one(%{actor_id: actor_id} = params) do
|
|
||||||
actor = User.get_cached_by_id(actor_id)
|
|
||||||
|
|
||||||
params
|
|
||||||
|> Map.delete(:actor_id)
|
|
||||||
|> Map.put(:actor, actor)
|
|
||||||
|> publish_one()
|
|
||||||
end
|
|
||||||
|
|
||||||
defp signature_host(%URI{port: port, scheme: scheme, host: host}) do
|
defp signature_host(%URI{port: port, scheme: scheme, host: host}) do
|
||||||
if port == URI.default_port(scheme) do
|
if port == URI.default_port(scheme) do
|
||||||
host
|
host
|
||||||
|
|
|
@ -1000,7 +1000,7 @@ def perform(:user_upgrade, user) do
|
||||||
|
|
||||||
def upgrade_user_from_ap_id(ap_id) do
|
def upgrade_user_from_ap_id(ap_id) 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, force_http: true),
|
||||||
{:ok, user} <- update_user(user, data) do
|
{:ok, user} <- update_user(user, data) do
|
||||||
TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
|
TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
|
||||||
{:ok, user}
|
{:ok, user}
|
||||||
|
|
|
@ -23,8 +23,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
||||||
alias Pleroma.Web.Endpoint
|
alias Pleroma.Web.Endpoint
|
||||||
alias Pleroma.Web.Router
|
alias Pleroma.Web.Router
|
||||||
|
|
||||||
require Logger
|
|
||||||
|
|
||||||
@users_page_size 50
|
@users_page_size 50
|
||||||
|
|
||||||
plug(
|
plug(
|
||||||
|
@ -68,6 +66,12 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
||||||
when action in [:list_user_statuses, :list_instance_statuses]
|
when action in [:list_user_statuses, :list_instance_statuses]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
plug(
|
||||||
|
OAuthScopesPlug,
|
||||||
|
%{scopes: ["read:chats"], admin: true}
|
||||||
|
when action in [:list_user_chats]
|
||||||
|
)
|
||||||
|
|
||||||
plug(
|
plug(
|
||||||
OAuthScopesPlug,
|
OAuthScopesPlug,
|
||||||
%{scopes: ["read"], admin: true}
|
%{scopes: ["read"], admin: true}
|
||||||
|
@ -256,6 +260,20 @@ def list_user_statuses(%{assigns: %{user: admin}} = conn, %{"nickname" => nickna
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def list_user_chats(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname} = _params) do
|
||||||
|
with %User{id: user_id} <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
|
||||||
|
chats =
|
||||||
|
Pleroma.Chat.for_user_query(user_id)
|
||||||
|
|> Pleroma.Repo.all()
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_view(AdminAPI.ChatView)
|
||||||
|
|> render("index.json", chats: chats)
|
||||||
|
else
|
||||||
|
_ -> {:error, :not_found}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
|
def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
|
||||||
user = User.get_cached_by_nickname(nickname)
|
user = User.get_cached_by_nickname(nickname)
|
||||||
|
|
||||||
|
|
85
lib/pleroma/web/admin_api/controllers/chat_controller.ex
Normal file
85
lib/pleroma/web/admin_api/controllers/chat_controller.ex
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.AdminAPI.ChatController do
|
||||||
|
use Pleroma.Web, :controller
|
||||||
|
|
||||||
|
alias Pleroma.Activity
|
||||||
|
alias Pleroma.Chat
|
||||||
|
alias Pleroma.Chat.MessageReference
|
||||||
|
alias Pleroma.ModerationLog
|
||||||
|
alias Pleroma.Pagination
|
||||||
|
alias Pleroma.Plugs.OAuthScopesPlug
|
||||||
|
alias Pleroma.Web.AdminAPI
|
||||||
|
alias Pleroma.Web.CommonAPI
|
||||||
|
alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||||
|
|
||||||
|
plug(
|
||||||
|
OAuthScopesPlug,
|
||||||
|
%{scopes: ["read:chats"], admin: true} when action in [:show, :messages]
|
||||||
|
)
|
||||||
|
|
||||||
|
plug(
|
||||||
|
OAuthScopesPlug,
|
||||||
|
%{scopes: ["write:chats"], admin: true} when action in [:delete_message]
|
||||||
|
)
|
||||||
|
|
||||||
|
action_fallback(Pleroma.Web.AdminAPI.FallbackController)
|
||||||
|
|
||||||
|
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.ChatOperation
|
||||||
|
|
||||||
|
def delete_message(%{assigns: %{user: user}} = conn, %{
|
||||||
|
message_id: message_id,
|
||||||
|
id: chat_id
|
||||||
|
}) do
|
||||||
|
with %MessageReference{object: %{data: %{"id" => object_ap_id}}} = cm_ref <-
|
||||||
|
MessageReference.get_by_id(message_id),
|
||||||
|
^chat_id <- to_string(cm_ref.chat_id),
|
||||||
|
%Activity{id: activity_id} <- Activity.get_create_by_object_ap_id(object_ap_id),
|
||||||
|
{:ok, _} <- CommonAPI.delete(activity_id, user) do
|
||||||
|
ModerationLog.insert_log(%{
|
||||||
|
action: "chat_message_delete",
|
||||||
|
actor: user,
|
||||||
|
subject_id: message_id
|
||||||
|
})
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_view(MessageReferenceView)
|
||||||
|
|> render("show.json", chat_message_reference: cm_ref)
|
||||||
|
else
|
||||||
|
_e ->
|
||||||
|
{:error, :could_not_delete}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def messages(conn, %{id: id} = params) do
|
||||||
|
with %Chat{} = chat <- Chat.get_by_id(id) do
|
||||||
|
cm_refs =
|
||||||
|
chat
|
||||||
|
|> MessageReference.for_chat_query()
|
||||||
|
|> Pagination.fetch_paginated(params)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_view(MessageReferenceView)
|
||||||
|
|> render("index.json", chat_message_references: cm_refs)
|
||||||
|
else
|
||||||
|
_ ->
|
||||||
|
conn
|
||||||
|
|> put_status(:not_found)
|
||||||
|
|> json(%{error: "not found"})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def show(conn, %{id: id}) do
|
||||||
|
with %Chat{} = chat <- Chat.get_by_id(id) do
|
||||||
|
conn
|
||||||
|
|> put_view(AdminAPI.ChatView)
|
||||||
|
|> render("show.json", chat: chat)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.AdminAPI.InstanceDocumentController do
|
||||||
|
use Pleroma.Web, :controller
|
||||||
|
|
||||||
|
alias Pleroma.Plugs.InstanceStatic
|
||||||
|
alias Pleroma.Plugs.OAuthScopesPlug
|
||||||
|
alias Pleroma.Web.InstanceDocument
|
||||||
|
|
||||||
|
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||||
|
|
||||||
|
action_fallback(Pleroma.Web.AdminAPI.FallbackController)
|
||||||
|
|
||||||
|
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.InstanceDocumentOperation
|
||||||
|
|
||||||
|
plug(OAuthScopesPlug, %{scopes: ["read"], admin: true} when action == :show)
|
||||||
|
plug(OAuthScopesPlug, %{scopes: ["write"], admin: true} when action in [:update, :delete])
|
||||||
|
|
||||||
|
def show(conn, %{name: document_name}) do
|
||||||
|
with {:ok, url} <- InstanceDocument.get(document_name),
|
||||||
|
{:ok, content} <- File.read(InstanceStatic.file_path(url)) do
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("text/html")
|
||||||
|
|> send_resp(200, content)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update(%{body_params: %{file: file}} = conn, %{name: document_name}) do
|
||||||
|
with {:ok, url} <- InstanceDocument.put(document_name, file.path) do
|
||||||
|
json(conn, %{"url" => url})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete(conn, %{name: document_name}) do
|
||||||
|
with :ok <- InstanceDocument.delete(document_name) do
|
||||||
|
json(conn, %{})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
30
lib/pleroma/web/admin_api/views/chat_view.ex
Normal file
30
lib/pleroma/web/admin_api/views/chat_view.ex
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.AdminAPI.ChatView do
|
||||||
|
use Pleroma.Web, :view
|
||||||
|
|
||||||
|
alias Pleroma.Chat
|
||||||
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Web.MastodonAPI
|
||||||
|
alias Pleroma.Web.PleromaAPI
|
||||||
|
|
||||||
|
def render("index.json", %{chats: chats} = opts) do
|
||||||
|
render_many(chats, __MODULE__, "show.json", Map.delete(opts, :chats))
|
||||||
|
end
|
||||||
|
|
||||||
|
def render("show.json", %{chat: %Chat{user_id: user_id}} = opts) do
|
||||||
|
user = User.get_by_id(user_id)
|
||||||
|
sender = MastodonAPI.AccountView.render("show.json", user: user, skip_visibility_check: true)
|
||||||
|
|
||||||
|
serialized_chat = PleromaAPI.ChatView.render("show.json", opts)
|
||||||
|
|
||||||
|
serialized_chat
|
||||||
|
|> Map.put(:sender, sender)
|
||||||
|
|> Map.put(:receiver, serialized_chat[:account])
|
||||||
|
|> Map.delete(:account)
|
||||||
|
end
|
||||||
|
|
||||||
|
def render(view, opts), do: PleromaAPI.ChatView.render(view, opts)
|
||||||
|
end
|
|
@ -8,6 +8,7 @@ defmodule Pleroma.Web.AdminAPI.StatusView do
|
||||||
require Pleroma.Constants
|
require Pleroma.Constants
|
||||||
|
|
||||||
alias Pleroma.Web.AdminAPI
|
alias Pleroma.Web.AdminAPI
|
||||||
|
alias Pleroma.Web.CommonAPI
|
||||||
alias Pleroma.Web.MastodonAPI
|
alias Pleroma.Web.MastodonAPI
|
||||||
|
|
||||||
defdelegate merge_account_views(user), to: AdminAPI.AccountView
|
defdelegate merge_account_views(user), to: AdminAPI.AccountView
|
||||||
|
@ -17,7 +18,7 @@ def render("index.json", opts) do
|
||||||
end
|
end
|
||||||
|
|
||||||
def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
|
def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
|
||||||
user = MastodonAPI.StatusView.get_user(activity.data["actor"])
|
user = CommonAPI.get_user(activity.data["actor"])
|
||||||
|
|
||||||
MastodonAPI.StatusView.render("show.json", opts)
|
MastodonAPI.StatusView.render("show.json", opts)
|
||||||
|> Map.merge(%{account: merge_account_views(user)})
|
|> Map.merge(%{account: merge_account_views(user)})
|
||||||
|
|
|
@ -13,10 +13,15 @@ defmodule Pleroma.Web.ApiSpec do
|
||||||
@impl OpenApi
|
@impl OpenApi
|
||||||
def spec do
|
def spec do
|
||||||
%OpenApi{
|
%OpenApi{
|
||||||
servers: [
|
servers:
|
||||||
# Populate the Server info from a phoenix endpoint
|
if Phoenix.Endpoint.server?(:pleroma, Endpoint) do
|
||||||
OpenApiSpex.Server.from_endpoint(Endpoint)
|
[
|
||||||
],
|
# Populate the Server info from a phoenix endpoint
|
||||||
|
OpenApiSpex.Server.from_endpoint(Endpoint)
|
||||||
|
]
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end,
|
||||||
info: %OpenApiSpex.Info{
|
info: %OpenApiSpex.Info{
|
||||||
title: "Pleroma",
|
title: "Pleroma",
|
||||||
description: Application.spec(:pleroma, :description) |> to_string(),
|
description: Application.spec(:pleroma, :description) |> to_string(),
|
||||||
|
|
|
@ -72,7 +72,11 @@ def empty_object_response do
|
||||||
end
|
end
|
||||||
|
|
||||||
def empty_array_response do
|
def empty_array_response do
|
||||||
Operation.response("Empty array", "application/json", %Schema{type: :array, example: []})
|
Operation.response("Empty array", "application/json", %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: %Schema{type: :object, example: %{}},
|
||||||
|
example: []
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
def no_content_response do
|
def no_content_response do
|
||||||
|
|
|
@ -378,6 +378,10 @@ def identity_proofs_operation do
|
||||||
tags: ["accounts"],
|
tags: ["accounts"],
|
||||||
summary: "Identity proofs",
|
summary: "Identity proofs",
|
||||||
operationId: "AccountController.identity_proofs",
|
operationId: "AccountController.identity_proofs",
|
||||||
|
# Validators complains about unused path params otherwise
|
||||||
|
parameters: [
|
||||||
|
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}
|
||||||
|
],
|
||||||
description: "Not implemented",
|
description: "Not implemented",
|
||||||
responses: %{
|
responses: %{
|
||||||
200 => empty_array_response()
|
200 => empty_array_response()
|
||||||
|
@ -475,7 +479,6 @@ defp create_response do
|
||||||
identifier: %Schema{type: :string},
|
identifier: %Schema{type: :string},
|
||||||
message: %Schema{type: :string}
|
message: %Schema{type: :string}
|
||||||
},
|
},
|
||||||
required: [],
|
|
||||||
# Note: example of successful registration with failed login response:
|
# Note: example of successful registration with failed login response:
|
||||||
# example: %{
|
# example: %{
|
||||||
# "identifier" => "missing_confirmed_email",
|
# "identifier" => "missing_confirmed_email",
|
||||||
|
@ -536,7 +539,7 @@ defp update_credentials_request do
|
||||||
nullable: true,
|
nullable: true,
|
||||||
oneOf: [
|
oneOf: [
|
||||||
%Schema{type: :array, items: attribute_field()},
|
%Schema{type: :array, items: attribute_field()},
|
||||||
%Schema{type: :object, additionalProperties: %Schema{type: attribute_field()}}
|
%Schema{type: :object, additionalProperties: attribute_field()}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
# NOTE: `source` field is not supported
|
# NOTE: `source` field is not supported
|
||||||
|
|
96
lib/pleroma/web/api_spec/operations/admin/chat_operation.ex
Normal file
96
lib/pleroma/web/api_spec/operations/admin/chat_operation.ex
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ApiSpec.Admin.ChatOperation do
|
||||||
|
alias OpenApiSpex.Operation
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.Chat
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.ChatMessage
|
||||||
|
|
||||||
|
import Pleroma.Web.ApiSpec.Helpers
|
||||||
|
|
||||||
|
def open_api_operation(action) do
|
||||||
|
operation = String.to_existing_atom("#{action}_operation")
|
||||||
|
apply(__MODULE__, operation, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_message_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["admin", "chat"],
|
||||||
|
summary: "Delete an individual chat message",
|
||||||
|
operationId: "AdminAPI.ChatController.delete_message",
|
||||||
|
parameters: [
|
||||||
|
Operation.parameter(:id, :path, :string, "The ID of the Chat"),
|
||||||
|
Operation.parameter(:message_id, :path, :string, "The ID of the message")
|
||||||
|
],
|
||||||
|
responses: %{
|
||||||
|
200 =>
|
||||||
|
Operation.response(
|
||||||
|
"The deleted ChatMessage",
|
||||||
|
"application/json",
|
||||||
|
ChatMessage
|
||||||
|
)
|
||||||
|
},
|
||||||
|
security: [
|
||||||
|
%{
|
||||||
|
"oAuth" => ["write:chats"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def messages_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["admin", "chat"],
|
||||||
|
summary: "Get the most recent messages of the chat",
|
||||||
|
operationId: "AdminAPI.ChatController.messages",
|
||||||
|
parameters:
|
||||||
|
[Operation.parameter(:id, :path, :string, "The ID of the Chat")] ++
|
||||||
|
pagination_params(),
|
||||||
|
responses: %{
|
||||||
|
200 =>
|
||||||
|
Operation.response(
|
||||||
|
"The messages in the chat",
|
||||||
|
"application/json",
|
||||||
|
Pleroma.Web.ApiSpec.ChatOperation.chat_messages_response()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
security: [
|
||||||
|
%{
|
||||||
|
"oAuth" => ["read:chats"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def show_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["chat"],
|
||||||
|
summary: "Create a chat",
|
||||||
|
operationId: "AdminAPI.ChatController.show",
|
||||||
|
parameters: [
|
||||||
|
Operation.parameter(
|
||||||
|
:id,
|
||||||
|
:path,
|
||||||
|
:string,
|
||||||
|
"The id of the chat",
|
||||||
|
required: true,
|
||||||
|
example: "1234"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
responses: %{
|
||||||
|
200 =>
|
||||||
|
Operation.response(
|
||||||
|
"The existing chat",
|
||||||
|
"application/json",
|
||||||
|
Chat
|
||||||
|
)
|
||||||
|
},
|
||||||
|
security: [
|
||||||
|
%{
|
||||||
|
"oAuth" => ["read"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,115 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ApiSpec.Admin.InstanceDocumentOperation do
|
||||||
|
alias OpenApiSpex.Operation
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
alias Pleroma.Web.ApiSpec.Helpers
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||||
|
|
||||||
|
def open_api_operation(action) do
|
||||||
|
operation = String.to_existing_atom("#{action}_operation")
|
||||||
|
apply(__MODULE__, operation, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def show_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Admin", "InstanceDocument"],
|
||||||
|
summary: "Get the instance document",
|
||||||
|
operationId: "AdminAPI.InstanceDocumentController.show",
|
||||||
|
security: [%{"oAuth" => ["read"]}],
|
||||||
|
parameters: [
|
||||||
|
Operation.parameter(:name, :path, %Schema{type: :string}, "The document name",
|
||||||
|
required: true
|
||||||
|
)
|
||||||
|
| Helpers.admin_api_params()
|
||||||
|
],
|
||||||
|
responses: %{
|
||||||
|
200 => document_content(),
|
||||||
|
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||||
|
403 => Operation.response("Forbidden", "application/json", ApiError),
|
||||||
|
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Admin", "InstanceDocument"],
|
||||||
|
summary: "Update the instance document",
|
||||||
|
operationId: "AdminAPI.InstanceDocumentController.update",
|
||||||
|
security: [%{"oAuth" => ["write"]}],
|
||||||
|
requestBody: Helpers.request_body("Parameters", update_request()),
|
||||||
|
parameters: [
|
||||||
|
Operation.parameter(:name, :path, %Schema{type: :string}, "The document name",
|
||||||
|
required: true
|
||||||
|
)
|
||||||
|
| Helpers.admin_api_params()
|
||||||
|
],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("InstanceDocument", "application/json", instance_document()),
|
||||||
|
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||||
|
403 => Operation.response("Forbidden", "application/json", ApiError),
|
||||||
|
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp update_request do
|
||||||
|
%Schema{
|
||||||
|
title: "UpdateRequest",
|
||||||
|
description: "POST body for uploading the file",
|
||||||
|
type: :object,
|
||||||
|
required: [:file],
|
||||||
|
properties: %{
|
||||||
|
file: %Schema{
|
||||||
|
type: :string,
|
||||||
|
format: :binary,
|
||||||
|
description: "The file to be uploaded, using multipart form data."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Admin", "InstanceDocument"],
|
||||||
|
summary: "Get the instance document",
|
||||||
|
operationId: "AdminAPI.InstanceDocumentController.delete",
|
||||||
|
security: [%{"oAuth" => ["write"]}],
|
||||||
|
parameters: [
|
||||||
|
Operation.parameter(:name, :path, %Schema{type: :string}, "The document name",
|
||||||
|
required: true
|
||||||
|
)
|
||||||
|
| Helpers.admin_api_params()
|
||||||
|
],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("InstanceDocument", "application/json", instance_document()),
|
||||||
|
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||||
|
403 => Operation.response("Forbidden", "application/json", ApiError),
|
||||||
|
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp instance_document do
|
||||||
|
%Schema{
|
||||||
|
title: "InstanceDocument",
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
url: %Schema{type: :string}
|
||||||
|
},
|
||||||
|
example: %{
|
||||||
|
"url" => "https://example.com/static/terms-of-service.html"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp document_content do
|
||||||
|
Operation.response("InstanceDocumentContent", "text/html", %Schema{
|
||||||
|
type: :string,
|
||||||
|
example: "<h1>Instance panel</h1>"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
|
@ -184,7 +184,8 @@ def post_chat_message_operation do
|
||||||
"application/json",
|
"application/json",
|
||||||
ChatMessage
|
ChatMessage
|
||||||
),
|
),
|
||||||
400 => Operation.response("Bad Request", "application/json", ApiError)
|
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||||
|
422 => Operation.response("MRF Rejection", "application/json", ApiError)
|
||||||
},
|
},
|
||||||
security: [
|
security: [
|
||||||
%{
|
%{
|
||||||
|
|
|
@ -69,7 +69,7 @@ defp custom_emoji do
|
||||||
type: :object,
|
type: :object,
|
||||||
properties: %{
|
properties: %{
|
||||||
category: %Schema{type: :string},
|
category: %Schema{type: :string},
|
||||||
tags: %Schema{type: :array}
|
tags: %Schema{type: :array, items: %Schema{type: :string}}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -23,7 +23,7 @@ def index_operation do
|
||||||
parameters: [
|
parameters: [
|
||||||
Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
|
Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
|
||||||
Operation.parameter(:emoji, :path, :string, "Filter by a single unicode emoji",
|
Operation.parameter(:emoji, :path, :string, "Filter by a single unicode emoji",
|
||||||
required: false
|
required: nil
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
security: [%{"oAuth" => ["read:statuses"]}],
|
security: [%{"oAuth" => ["read:statuses"]}],
|
||||||
|
|
|
@ -187,8 +187,7 @@ defp add_remove_accounts_request(required) when is_boolean(required) do
|
||||||
type: :object,
|
type: :object,
|
||||||
properties: %{
|
properties: %{
|
||||||
account_ids: %Schema{type: :array, description: "Array of account IDs", items: FlakeID}
|
account_ids: %Schema{type: :array, description: "Array of account IDs", items: FlakeID}
|
||||||
},
|
}
|
||||||
required: required && [:account_ids]
|
|
||||||
},
|
},
|
||||||
required: required
|
required: required
|
||||||
)
|
)
|
||||||
|
|
|
@ -55,7 +55,7 @@ def create_operation do
|
||||||
"application/json",
|
"application/json",
|
||||||
%Schema{oneOf: [Status, ScheduledStatus]}
|
%Schema{oneOf: [Status, ScheduledStatus]}
|
||||||
),
|
),
|
||||||
422 => Operation.response("Bad Request", "application/json", ApiError)
|
422 => Operation.response("Bad Request / MRF Rejection", "application/json", ApiError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessage do
|
defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessage do
|
||||||
alias OpenApiSpex.Schema
|
alias OpenApiSpex.Schema
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.Emoji
|
||||||
|
|
||||||
require OpenApiSpex
|
require OpenApiSpex
|
||||||
|
|
||||||
|
@ -18,7 +19,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessage do
|
||||||
chat_id: %Schema{type: :string},
|
chat_id: %Schema{type: :string},
|
||||||
content: %Schema{type: :string, nullable: true},
|
content: %Schema{type: :string, nullable: true},
|
||||||
created_at: %Schema{type: :string, format: :"date-time"},
|
created_at: %Schema{type: :string, format: :"date-time"},
|
||||||
emojis: %Schema{type: :array},
|
emojis: %Schema{type: :array, items: Emoji},
|
||||||
attachment: %Schema{type: :object, nullable: true},
|
attachment: %Schema{type: :object, nullable: true},
|
||||||
card: %Schema{
|
card: %Schema{
|
||||||
type: :object,
|
type: :object,
|
||||||
|
|
|
@ -27,9 +27,9 @@ defmodule Pleroma.Web.ApiSpec.Schemas.ScheduledStatus do
|
||||||
media_ids: %Schema{type: :array, nullable: true, items: %Schema{type: :string}},
|
media_ids: %Schema{type: :array, nullable: true, items: %Schema{type: :string}},
|
||||||
sensitive: %Schema{type: :boolean, nullable: true},
|
sensitive: %Schema{type: :boolean, nullable: true},
|
||||||
spoiler_text: %Schema{type: :string, nullable: true},
|
spoiler_text: %Schema{type: :string, nullable: true},
|
||||||
visibility: %Schema{type: VisibilityScope, nullable: true},
|
visibility: %Schema{allOf: [VisibilityScope], nullable: true},
|
||||||
scheduled_at: %Schema{type: :string, format: :"date-time", nullable: true},
|
scheduled_at: %Schema{type: :string, format: :"date-time", nullable: true},
|
||||||
poll: %Schema{type: Poll, nullable: true},
|
poll: %Schema{allOf: [Poll], nullable: true},
|
||||||
in_reply_to_id: %Schema{type: :string, nullable: true}
|
in_reply_to_id: %Schema{type: :string, nullable: true}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,9 @@ def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ [])
|
||||||
local: true
|
local: true
|
||||||
)} do
|
)} do
|
||||||
{:ok, activity}
|
{:ok, activity}
|
||||||
|
else
|
||||||
|
{:common_pipeline, {:reject, _} = e} -> e
|
||||||
|
e -> e
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -560,4 +563,21 @@ def hide_reblogs(%User{} = user, %User{} = target) do
|
||||||
def show_reblogs(%User{} = user, %User{} = target) do
|
def show_reblogs(%User{} = user, %User{} = target) do
|
||||||
UserRelationship.delete_reblog_mute(user, target)
|
UserRelationship.delete_reblog_mute(user, target)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_user(ap_id, fake_record_fallback \\ true) do
|
||||||
|
cond do
|
||||||
|
user = User.get_cached_by_ap_id(ap_id) ->
|
||||||
|
user
|
||||||
|
|
||||||
|
user = User.get_by_guessed_nickname(ap_id) ->
|
||||||
|
user
|
||||||
|
|
||||||
|
fake_record_fallback ->
|
||||||
|
# TODO: refactor (fake records is never a good idea)
|
||||||
|
User.error_user(ap_id)
|
||||||
|
|
||||||
|
true ->
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
185
lib/pleroma/web/fed_sockets/fed_registry.ex
Normal file
185
lib/pleroma/web/fed_sockets/fed_registry.ex
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.FedSockets.FedRegistry do
|
||||||
|
@moduledoc """
|
||||||
|
The FedRegistry stores the active FedSockets for quick retrieval.
|
||||||
|
|
||||||
|
The storage and retrieval portion of the FedRegistry is done in process through
|
||||||
|
elixir's `Registry` module for speed and its ability to monitor for terminated processes.
|
||||||
|
|
||||||
|
Dropped connections will be caught by `Registry` and deleted. Since the next
|
||||||
|
message will initiate a new connection there is no reason to try and reconnect at that point.
|
||||||
|
|
||||||
|
Normally outside modules should have no need to call or use the FedRegistry themselves.
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias Pleroma.Web.FedSockets.FedSocket
|
||||||
|
alias Pleroma.Web.FedSockets.SocketInfo
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@default_rejection_duration 15 * 60 * 1000
|
||||||
|
@rejections :fed_socket_rejections
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Retrieves a FedSocket from the Registry given it's origin.
|
||||||
|
|
||||||
|
The origin is expected to be a string identifying the endpoint "example.com" or "example2.com:8080"
|
||||||
|
|
||||||
|
Will return:
|
||||||
|
* {:ok, fed_socket} for working FedSockets
|
||||||
|
* {:error, :rejected} for origins that have been tried and refused within the rejection duration interval
|
||||||
|
* {:error, some_reason} usually :missing for unknown origins
|
||||||
|
"""
|
||||||
|
def get_fed_socket(origin) do
|
||||||
|
case get_registry_data(origin) do
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
|
||||||
|
{:ok, %{state: :connected} = socket_info} ->
|
||||||
|
{:ok, socket_info}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Adds a connected FedSocket to the Registry.
|
||||||
|
|
||||||
|
Always returns {:ok, fed_socket}
|
||||||
|
"""
|
||||||
|
def add_fed_socket(origin, pid \\ nil) do
|
||||||
|
origin
|
||||||
|
|> SocketInfo.build(pid)
|
||||||
|
|> SocketInfo.connect()
|
||||||
|
|> add_socket_info
|
||||||
|
end
|
||||||
|
|
||||||
|
defp add_socket_info(%{origin: origin, state: :connected} = socket_info) do
|
||||||
|
case Registry.register(FedSockets.Registry, origin, socket_info) do
|
||||||
|
{:ok, _owner} ->
|
||||||
|
clear_prior_rejection(origin)
|
||||||
|
Logger.debug("fedsocket added: #{inspect(origin)}")
|
||||||
|
|
||||||
|
{:ok, socket_info}
|
||||||
|
|
||||||
|
{:error, {:already_registered, _pid}} ->
|
||||||
|
FedSocket.close(socket_info)
|
||||||
|
existing_socket_info = Registry.lookup(FedSockets.Registry, origin)
|
||||||
|
|
||||||
|
{:ok, existing_socket_info}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{:error, :error_adding_socket}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Mark this origin as having rejected a connection attempt.
|
||||||
|
This will keep it from getting additional connection attempts
|
||||||
|
for a period of time specified in the config.
|
||||||
|
|
||||||
|
Always returns {:ok, new_reg_data}
|
||||||
|
"""
|
||||||
|
def set_host_rejected(uri) do
|
||||||
|
new_reg_data =
|
||||||
|
uri
|
||||||
|
|> SocketInfo.origin()
|
||||||
|
|> get_or_create_registry_data()
|
||||||
|
|> set_to_rejected()
|
||||||
|
|> save_registry_data()
|
||||||
|
|
||||||
|
{:ok, new_reg_data}
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Retrieves the FedRegistryData from the Registry given it's origin.
|
||||||
|
|
||||||
|
The origin is expected to be a string identifying the endpoint "example.com" or "example2.com:8080"
|
||||||
|
|
||||||
|
Will return:
|
||||||
|
* {:ok, fed_registry_data} for known origins
|
||||||
|
* {:error, :missing} for uniknown origins
|
||||||
|
* {:error, :cache_error} indicating some low level runtime issues
|
||||||
|
"""
|
||||||
|
def get_registry_data(origin) do
|
||||||
|
case Registry.lookup(FedSockets.Registry, origin) do
|
||||||
|
[] ->
|
||||||
|
if is_rejected?(origin) do
|
||||||
|
Logger.debug("previously rejected fedsocket requested")
|
||||||
|
{:error, :rejected}
|
||||||
|
else
|
||||||
|
{:error, :missing}
|
||||||
|
end
|
||||||
|
|
||||||
|
[{_pid, %{state: :connected} = socket_info}] ->
|
||||||
|
{:ok, socket_info}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{:error, :cache_error}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Retrieves a map of all sockets from the Registry. The keys are the origins and the values are the corresponding SocketInfo
|
||||||
|
"""
|
||||||
|
def list_all do
|
||||||
|
(list_all_connected() ++ list_all_rejected())
|
||||||
|
|> Enum.into(%{})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp list_all_connected do
|
||||||
|
FedSockets.Registry
|
||||||
|
|> Registry.select([{{:"$1", :_, :"$3"}, [], [{{:"$1", :"$3"}}]}])
|
||||||
|
end
|
||||||
|
|
||||||
|
defp list_all_rejected do
|
||||||
|
{:ok, keys} = Cachex.keys(@rejections)
|
||||||
|
|
||||||
|
{:ok, registry_data} =
|
||||||
|
Cachex.execute(@rejections, fn worker ->
|
||||||
|
Enum.map(keys, fn k -> {k, Cachex.get!(worker, k)} end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
registry_data
|
||||||
|
end
|
||||||
|
|
||||||
|
defp clear_prior_rejection(origin),
|
||||||
|
do: Cachex.del(@rejections, origin)
|
||||||
|
|
||||||
|
defp is_rejected?(origin) do
|
||||||
|
case Cachex.get(@rejections, origin) do
|
||||||
|
{:ok, nil} ->
|
||||||
|
false
|
||||||
|
|
||||||
|
{:ok, _} ->
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_or_create_registry_data(origin) do
|
||||||
|
case get_registry_data(origin) do
|
||||||
|
{:error, :missing} ->
|
||||||
|
%SocketInfo{origin: origin}
|
||||||
|
|
||||||
|
{:ok, socket_info} ->
|
||||||
|
socket_info
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp save_registry_data(%SocketInfo{origin: origin, state: :connected} = socket_info) do
|
||||||
|
{:ok, true} = Registry.update_value(FedSockets.Registry, origin, fn _ -> socket_info end)
|
||||||
|
socket_info
|
||||||
|
end
|
||||||
|
|
||||||
|
defp save_registry_data(%SocketInfo{origin: origin, state: :rejected} = socket_info) do
|
||||||
|
rejection_expiration =
|
||||||
|
Pleroma.Config.get([:fed_sockets, :rejection_duration], @default_rejection_duration)
|
||||||
|
|
||||||
|
{:ok, true} = Cachex.put(@rejections, origin, socket_info, ttl: rejection_expiration)
|
||||||
|
socket_info
|
||||||
|
end
|
||||||
|
|
||||||
|
defp set_to_rejected(%SocketInfo{} = socket_info),
|
||||||
|
do: %SocketInfo{socket_info | state: :rejected}
|
||||||
|
end
|
137
lib/pleroma/web/fed_sockets/fed_socket.ex
Normal file
137
lib/pleroma/web/fed_sockets/fed_socket.ex
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.FedSockets.FedSocket do
|
||||||
|
@moduledoc """
|
||||||
|
The FedSocket module abstracts the actions to be taken taken on connections regardless of
|
||||||
|
whether the connection started as inbound or outbound.
|
||||||
|
|
||||||
|
|
||||||
|
Normally outside modules will have no need to call the FedSocket module directly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias Pleroma.Object
|
||||||
|
alias Pleroma.Object.Containment
|
||||||
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Web.ActivityPub.ObjectView
|
||||||
|
alias Pleroma.Web.ActivityPub.UserView
|
||||||
|
alias Pleroma.Web.ActivityPub.Visibility
|
||||||
|
alias Pleroma.Web.FedSockets.FetchRegistry
|
||||||
|
alias Pleroma.Web.FedSockets.IngesterWorker
|
||||||
|
alias Pleroma.Web.FedSockets.OutgoingHandler
|
||||||
|
alias Pleroma.Web.FedSockets.SocketInfo
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@shake "61dd18f7-f1e6-49a4-939a-a749fcdc1103"
|
||||||
|
|
||||||
|
def connect_to_host(uri) do
|
||||||
|
case OutgoingHandler.start_link(uri) do
|
||||||
|
{:ok, pid} ->
|
||||||
|
{:ok, pid}
|
||||||
|
|
||||||
|
error ->
|
||||||
|
{:error, error}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def close(%SocketInfo{pid: socket_pid}),
|
||||||
|
do: Process.send(socket_pid, :close, [])
|
||||||
|
|
||||||
|
def publish(%SocketInfo{pid: socket_pid}, json) do
|
||||||
|
%{action: :publish, data: json}
|
||||||
|
|> Jason.encode!()
|
||||||
|
|> send_packet(socket_pid)
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch(%SocketInfo{pid: socket_pid}, id) do
|
||||||
|
fetch_uuid = FetchRegistry.register_fetch(id)
|
||||||
|
|
||||||
|
%{action: :fetch, data: id, uuid: fetch_uuid}
|
||||||
|
|> Jason.encode!()
|
||||||
|
|> send_packet(socket_pid)
|
||||||
|
|
||||||
|
wait_for_fetch_to_return(fetch_uuid, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
def receive_package(%SocketInfo{} = fed_socket, json) do
|
||||||
|
json
|
||||||
|
|> Jason.decode!()
|
||||||
|
|> process_package(fed_socket)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp wait_for_fetch_to_return(uuid, cntr) do
|
||||||
|
case FetchRegistry.check_fetch(uuid) do
|
||||||
|
{:error, :waiting} ->
|
||||||
|
Process.sleep(:math.pow(cntr, 3) |> Kernel.trunc())
|
||||||
|
wait_for_fetch_to_return(uuid, cntr + 1)
|
||||||
|
|
||||||
|
{:error, :missing} ->
|
||||||
|
Logger.error("FedSocket fetch timed out - #{inspect(uuid)}")
|
||||||
|
{:error, :timeout}
|
||||||
|
|
||||||
|
{:ok, _fr} ->
|
||||||
|
FetchRegistry.pop_fetch(uuid)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp process_package(%{"action" => "publish", "data" => data}, %{origin: origin} = _fed_socket) do
|
||||||
|
if Containment.contain_origin(origin, data) do
|
||||||
|
IngesterWorker.enqueue("ingest", %{"object" => data})
|
||||||
|
end
|
||||||
|
|
||||||
|
{:reply, %{"action" => "publish_reply", "status" => "processed"}}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp process_package(%{"action" => "fetch_reply", "uuid" => uuid, "data" => data}, _fed_socket) do
|
||||||
|
FetchRegistry.register_fetch_received(uuid, data)
|
||||||
|
{:noreply, nil}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp process_package(%{"action" => "fetch", "uuid" => uuid, "data" => ap_id}, _fed_socket) do
|
||||||
|
{:ok, data} = render_fetched_data(ap_id, uuid)
|
||||||
|
{:reply, data}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp process_package(%{"action" => "publish_reply"}, _fed_socket) do
|
||||||
|
{:noreply, nil}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp process_package(other, _fed_socket) do
|
||||||
|
Logger.warn("unknown json packages received #{inspect(other)}")
|
||||||
|
{:noreply, nil}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp render_fetched_data(ap_id, uuid) do
|
||||||
|
{:ok,
|
||||||
|
%{
|
||||||
|
"action" => "fetch_reply",
|
||||||
|
"status" => "processed",
|
||||||
|
"uuid" => uuid,
|
||||||
|
"data" => represent_item(ap_id)
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp represent_item(ap_id) do
|
||||||
|
case User.get_by_ap_id(ap_id) do
|
||||||
|
nil ->
|
||||||
|
object = Object.get_cached_by_ap_id(ap_id)
|
||||||
|
|
||||||
|
if Visibility.is_public?(object) do
|
||||||
|
Phoenix.View.render_to_string(ObjectView, "object.json", object: object)
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
user ->
|
||||||
|
Phoenix.View.render_to_string(UserView, "user.json", user: user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp send_packet(data, socket_pid) do
|
||||||
|
Process.send(socket_pid, {:send, data}, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def shake, do: @shake
|
||||||
|
end
|
185
lib/pleroma/web/fed_sockets/fed_sockets.ex
Normal file
185
lib/pleroma/web/fed_sockets/fed_sockets.ex
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.FedSockets do
|
||||||
|
@moduledoc """
|
||||||
|
This documents the FedSockets framework. A framework for federating
|
||||||
|
ActivityPub objects between servers via persistant WebSocket connections.
|
||||||
|
|
||||||
|
FedSockets allow servers to authenticate on first contact and maintain that
|
||||||
|
connection, eliminating the need to authenticate every time data needs to be shared.
|
||||||
|
|
||||||
|
## Protocol
|
||||||
|
FedSockets currently support 2 types of data transfer:
|
||||||
|
* `publish` method which doesn't require a response
|
||||||
|
* `fetch` method requires a response be sent
|
||||||
|
|
||||||
|
### Publish
|
||||||
|
The publish operation sends a json encoded map of the shape:
|
||||||
|
%{action: :publish, data: json}
|
||||||
|
and accepts (but does not require) a reply of form:
|
||||||
|
%{"action" => "publish_reply"}
|
||||||
|
|
||||||
|
The outgoing params represent
|
||||||
|
* data: ActivityPub object encoded into json
|
||||||
|
|
||||||
|
|
||||||
|
### Fetch
|
||||||
|
The fetch operation sends a json encoded map of the shape:
|
||||||
|
%{action: :fetch, data: id, uuid: fetch_uuid}
|
||||||
|
and requires a reply of form:
|
||||||
|
%{"action" => "fetch_reply", "uuid" => uuid, "data" => data}
|
||||||
|
|
||||||
|
The outgoing params represent
|
||||||
|
* id: an ActivityPub object URI
|
||||||
|
* uuid: a unique uuid generated by the sender
|
||||||
|
|
||||||
|
The reply params represent
|
||||||
|
* data: an ActivityPub object encoded into json
|
||||||
|
* uuid: the uuid sent along with the fetch request
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
Clients of FedSocket transfers shouldn't need to use any of the functions outside of this module.
|
||||||
|
|
||||||
|
A typical publish operation can be performed through the following code, and a fetch operation in a similar manner.
|
||||||
|
|
||||||
|
case FedSockets.get_or_create_fed_socket(inbox) do
|
||||||
|
{:ok, fedsocket} ->
|
||||||
|
FedSockets.publish(fedsocket, json)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
alternative_publish(inbox, actor, json, params)
|
||||||
|
end
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
FedSockets have the following config settings
|
||||||
|
|
||||||
|
config :pleroma, :fed_sockets,
|
||||||
|
enabled: true,
|
||||||
|
ping_interval: :timer.seconds(15),
|
||||||
|
connection_duration: :timer.hours(1),
|
||||||
|
rejection_duration: :timer.hours(1),
|
||||||
|
fed_socket_fetches: [
|
||||||
|
default: 12_000,
|
||||||
|
interval: 3_000,
|
||||||
|
lazy: false
|
||||||
|
]
|
||||||
|
* enabled - turn FedSockets on or off with this flag. Can be toggled at runtime.
|
||||||
|
* connection_duration - How long a FedSocket can sit idle before it's culled.
|
||||||
|
* rejection_duration - After failing to make a FedSocket connection a host will be excluded
|
||||||
|
from further connections for this amount of time
|
||||||
|
* fed_socket_fetches - Use these parameters to pass options to the Cachex queue backing the FetchRegistry
|
||||||
|
* fed_socket_rejections - Use these parameters to pass options to the Cachex queue backing the FedRegistry
|
||||||
|
|
||||||
|
Cachex options are
|
||||||
|
* default: the minimum amount of time a fetch can wait before it times out.
|
||||||
|
* interval: the interval between checks for timed out entries. This plus the default represent the maximum time allowed
|
||||||
|
* lazy: leave at false for consistant and fast lookups, set to true for stricter timeout enforcement
|
||||||
|
|
||||||
|
"""
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
alias Pleroma.Web.FedSockets.FedRegistry
|
||||||
|
alias Pleroma.Web.FedSockets.FedSocket
|
||||||
|
alias Pleroma.Web.FedSockets.SocketInfo
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
returns a FedSocket for the given origin. Will reuse an existing one or create a new one.
|
||||||
|
|
||||||
|
address is expected to be a fully formed URL such as:
|
||||||
|
"http://www.example.com" or "http://www.example.com:8080"
|
||||||
|
|
||||||
|
It can and usually does include additional path parameters,
|
||||||
|
but these are ignored as the FedSockets are organized by host and port info alone.
|
||||||
|
"""
|
||||||
|
def get_or_create_fed_socket(address) do
|
||||||
|
with {:cache, {:error, :missing}} <- {:cache, get_fed_socket(address)},
|
||||||
|
{:connect, {:ok, _pid}} <- {:connect, FedSocket.connect_to_host(address)},
|
||||||
|
{:cache, {:ok, fed_socket}} <- {:cache, get_fed_socket(address)} do
|
||||||
|
Logger.debug("fedsocket created for - #{inspect(address)}")
|
||||||
|
{:ok, fed_socket}
|
||||||
|
else
|
||||||
|
{:cache, {:ok, socket}} ->
|
||||||
|
Logger.debug("fedsocket found in cache - #{inspect(address)}")
|
||||||
|
{:ok, socket}
|
||||||
|
|
||||||
|
{:cache, {:error, :rejected} = e} ->
|
||||||
|
e
|
||||||
|
|
||||||
|
{:connect, {:error, _host}} ->
|
||||||
|
Logger.debug("set host rejected for - #{inspect(address)}")
|
||||||
|
FedRegistry.set_host_rejected(address)
|
||||||
|
{:error, :rejected}
|
||||||
|
|
||||||
|
{_, {:error, :disabled}} ->
|
||||||
|
{:error, :disabled}
|
||||||
|
|
||||||
|
{_, {:error, reason}} ->
|
||||||
|
Logger.warn("get_or_create_fed_socket error - #{inspect(reason)}")
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
returns a FedSocket for the given origin. Will not create a new FedSocket if one does not exist.
|
||||||
|
|
||||||
|
address is expected to be a fully formed URL such as:
|
||||||
|
"http://www.example.com" or "http://www.example.com:8080"
|
||||||
|
"""
|
||||||
|
def get_fed_socket(address) do
|
||||||
|
origin = SocketInfo.origin(address)
|
||||||
|
|
||||||
|
with {:config, true} <- {:config, Pleroma.Config.get([:fed_sockets, :enabled], false)},
|
||||||
|
{:ok, socket} <- FedRegistry.get_fed_socket(origin) do
|
||||||
|
{:ok, socket}
|
||||||
|
else
|
||||||
|
{:config, _} ->
|
||||||
|
{:error, :disabled}
|
||||||
|
|
||||||
|
{:error, :rejected} ->
|
||||||
|
Logger.debug("FedSocket previously rejected - #{inspect(origin)}")
|
||||||
|
{:error, :rejected}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Sends the supplied data via the publish protocol.
|
||||||
|
It will not block waiting for a reply.
|
||||||
|
Returns :ok but this is not an indication of a successful transfer.
|
||||||
|
|
||||||
|
the data is expected to be JSON encoded binary data.
|
||||||
|
"""
|
||||||
|
def publish(%SocketInfo{} = fed_socket, json) do
|
||||||
|
FedSocket.publish(fed_socket, json)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Sends the supplied data via the fetch protocol.
|
||||||
|
It will block waiting for a reply or timeout.
|
||||||
|
|
||||||
|
Returns {:ok, object} where object is the requested object (or nil)
|
||||||
|
{:error, :timeout} in the event the message was not responded to
|
||||||
|
|
||||||
|
the id is expected to be the URI of an ActivityPub object.
|
||||||
|
"""
|
||||||
|
def fetch(%SocketInfo{} = fed_socket, id) do
|
||||||
|
FedSocket.fetch(fed_socket, id)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Disconnect all and restart FedSockets.
|
||||||
|
This is mainly used in development and testing but could be useful in production.
|
||||||
|
"""
|
||||||
|
def reset do
|
||||||
|
FedRegistry
|
||||||
|
|> Process.whereis()
|
||||||
|
|> Process.exit(:testing)
|
||||||
|
end
|
||||||
|
|
||||||
|
def uri_for_origin(origin),
|
||||||
|
do: "ws://#{origin}/api/fedsocket/v1"
|
||||||
|
end
|
151
lib/pleroma/web/fed_sockets/fetch_registry.ex
Normal file
151
lib/pleroma/web/fed_sockets/fetch_registry.ex
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.FedSockets.FetchRegistry do
|
||||||
|
@moduledoc """
|
||||||
|
The FetchRegistry acts as a broker for fetch requests and return values.
|
||||||
|
This allows calling processes to block while waiting for a reply.
|
||||||
|
It doesn't impose it's own process instead using `Cachex` to handle fetches in process, allowing
|
||||||
|
multi threaded processes to avoid bottlenecking.
|
||||||
|
|
||||||
|
Normally outside modules will have no need to call or use the FetchRegistry themselves.
|
||||||
|
|
||||||
|
The `Cachex` parameters can be controlled from the config. Since exact timeout intervals
|
||||||
|
aren't necessary the following settings are used by default:
|
||||||
|
|
||||||
|
config :pleroma, :fed_sockets,
|
||||||
|
fed_socket_fetches: [
|
||||||
|
default: 12_000,
|
||||||
|
interval: 3_000,
|
||||||
|
lazy: false
|
||||||
|
]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
defmodule FetchRegistryData do
|
||||||
|
defstruct uuid: nil,
|
||||||
|
sent_json: nil,
|
||||||
|
received_json: nil,
|
||||||
|
sent_at: nil,
|
||||||
|
received_at: nil
|
||||||
|
end
|
||||||
|
|
||||||
|
alias Ecto.UUID
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@fetches :fed_socket_fetches
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Registers a json request wth the FetchRegistry and returns the identifying UUID.
|
||||||
|
"""
|
||||||
|
def register_fetch(json) do
|
||||||
|
%FetchRegistryData{uuid: uuid} =
|
||||||
|
json
|
||||||
|
|> new_registry_data
|
||||||
|
|> save_registry_data
|
||||||
|
|
||||||
|
uuid
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Reports on the status of a Fetch given the identifying UUID.
|
||||||
|
|
||||||
|
Will return
|
||||||
|
* {:ok, fetched_object} if a fetch has completed
|
||||||
|
* {:error, :waiting} if a fetch is still pending
|
||||||
|
* {:error, other_error} usually :missing to indicate a fetch that has timed out
|
||||||
|
"""
|
||||||
|
def check_fetch(uuid) do
|
||||||
|
case get_registry_data(uuid) do
|
||||||
|
{:ok, %FetchRegistryData{received_at: nil}} ->
|
||||||
|
{:error, :waiting}
|
||||||
|
|
||||||
|
{:ok, %FetchRegistryData{} = reg_data} ->
|
||||||
|
{:ok, reg_data}
|
||||||
|
|
||||||
|
e ->
|
||||||
|
e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Retrieves the response to a fetch given the identifying UUID.
|
||||||
|
The completed fetch will be deleted from the FetchRegistry
|
||||||
|
|
||||||
|
Will return
|
||||||
|
* {:ok, fetched_object} if a fetch has completed
|
||||||
|
* {:error, :waiting} if a fetch is still pending
|
||||||
|
* {:error, other_error} usually :missing to indicate a fetch that has timed out
|
||||||
|
"""
|
||||||
|
def pop_fetch(uuid) do
|
||||||
|
case check_fetch(uuid) do
|
||||||
|
{:ok, %FetchRegistryData{received_json: received_json}} ->
|
||||||
|
delete_registry_data(uuid)
|
||||||
|
{:ok, received_json}
|
||||||
|
|
||||||
|
e ->
|
||||||
|
e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
This is called to register a fetch has returned.
|
||||||
|
It expects the result data along with the UUID that was sent in the request
|
||||||
|
|
||||||
|
Will return the fetched object or :error
|
||||||
|
"""
|
||||||
|
def register_fetch_received(uuid, data) do
|
||||||
|
case get_registry_data(uuid) do
|
||||||
|
{:ok, %FetchRegistryData{received_at: nil} = reg_data} ->
|
||||||
|
reg_data
|
||||||
|
|> set_fetch_received(data)
|
||||||
|
|> save_registry_data()
|
||||||
|
|
||||||
|
{:ok, %FetchRegistryData{} = reg_data} ->
|
||||||
|
Logger.warn("tried to add fetched data twice - #{uuid}")
|
||||||
|
reg_data
|
||||||
|
|
||||||
|
{:error, _} ->
|
||||||
|
Logger.warn("Error adding fetch to registry - #{uuid}")
|
||||||
|
:error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp new_registry_data(json) do
|
||||||
|
%FetchRegistryData{
|
||||||
|
uuid: UUID.generate(),
|
||||||
|
sent_json: json,
|
||||||
|
sent_at: :erlang.monotonic_time(:millisecond)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_registry_data(origin) do
|
||||||
|
case Cachex.get(@fetches, origin) do
|
||||||
|
{:ok, nil} ->
|
||||||
|
{:error, :missing}
|
||||||
|
|
||||||
|
{:ok, reg_data} ->
|
||||||
|
{:ok, reg_data}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{:error, :cache_error}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp set_fetch_received(%FetchRegistryData{} = reg_data, data),
|
||||||
|
do: %FetchRegistryData{
|
||||||
|
reg_data
|
||||||
|
| received_at: :erlang.monotonic_time(:millisecond),
|
||||||
|
received_json: data
|
||||||
|
}
|
||||||
|
|
||||||
|
defp save_registry_data(%FetchRegistryData{uuid: uuid} = reg_data) do
|
||||||
|
{:ok, true} = Cachex.put(@fetches, uuid, reg_data)
|
||||||
|
reg_data
|
||||||
|
end
|
||||||
|
|
||||||
|
defp delete_registry_data(origin),
|
||||||
|
do: {:ok, true} = Cachex.del(@fetches, origin)
|
||||||
|
end
|
88
lib/pleroma/web/fed_sockets/incoming_handler.ex
Normal file
88
lib/pleroma/web/fed_sockets/incoming_handler.ex
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.FedSockets.IncomingHandler do
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
alias Pleroma.Web.FedSockets.FedRegistry
|
||||||
|
alias Pleroma.Web.FedSockets.FedSocket
|
||||||
|
alias Pleroma.Web.FedSockets.SocketInfo
|
||||||
|
|
||||||
|
import HTTPSignatures, only: [validate_conn: 1, split_signature: 1]
|
||||||
|
|
||||||
|
@behaviour :cowboy_websocket
|
||||||
|
|
||||||
|
def init(req, state) do
|
||||||
|
shake = FedSocket.shake()
|
||||||
|
|
||||||
|
with true <- Pleroma.Config.get([:fed_sockets, :enabled]),
|
||||||
|
sec_protocol <- :cowboy_req.header("sec-websocket-protocol", req, nil),
|
||||||
|
headers = %{"(request-target)" => ^shake} <- :cowboy_req.headers(req),
|
||||||
|
true <- validate_conn(%{req_headers: headers}),
|
||||||
|
%{"keyId" => origin} <- split_signature(headers["signature"]) do
|
||||||
|
req =
|
||||||
|
if is_nil(sec_protocol) do
|
||||||
|
req
|
||||||
|
else
|
||||||
|
:cowboy_req.set_resp_header("sec-websocket-protocol", sec_protocol, req)
|
||||||
|
end
|
||||||
|
|
||||||
|
{:cowboy_websocket, req, %{origin: origin}, %{}}
|
||||||
|
else
|
||||||
|
_ ->
|
||||||
|
{:ok, req, state}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def websocket_init(%{origin: origin}) do
|
||||||
|
case FedRegistry.add_fed_socket(origin) do
|
||||||
|
{:ok, socket_info} ->
|
||||||
|
{:ok, socket_info}
|
||||||
|
|
||||||
|
e ->
|
||||||
|
Logger.error("FedSocket websocket_init failed - #{inspect(e)}")
|
||||||
|
{:error, inspect(e)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Use the ping to check if the connection should be expired
|
||||||
|
def websocket_handle(:ping, socket_info) do
|
||||||
|
if SocketInfo.expired?(socket_info) do
|
||||||
|
{:stop, socket_info}
|
||||||
|
else
|
||||||
|
{:ok, socket_info, :hibernate}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def websocket_handle({:text, data}, socket_info) do
|
||||||
|
socket_info = SocketInfo.touch(socket_info)
|
||||||
|
|
||||||
|
case FedSocket.receive_package(socket_info, data) do
|
||||||
|
{:noreply, _} ->
|
||||||
|
{:ok, socket_info}
|
||||||
|
|
||||||
|
{:reply, reply} ->
|
||||||
|
{:reply, {:text, Jason.encode!(reply)}, socket_info}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
Logger.error("incoming error - receive_package: #{inspect(reason)}")
|
||||||
|
{:ok, socket_info}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def websocket_info({:send, message}, socket_info) do
|
||||||
|
socket_info = SocketInfo.touch(socket_info)
|
||||||
|
|
||||||
|
{:reply, {:text, message}, socket_info}
|
||||||
|
end
|
||||||
|
|
||||||
|
def websocket_info(:close, state) do
|
||||||
|
{:stop, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def websocket_info(message, state) do
|
||||||
|
Logger.debug("#{__MODULE__} unknown message #{inspect(message)}")
|
||||||
|
{:ok, state}
|
||||||
|
end
|
||||||
|
end
|
33
lib/pleroma/web/fed_sockets/ingester_worker.ex
Normal file
33
lib/pleroma/web/fed_sockets/ingester_worker.ex
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.FedSockets.IngesterWorker do
|
||||||
|
use Pleroma.Workers.WorkerHelper, queue: "ingestion_queue"
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
alias Pleroma.Web.Federator
|
||||||
|
|
||||||
|
@impl Oban.Worker
|
||||||
|
def perform(%Job{args: %{"op" => "ingest", "object" => ingestee}}) do
|
||||||
|
try do
|
||||||
|
ingestee
|
||||||
|
|> Jason.decode!()
|
||||||
|
|> do_ingestion()
|
||||||
|
rescue
|
||||||
|
e ->
|
||||||
|
Logger.error("IngesterWorker error - #{inspect(e)}")
|
||||||
|
e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_ingestion(params) do
|
||||||
|
case Federator.incoming_ap_doc(params) do
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
|
||||||
|
{:ok, object} ->
|
||||||
|
{:ok, object}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
146
lib/pleroma/web/fed_sockets/outgoing_handler.ex
Normal file
146
lib/pleroma/web/fed_sockets/outgoing_handler.ex
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.FedSockets.OutgoingHandler do
|
||||||
|
use GenServer
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
alias Pleroma.Web.ActivityPub.InternalFetchActor
|
||||||
|
alias Pleroma.Web.FedSockets
|
||||||
|
alias Pleroma.Web.FedSockets.FedRegistry
|
||||||
|
alias Pleroma.Web.FedSockets.FedSocket
|
||||||
|
alias Pleroma.Web.FedSockets.SocketInfo
|
||||||
|
|
||||||
|
def start_link(uri) do
|
||||||
|
GenServer.start_link(__MODULE__, %{uri: uri})
|
||||||
|
end
|
||||||
|
|
||||||
|
def init(%{uri: uri}) do
|
||||||
|
case initiate_connection(uri) do
|
||||||
|
{:ok, ws_origin, conn_pid} ->
|
||||||
|
FedRegistry.add_fed_socket(ws_origin, conn_pid)
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
Logger.debug("Outgoing connection failed - #{inspect(reason)}")
|
||||||
|
:ignore
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({:gun_ws, conn_pid, _ref, {:text, data}}, socket_info) do
|
||||||
|
socket_info = SocketInfo.touch(socket_info)
|
||||||
|
|
||||||
|
case FedSocket.receive_package(socket_info, data) do
|
||||||
|
{:noreply, _} ->
|
||||||
|
{:noreply, socket_info}
|
||||||
|
|
||||||
|
{:reply, reply} ->
|
||||||
|
:gun.ws_send(conn_pid, {:text, Jason.encode!(reply)})
|
||||||
|
{:noreply, socket_info}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
Logger.error("incoming error - receive_package: #{inspect(reason)}")
|
||||||
|
{:noreply, socket_info}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info(:close, state) do
|
||||||
|
Logger.debug("Sending close frame !!!!!!!")
|
||||||
|
{:close, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({:gun_down, _pid, _prot, :closed, _}, state) do
|
||||||
|
{:stop, :normal, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({:send, data}, %{conn_pid: conn_pid} = socket_info) do
|
||||||
|
socket_info = SocketInfo.touch(socket_info)
|
||||||
|
:gun.ws_send(conn_pid, {:text, data})
|
||||||
|
{:noreply, socket_info}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({:gun_ws, _, _, :pong}, state) do
|
||||||
|
{:noreply, state, :hibernate}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info(msg, state) do
|
||||||
|
Logger.debug("#{__MODULE__} unhandled event #{inspect(msg)}")
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def terminate(reason, state) do
|
||||||
|
Logger.debug(
|
||||||
|
"#{__MODULE__} terminating outgoing connection for #{inspect(state)} for #{inspect(reason)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
{:ok, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def initiate_connection(uri) do
|
||||||
|
ws_uri =
|
||||||
|
uri
|
||||||
|
|> SocketInfo.origin()
|
||||||
|
|> FedSockets.uri_for_origin()
|
||||||
|
|
||||||
|
%{host: host, port: port, path: path} = URI.parse(ws_uri)
|
||||||
|
|
||||||
|
with {:ok, conn_pid} <- :gun.open(to_charlist(host), port),
|
||||||
|
{:ok, _} <- :gun.await_up(conn_pid),
|
||||||
|
reference <- :gun.get(conn_pid, to_charlist(path)),
|
||||||
|
{:response, :fin, 204, _} <- :gun.await(conn_pid, reference),
|
||||||
|
headers <- build_headers(uri),
|
||||||
|
ref <- :gun.ws_upgrade(conn_pid, to_charlist(path), headers, %{silence_pings: false}) do
|
||||||
|
receive do
|
||||||
|
{:gun_upgrade, ^conn_pid, ^ref, [<<"websocket">>], _} ->
|
||||||
|
{:ok, ws_uri, conn_pid}
|
||||||
|
after
|
||||||
|
15_000 ->
|
||||||
|
Logger.debug("Fedsocket timeout connecting to #{inspect(uri)}")
|
||||||
|
{:error, :timeout}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
{:response, :nofin, 404, _} ->
|
||||||
|
{:error, :fedsockets_not_supported}
|
||||||
|
|
||||||
|
e ->
|
||||||
|
Logger.debug("Fedsocket error connecting to #{inspect(uri)}")
|
||||||
|
{:error, e}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp build_headers(uri) do
|
||||||
|
host_for_sig = uri |> URI.parse() |> host_signature()
|
||||||
|
|
||||||
|
shake = FedSocket.shake()
|
||||||
|
digest = "SHA-256=" <> (:crypto.hash(:sha256, shake) |> Base.encode64())
|
||||||
|
date = Pleroma.Signature.signed_date()
|
||||||
|
shake_size = byte_size(shake)
|
||||||
|
|
||||||
|
signature_opts = %{
|
||||||
|
"(request-target)": shake,
|
||||||
|
"content-length": to_charlist("#{shake_size}"),
|
||||||
|
date: date,
|
||||||
|
digest: digest,
|
||||||
|
host: host_for_sig
|
||||||
|
}
|
||||||
|
|
||||||
|
signature = Pleroma.Signature.sign(InternalFetchActor.get_actor(), signature_opts)
|
||||||
|
|
||||||
|
[
|
||||||
|
{'signature', to_charlist(signature)},
|
||||||
|
{'date', date},
|
||||||
|
{'digest', to_charlist(digest)},
|
||||||
|
{'content-length', to_charlist("#{shake_size}")},
|
||||||
|
{to_charlist("(request-target)"), to_charlist(shake)}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp host_signature(%{host: host, scheme: scheme, port: port}) do
|
||||||
|
if port == URI.default_port(scheme) do
|
||||||
|
host
|
||||||
|
else
|
||||||
|
"#{host}:#{port}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
52
lib/pleroma/web/fed_sockets/socket_info.ex
Normal file
52
lib/pleroma/web/fed_sockets/socket_info.ex
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.FedSockets.SocketInfo do
|
||||||
|
defstruct origin: nil,
|
||||||
|
pid: nil,
|
||||||
|
conn_pid: nil,
|
||||||
|
state: :default,
|
||||||
|
connected_until: nil
|
||||||
|
|
||||||
|
alias Pleroma.Web.FedSockets.SocketInfo
|
||||||
|
@default_connection_duration 15 * 60 * 1000
|
||||||
|
|
||||||
|
def build(uri, conn_pid \\ nil) do
|
||||||
|
uri
|
||||||
|
|> build_origin()
|
||||||
|
|> build_pids(conn_pid)
|
||||||
|
|> touch()
|
||||||
|
end
|
||||||
|
|
||||||
|
def touch(%SocketInfo{} = socket_info),
|
||||||
|
do: %{socket_info | connected_until: new_ttl()}
|
||||||
|
|
||||||
|
def connect(%SocketInfo{} = socket_info),
|
||||||
|
do: %{socket_info | state: :connected}
|
||||||
|
|
||||||
|
def expired?(%{connected_until: connected_until}),
|
||||||
|
do: connected_until < :erlang.monotonic_time(:millisecond)
|
||||||
|
|
||||||
|
def origin(uri),
|
||||||
|
do: build_origin(uri).origin
|
||||||
|
|
||||||
|
defp build_pids(socket_info, conn_pid),
|
||||||
|
do: struct(socket_info, pid: self(), conn_pid: conn_pid)
|
||||||
|
|
||||||
|
defp build_origin(uri) when is_binary(uri),
|
||||||
|
do: uri |> URI.parse() |> build_origin
|
||||||
|
|
||||||
|
defp build_origin(%{host: host, port: nil, scheme: scheme}),
|
||||||
|
do: build_origin(%{host: host, port: URI.default_port(scheme)})
|
||||||
|
|
||||||
|
defp build_origin(%{host: host, port: port}),
|
||||||
|
do: %SocketInfo{origin: "#{host}:#{port}"}
|
||||||
|
|
||||||
|
defp new_ttl do
|
||||||
|
connection_duration =
|
||||||
|
Pleroma.Config.get([:fed_sockets, :connection_duration], @default_connection_duration)
|
||||||
|
|
||||||
|
:erlang.monotonic_time(:millisecond) + connection_duration
|
||||||
|
end
|
||||||
|
end
|
59
lib/pleroma/web/fed_sockets/supervisor.ex
Normal file
59
lib/pleroma/web/fed_sockets/supervisor.ex
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.FedSockets.Supervisor do
|
||||||
|
use Supervisor
|
||||||
|
import Cachex.Spec
|
||||||
|
|
||||||
|
def start_link(opts) do
|
||||||
|
Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
|
||||||
|
end
|
||||||
|
|
||||||
|
def init(args) do
|
||||||
|
children = [
|
||||||
|
build_cache(:fed_socket_fetches, args),
|
||||||
|
build_cache(:fed_socket_rejections, args),
|
||||||
|
{Registry, keys: :unique, name: FedSockets.Registry, meta: [rejected: %{}]}
|
||||||
|
]
|
||||||
|
|
||||||
|
opts = [strategy: :one_for_all, name: Pleroma.Web.Streamer.Supervisor]
|
||||||
|
Supervisor.init(children, opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp build_cache(name, args) do
|
||||||
|
opts = get_opts(name, args)
|
||||||
|
|
||||||
|
%{
|
||||||
|
id: String.to_atom("#{name}_cache"),
|
||||||
|
start: {Cachex, :start_link, [name, opts]},
|
||||||
|
type: :worker
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_opts(cache_name, args)
|
||||||
|
when cache_name in [:fed_socket_fetches, :fed_socket_rejections] do
|
||||||
|
default = get_opts_or_config(args, cache_name, :default, 15_000)
|
||||||
|
interval = get_opts_or_config(args, cache_name, :interval, 3_000)
|
||||||
|
lazy = get_opts_or_config(args, cache_name, :lazy, false)
|
||||||
|
|
||||||
|
[expiration: expiration(default: default, interval: interval, lazy: lazy)]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_opts(name, args) do
|
||||||
|
Keyword.get(args, name, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_opts_or_config(args, name, key, default) do
|
||||||
|
args
|
||||||
|
|> Keyword.get(name, [])
|
||||||
|
|> Keyword.get(key)
|
||||||
|
|> case do
|
||||||
|
nil ->
|
||||||
|
Pleroma.Config.get([:fed_sockets, name, key], default)
|
||||||
|
|
||||||
|
value ->
|
||||||
|
value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
62
lib/pleroma/web/instance_document.ex
Normal file
62
lib/pleroma/web/instance_document.ex
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.InstanceDocument do
|
||||||
|
alias Pleroma.Config
|
||||||
|
alias Pleroma.Web.Endpoint
|
||||||
|
|
||||||
|
@instance_documents %{
|
||||||
|
"terms-of-service" => "/static/terms-of-service.html",
|
||||||
|
"instance-panel" => "/instance/panel.html"
|
||||||
|
}
|
||||||
|
|
||||||
|
@spec get(String.t()) :: {:ok, String.t()} | {:error, atom()}
|
||||||
|
def get(document_name) do
|
||||||
|
case Map.fetch(@instance_documents, document_name) do
|
||||||
|
{:ok, path} -> {:ok, path}
|
||||||
|
_ -> {:error, :not_found}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec put(String.t(), String.t()) :: {:ok, String.t()} | {:error, atom()}
|
||||||
|
def put(document_name, origin_path) do
|
||||||
|
with {_, {:ok, destination_path}} <-
|
||||||
|
{:instance_document, Map.fetch(@instance_documents, document_name)},
|
||||||
|
:ok <- put_file(origin_path, destination_path) do
|
||||||
|
{:ok, Path.join(Endpoint.url(), destination_path)}
|
||||||
|
else
|
||||||
|
{:instance_document, :error} -> {:error, :not_found}
|
||||||
|
error -> error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec delete(String.t()) :: :ok | {:error, atom()}
|
||||||
|
def delete(document_name) do
|
||||||
|
with {_, {:ok, path}} <- {:instance_document, Map.fetch(@instance_documents, document_name)},
|
||||||
|
instance_static_dir_path <- instance_static_dir(path),
|
||||||
|
:ok <- File.rm(instance_static_dir_path) do
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
{:instance_document, :error} -> {:error, :not_found}
|
||||||
|
{:error, :enoent} -> {:error, :not_found}
|
||||||
|
error -> error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp put_file(origin_path, destination_path) do
|
||||||
|
with destination <- instance_static_dir(destination_path),
|
||||||
|
{_, :ok} <- {:mkdir_p, File.mkdir_p(Path.dirname(destination))},
|
||||||
|
{_, {:ok, _}} <- {:copy, File.copy(origin_path, destination)} do
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
{error, _} -> {:error, error}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp instance_static_dir(filename) do
|
||||||
|
[:instance, :static_dir]
|
||||||
|
|> Config.get!()
|
||||||
|
|> Path.join(filename)
|
||||||
|
end
|
||||||
|
end
|
|
@ -181,8 +181,10 @@ defp do_render("show.json", %{user: user} = opts) do
|
||||||
user = User.sanitize_html(user, User.html_filter_policy(opts[:for]))
|
user = User.sanitize_html(user, User.html_filter_policy(opts[:for]))
|
||||||
display_name = user.name || user.nickname
|
display_name = user.name || user.nickname
|
||||||
|
|
||||||
image = User.avatar_url(user) |> MediaProxy.url()
|
avatar = User.avatar_url(user) |> MediaProxy.url()
|
||||||
|
avatar_static = User.avatar_url(user) |> MediaProxy.preview_url(static: true)
|
||||||
header = User.banner_url(user) |> MediaProxy.url()
|
header = User.banner_url(user) |> MediaProxy.url()
|
||||||
|
header_static = User.banner_url(user) |> MediaProxy.preview_url(static: true)
|
||||||
|
|
||||||
following_count =
|
following_count =
|
||||||
if !user.hide_follows_count or !user.hide_follows or opts[:for] == user do
|
if !user.hide_follows_count or !user.hide_follows or opts[:for] == user do
|
||||||
|
@ -247,10 +249,10 @@ defp do_render("show.json", %{user: user} = opts) do
|
||||||
statuses_count: user.note_count,
|
statuses_count: user.note_count,
|
||||||
note: user.bio,
|
note: user.bio,
|
||||||
url: user.uri || user.ap_id,
|
url: user.uri || user.ap_id,
|
||||||
avatar: image,
|
avatar: avatar,
|
||||||
avatar_static: image,
|
avatar_static: avatar_static,
|
||||||
header: header,
|
header: header,
|
||||||
header_static: header,
|
header_static: header_static,
|
||||||
emojis: emojis,
|
emojis: emojis,
|
||||||
fields: user.fields,
|
fields: user.fields,
|
||||||
bot: bot,
|
bot: bot,
|
||||||
|
|
|
@ -55,23 +55,6 @@ defp get_replied_to_activities(activities) do
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_user(ap_id, fake_record_fallback \\ true) do
|
|
||||||
cond do
|
|
||||||
user = User.get_cached_by_ap_id(ap_id) ->
|
|
||||||
user
|
|
||||||
|
|
||||||
user = User.get_by_guessed_nickname(ap_id) ->
|
|
||||||
user
|
|
||||||
|
|
||||||
fake_record_fallback ->
|
|
||||||
# TODO: refactor (fake records is never a good idea)
|
|
||||||
User.error_user(ap_id)
|
|
||||||
|
|
||||||
true ->
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp get_context_id(%{data: %{"context_id" => context_id}}) when not is_nil(context_id),
|
defp get_context_id(%{data: %{"context_id" => context_id}}) when not is_nil(context_id),
|
||||||
do: context_id
|
do: context_id
|
||||||
|
|
||||||
|
@ -119,7 +102,7 @@ def render("index.json", opts) do
|
||||||
# Note: unresolved users are filtered out
|
# Note: unresolved users are filtered out
|
||||||
actors =
|
actors =
|
||||||
(activities ++ parent_activities)
|
(activities ++ parent_activities)
|
||||||
|> Enum.map(&get_user(&1.data["actor"], false))
|
|> Enum.map(&CommonAPI.get_user(&1.data["actor"], false))
|
||||||
|> Enum.filter(& &1)
|
|> Enum.filter(& &1)
|
||||||
|
|
||||||
UserRelationship.view_relationships_option(reading_user, actors, subset: :source_mutes)
|
UserRelationship.view_relationships_option(reading_user, actors, subset: :source_mutes)
|
||||||
|
@ -138,7 +121,7 @@ def render(
|
||||||
"show.json",
|
"show.json",
|
||||||
%{activity: %{data: %{"type" => "Announce", "object" => _object}} = activity} = opts
|
%{activity: %{data: %{"type" => "Announce", "object" => _object}} = activity} = opts
|
||||||
) do
|
) do
|
||||||
user = get_user(activity.data["actor"])
|
user = CommonAPI.get_user(activity.data["actor"])
|
||||||
created_at = Utils.to_masto_date(activity.data["published"])
|
created_at = Utils.to_masto_date(activity.data["published"])
|
||||||
activity_object = Object.normalize(activity)
|
activity_object = Object.normalize(activity)
|
||||||
|
|
||||||
|
@ -211,7 +194,7 @@ def render(
|
||||||
def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
|
def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
|
||||||
object = Object.normalize(activity)
|
object = Object.normalize(activity)
|
||||||
|
|
||||||
user = get_user(activity.data["actor"])
|
user = CommonAPI.get_user(activity.data["actor"])
|
||||||
user_follower_address = user.follower_address
|
user_follower_address = user.follower_address
|
||||||
|
|
||||||
like_count = object.data["like_count"] || 0
|
like_count = object.data["like_count"] || 0
|
||||||
|
@ -265,7 +248,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
|
||||||
|
|
||||||
reply_to = get_reply_to(activity, opts)
|
reply_to = get_reply_to(activity, opts)
|
||||||
|
|
||||||
reply_to_user = reply_to && get_user(reply_to.data["actor"])
|
reply_to_user = reply_to && CommonAPI.get_user(reply_to.data["actor"])
|
||||||
|
|
||||||
content =
|
content =
|
||||||
object
|
object
|
||||||
|
@ -432,6 +415,7 @@ def render("attachment.json", %{attachment: attachment}) do
|
||||||
[attachment_url | _] = attachment["url"]
|
[attachment_url | _] = attachment["url"]
|
||||||
media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
|
media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
|
||||||
href = attachment_url["href"] |> MediaProxy.url()
|
href = attachment_url["href"] |> MediaProxy.url()
|
||||||
|
href_preview = attachment_url["href"] |> MediaProxy.preview_url()
|
||||||
|
|
||||||
type =
|
type =
|
||||||
cond do
|
cond do
|
||||||
|
@ -447,7 +431,7 @@ def render("attachment.json", %{attachment: attachment}) do
|
||||||
id: to_string(attachment["id"] || hash_id),
|
id: to_string(attachment["id"] || hash_id),
|
||||||
url: href,
|
url: href,
|
||||||
remote_url: href,
|
remote_url: href,
|
||||||
preview_url: href,
|
preview_url: href_preview,
|
||||||
text_url: href,
|
text_url: href,
|
||||||
type: type,
|
type: type,
|
||||||
description: attachment["name"],
|
description: attachment["name"],
|
||||||
|
|
|
@ -33,6 +33,8 @@ defp do_purge(urls) do
|
||||||
def prepare_urls(urls) do
|
def prepare_urls(urls) do
|
||||||
urls
|
urls
|
||||||
|> List.wrap()
|
|> List.wrap()
|
||||||
|> Enum.map(&MediaProxy.url/1)
|
|> Enum.map(fn url -> [MediaProxy.url(url), MediaProxy.preview_url(url)] end)
|
||||||
|
|> List.flatten()
|
||||||
|
|> Enum.uniq()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
defmodule Pleroma.Web.MediaProxy do
|
defmodule Pleroma.Web.MediaProxy do
|
||||||
alias Pleroma.Config
|
alias Pleroma.Config
|
||||||
|
alias Pleroma.Helpers.UriHelper
|
||||||
alias Pleroma.Upload
|
alias Pleroma.Upload
|
||||||
alias Pleroma.Web
|
alias Pleroma.Web
|
||||||
alias Pleroma.Web.MediaProxy.Invalidation
|
alias Pleroma.Web.MediaProxy.Invalidation
|
||||||
|
@ -40,27 +41,35 @@ def url(url) when is_nil(url) or url == "", do: nil
|
||||||
def url("/" <> _ = url), do: url
|
def url("/" <> _ = url), do: url
|
||||||
|
|
||||||
def url(url) do
|
def url(url) do
|
||||||
if disabled?() or not url_proxiable?(url) do
|
if enabled?() and url_proxiable?(url) do
|
||||||
url
|
|
||||||
else
|
|
||||||
encode_url(url)
|
encode_url(url)
|
||||||
|
else
|
||||||
|
url
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec url_proxiable?(String.t()) :: boolean()
|
@spec url_proxiable?(String.t()) :: boolean()
|
||||||
def url_proxiable?(url) do
|
def url_proxiable?(url) do
|
||||||
if local?(url) or whitelisted?(url) do
|
not local?(url) and not whitelisted?(url)
|
||||||
false
|
end
|
||||||
|
|
||||||
|
def preview_url(url, preview_params \\ []) do
|
||||||
|
if preview_enabled?() do
|
||||||
|
encode_preview_url(url, preview_params)
|
||||||
else
|
else
|
||||||
true
|
url(url)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp disabled?, do: !Config.get([:media_proxy, :enabled], false)
|
def enabled?, do: Config.get([:media_proxy, :enabled], false)
|
||||||
|
|
||||||
defp local?(url), do: String.starts_with?(url, Pleroma.Web.base_url())
|
# Note: media proxy must be enabled for media preview proxy in order to load all
|
||||||
|
# non-local non-whitelisted URLs through it and be sure that body size constraint is preserved.
|
||||||
|
def preview_enabled?, do: enabled?() and !!Config.get([:media_preview_proxy, :enabled])
|
||||||
|
|
||||||
defp whitelisted?(url) do
|
def local?(url), do: String.starts_with?(url, Pleroma.Web.base_url())
|
||||||
|
|
||||||
|
def whitelisted?(url) do
|
||||||
%{host: domain} = URI.parse(url)
|
%{host: domain} = URI.parse(url)
|
||||||
|
|
||||||
mediaproxy_whitelist_domains =
|
mediaproxy_whitelist_domains =
|
||||||
|
@ -85,17 +94,29 @@ defp maybe_get_domain_from_url("http" <> _ = url) do
|
||||||
|
|
||||||
defp maybe_get_domain_from_url(domain), do: domain
|
defp maybe_get_domain_from_url(domain), do: domain
|
||||||
|
|
||||||
def encode_url(url) do
|
defp base64_sig64(url) do
|
||||||
base64 = Base.url_encode64(url, @base64_opts)
|
base64 = Base.url_encode64(url, @base64_opts)
|
||||||
|
|
||||||
sig64 =
|
sig64 =
|
||||||
base64
|
base64
|
||||||
|> signed_url
|
|> signed_url()
|
||||||
|> Base.url_encode64(@base64_opts)
|
|> Base.url_encode64(@base64_opts)
|
||||||
|
|
||||||
|
{base64, sig64}
|
||||||
|
end
|
||||||
|
|
||||||
|
def encode_url(url) do
|
||||||
|
{base64, sig64} = base64_sig64(url)
|
||||||
|
|
||||||
build_url(sig64, base64, filename(url))
|
build_url(sig64, base64, filename(url))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def encode_preview_url(url, preview_params \\ []) do
|
||||||
|
{base64, sig64} = base64_sig64(url)
|
||||||
|
|
||||||
|
build_preview_url(sig64, base64, filename(url), preview_params)
|
||||||
|
end
|
||||||
|
|
||||||
def decode_url(sig, url) do
|
def decode_url(sig, url) do
|
||||||
with {:ok, sig} <- Base.url_decode64(sig, @base64_opts),
|
with {:ok, sig} <- Base.url_decode64(sig, @base64_opts),
|
||||||
signature when signature == sig <- signed_url(url) do
|
signature when signature == sig <- signed_url(url) do
|
||||||
|
@ -113,10 +134,14 @@ def filename(url_or_path) do
|
||||||
if path = URI.parse(url_or_path).path, do: Path.basename(path)
|
if path = URI.parse(url_or_path).path, do: Path.basename(path)
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_url(sig_base64, url_base64, filename \\ nil) do
|
def base_url do
|
||||||
|
Config.get([:media_proxy, :base_url], Web.base_url())
|
||||||
|
end
|
||||||
|
|
||||||
|
defp proxy_url(path, sig_base64, url_base64, filename) do
|
||||||
[
|
[
|
||||||
Config.get([:media_proxy, :base_url], Web.base_url()),
|
base_url(),
|
||||||
"proxy",
|
path,
|
||||||
sig_base64,
|
sig_base64,
|
||||||
url_base64,
|
url_base64,
|
||||||
filename
|
filename
|
||||||
|
@ -124,4 +149,38 @@ def build_url(sig_base64, url_base64, filename \\ nil) do
|
||||||
|> Enum.filter(& &1)
|
|> Enum.filter(& &1)
|
||||||
|> Path.join()
|
|> Path.join()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def build_url(sig_base64, url_base64, filename \\ nil) do
|
||||||
|
proxy_url("proxy", sig_base64, url_base64, filename)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_preview_url(sig_base64, url_base64, filename \\ nil, preview_params \\ []) do
|
||||||
|
uri = proxy_url("proxy/preview", sig_base64, url_base64, filename)
|
||||||
|
|
||||||
|
UriHelper.modify_uri_params(uri, preview_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def verify_request_path_and_url(
|
||||||
|
%Plug.Conn{params: %{"filename" => _}, request_path: request_path},
|
||||||
|
url
|
||||||
|
) do
|
||||||
|
verify_request_path_and_url(request_path, url)
|
||||||
|
end
|
||||||
|
|
||||||
|
def verify_request_path_and_url(request_path, url) when is_binary(request_path) do
|
||||||
|
filename = filename(url)
|
||||||
|
|
||||||
|
if filename && not basename_matches?(request_path, filename) do
|
||||||
|
{:wrong_filename, filename}
|
||||||
|
else
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def verify_request_path_and_url(_, _), do: :ok
|
||||||
|
|
||||||
|
defp basename_matches?(path, filename) do
|
||||||
|
basename = Path.basename(path)
|
||||||
|
basename == filename or URI.decode(basename) == filename or URI.encode(basename) == filename
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,44 +5,201 @@
|
||||||
defmodule Pleroma.Web.MediaProxy.MediaProxyController do
|
defmodule Pleroma.Web.MediaProxy.MediaProxyController do
|
||||||
use Pleroma.Web, :controller
|
use Pleroma.Web, :controller
|
||||||
|
|
||||||
|
alias Pleroma.Config
|
||||||
|
alias Pleroma.Helpers.MediaHelper
|
||||||
|
alias Pleroma.Helpers.UriHelper
|
||||||
alias Pleroma.ReverseProxy
|
alias Pleroma.ReverseProxy
|
||||||
alias Pleroma.Web.MediaProxy
|
alias Pleroma.Web.MediaProxy
|
||||||
|
alias Plug.Conn
|
||||||
|
|
||||||
@default_proxy_opts [max_body_length: 25 * 1_048_576, http: [follow_redirect: true]]
|
def remote(conn, %{"sig" => sig64, "url" => url64}) do
|
||||||
|
with {_, true} <- {:enabled, MediaProxy.enabled?()},
|
||||||
def remote(conn, %{"sig" => sig64, "url" => url64} = params) do
|
|
||||||
with config <- Pleroma.Config.get([:media_proxy], []),
|
|
||||||
true <- Keyword.get(config, :enabled, false),
|
|
||||||
{:ok, url} <- MediaProxy.decode_url(sig64, url64),
|
{:ok, url} <- MediaProxy.decode_url(sig64, url64),
|
||||||
{_, false} <- {:in_banned_urls, MediaProxy.in_banned_urls(url)},
|
{_, false} <- {:in_banned_urls, MediaProxy.in_banned_urls(url)},
|
||||||
:ok <- filename_matches(params, conn.request_path, url) do
|
:ok <- MediaProxy.verify_request_path_and_url(conn, url) do
|
||||||
ReverseProxy.call(conn, url, Keyword.get(config, :proxy_opts, @default_proxy_opts))
|
ReverseProxy.call(conn, url, media_proxy_opts())
|
||||||
else
|
else
|
||||||
error when error in [false, {:in_banned_urls, true}] ->
|
{:enabled, false} ->
|
||||||
send_resp(conn, 404, Plug.Conn.Status.reason_phrase(404))
|
send_resp(conn, 404, Conn.Status.reason_phrase(404))
|
||||||
|
|
||||||
|
{:in_banned_urls, true} ->
|
||||||
|
send_resp(conn, 404, Conn.Status.reason_phrase(404))
|
||||||
|
|
||||||
{:error, :invalid_signature} ->
|
{:error, :invalid_signature} ->
|
||||||
send_resp(conn, 403, Plug.Conn.Status.reason_phrase(403))
|
send_resp(conn, 403, Conn.Status.reason_phrase(403))
|
||||||
|
|
||||||
{:wrong_filename, filename} ->
|
{:wrong_filename, filename} ->
|
||||||
redirect(conn, external: MediaProxy.build_url(sig64, url64, filename))
|
redirect(conn, external: MediaProxy.build_url(sig64, url64, filename))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def filename_matches(%{"filename" => _} = _, path, url) do
|
def preview(%Conn{} = conn, %{"sig" => sig64, "url" => url64}) do
|
||||||
filename = MediaProxy.filename(url)
|
with {_, true} <- {:enabled, MediaProxy.preview_enabled?()},
|
||||||
|
{:ok, url} <- MediaProxy.decode_url(sig64, url64),
|
||||||
if filename && does_not_match(path, filename) do
|
:ok <- MediaProxy.verify_request_path_and_url(conn, url) do
|
||||||
{:wrong_filename, filename}
|
handle_preview(conn, url)
|
||||||
else
|
else
|
||||||
:ok
|
{:enabled, false} ->
|
||||||
|
send_resp(conn, 404, Conn.Status.reason_phrase(404))
|
||||||
|
|
||||||
|
{:error, :invalid_signature} ->
|
||||||
|
send_resp(conn, 403, Conn.Status.reason_phrase(403))
|
||||||
|
|
||||||
|
{:wrong_filename, filename} ->
|
||||||
|
redirect(conn, external: MediaProxy.build_preview_url(sig64, url64, filename))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def filename_matches(_, _, _), do: :ok
|
defp handle_preview(conn, url) do
|
||||||
|
media_proxy_url = MediaProxy.url(url)
|
||||||
|
|
||||||
defp does_not_match(path, filename) do
|
with {:ok, %{status: status} = head_response} when status in 200..299 <-
|
||||||
basename = Path.basename(path)
|
Pleroma.HTTP.request("head", media_proxy_url, [], [], pool: :media) do
|
||||||
basename != filename and URI.decode(basename) != filename and URI.encode(basename) != filename
|
content_type = Tesla.get_header(head_response, "content-type")
|
||||||
|
content_length = Tesla.get_header(head_response, "content-length")
|
||||||
|
content_length = content_length && String.to_integer(content_length)
|
||||||
|
static = conn.params["static"] in ["true", true]
|
||||||
|
|
||||||
|
cond do
|
||||||
|
static and content_type == "image/gif" ->
|
||||||
|
handle_jpeg_preview(conn, media_proxy_url)
|
||||||
|
|
||||||
|
static ->
|
||||||
|
drop_static_param_and_redirect(conn)
|
||||||
|
|
||||||
|
content_type == "image/gif" ->
|
||||||
|
redirect(conn, external: media_proxy_url)
|
||||||
|
|
||||||
|
min_content_length_for_preview() > 0 and content_length > 0 and
|
||||||
|
content_length < min_content_length_for_preview() ->
|
||||||
|
redirect(conn, external: media_proxy_url)
|
||||||
|
|
||||||
|
true ->
|
||||||
|
handle_preview(content_type, conn, media_proxy_url)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# If HEAD failed, redirecting to media proxy URI doesn't make much sense; returning an error
|
||||||
|
{_, %{status: status}} ->
|
||||||
|
send_resp(conn, :failed_dependency, "Can't fetch HTTP headers (HTTP #{status}).")
|
||||||
|
|
||||||
|
{:error, :recv_response_timeout} ->
|
||||||
|
send_resp(conn, :failed_dependency, "HEAD request timeout.")
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
send_resp(conn, :failed_dependency, "Can't fetch HTTP headers.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_preview("image/png" <> _ = _content_type, conn, media_proxy_url) do
|
||||||
|
handle_png_preview(conn, media_proxy_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_preview("image/" <> _ = _content_type, conn, media_proxy_url) do
|
||||||
|
handle_jpeg_preview(conn, media_proxy_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_preview("video/" <> _ = _content_type, conn, media_proxy_url) do
|
||||||
|
handle_video_preview(conn, media_proxy_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_preview(_unsupported_content_type, conn, media_proxy_url) do
|
||||||
|
fallback_on_preview_error(conn, media_proxy_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_png_preview(conn, media_proxy_url) do
|
||||||
|
quality = Config.get!([:media_preview_proxy, :image_quality])
|
||||||
|
{thumbnail_max_width, thumbnail_max_height} = thumbnail_max_dimensions()
|
||||||
|
|
||||||
|
with {:ok, thumbnail_binary} <-
|
||||||
|
MediaHelper.image_resize(
|
||||||
|
media_proxy_url,
|
||||||
|
%{
|
||||||
|
max_width: thumbnail_max_width,
|
||||||
|
max_height: thumbnail_max_height,
|
||||||
|
quality: quality,
|
||||||
|
format: "png"
|
||||||
|
}
|
||||||
|
) do
|
||||||
|
conn
|
||||||
|
|> put_preview_response_headers(["image/png", "preview.png"])
|
||||||
|
|> send_resp(200, thumbnail_binary)
|
||||||
|
else
|
||||||
|
_ ->
|
||||||
|
fallback_on_preview_error(conn, media_proxy_url)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_jpeg_preview(conn, media_proxy_url) do
|
||||||
|
quality = Config.get!([:media_preview_proxy, :image_quality])
|
||||||
|
{thumbnail_max_width, thumbnail_max_height} = thumbnail_max_dimensions()
|
||||||
|
|
||||||
|
with {:ok, thumbnail_binary} <-
|
||||||
|
MediaHelper.image_resize(
|
||||||
|
media_proxy_url,
|
||||||
|
%{max_width: thumbnail_max_width, max_height: thumbnail_max_height, quality: quality}
|
||||||
|
) do
|
||||||
|
conn
|
||||||
|
|> put_preview_response_headers()
|
||||||
|
|> send_resp(200, thumbnail_binary)
|
||||||
|
else
|
||||||
|
_ ->
|
||||||
|
fallback_on_preview_error(conn, media_proxy_url)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_video_preview(conn, media_proxy_url) do
|
||||||
|
with {:ok, thumbnail_binary} <-
|
||||||
|
MediaHelper.video_framegrab(media_proxy_url) do
|
||||||
|
conn
|
||||||
|
|> put_preview_response_headers()
|
||||||
|
|> send_resp(200, thumbnail_binary)
|
||||||
|
else
|
||||||
|
_ ->
|
||||||
|
fallback_on_preview_error(conn, media_proxy_url)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp drop_static_param_and_redirect(conn) do
|
||||||
|
uri_without_static_param =
|
||||||
|
conn
|
||||||
|
|> current_url()
|
||||||
|
|> UriHelper.modify_uri_params(%{}, ["static"])
|
||||||
|
|
||||||
|
redirect(conn, external: uri_without_static_param)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fallback_on_preview_error(conn, media_proxy_url) do
|
||||||
|
redirect(conn, external: media_proxy_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp put_preview_response_headers(
|
||||||
|
conn,
|
||||||
|
[content_type, filename] = _content_info \\ ["image/jpeg", "preview.jpg"]
|
||||||
|
) do
|
||||||
|
conn
|
||||||
|
|> put_resp_header("content-type", content_type)
|
||||||
|
|> put_resp_header("content-disposition", "inline; filename=\"#{filename}\"")
|
||||||
|
|> put_resp_header("cache-control", ReverseProxy.default_cache_control_header())
|
||||||
|
end
|
||||||
|
|
||||||
|
defp thumbnail_max_dimensions do
|
||||||
|
config = media_preview_proxy_config()
|
||||||
|
|
||||||
|
thumbnail_max_width = Keyword.fetch!(config, :thumbnail_max_width)
|
||||||
|
thumbnail_max_height = Keyword.fetch!(config, :thumbnail_max_height)
|
||||||
|
|
||||||
|
{thumbnail_max_width, thumbnail_max_height}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp min_content_length_for_preview do
|
||||||
|
Keyword.get(media_preview_proxy_config(), :min_content_length, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp media_preview_proxy_config do
|
||||||
|
Config.get!([:media_preview_proxy])
|
||||||
|
end
|
||||||
|
|
||||||
|
defp media_proxy_opts do
|
||||||
|
Config.get([:media_proxy, :proxy_opts], [])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,7 +10,9 @@ defmodule Pleroma.Web.Metadata.Providers.RestrictIndexing do
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def build_tags(%{user: %{local: false}}) do
|
def build_tags(%{user: %{local: true, discoverable: true}}), do: []
|
||||||
|
|
||||||
|
def build_tags(_) do
|
||||||
[
|
[
|
||||||
{:meta,
|
{:meta,
|
||||||
[
|
[
|
||||||
|
@ -19,7 +21,4 @@ def build_tags(%{user: %{local: false}}) do
|
||||||
], []}
|
], []}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
|
||||||
def build_tags(%{user: %{local: true}}), do: []
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -38,7 +38,7 @@ def scrub_html(content) when is_binary(content) do
|
||||||
def scrub_html(content), do: content
|
def scrub_html(content), do: content
|
||||||
|
|
||||||
def attachment_url(url) do
|
def attachment_url(url) do
|
||||||
MediaProxy.url(url)
|
MediaProxy.preview_url(url)
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_name_string(user) do
|
def user_name_string(user) do
|
||||||
|
|
|
@ -119,7 +119,7 @@ defp handle_existing_authorization(
|
||||||
redirect_uri = redirect_uri(conn, redirect_uri)
|
redirect_uri = redirect_uri(conn, redirect_uri)
|
||||||
url_params = %{access_token: token.token}
|
url_params = %{access_token: token.token}
|
||||||
url_params = Maps.put_if_present(url_params, :state, params["state"])
|
url_params = Maps.put_if_present(url_params, :state, params["state"])
|
||||||
url = UriHelper.append_uri_params(redirect_uri, url_params)
|
url = UriHelper.modify_uri_params(redirect_uri, url_params)
|
||||||
redirect(conn, external: url)
|
redirect(conn, external: url)
|
||||||
else
|
else
|
||||||
conn
|
conn
|
||||||
|
@ -161,7 +161,7 @@ def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
|
||||||
redirect_uri = redirect_uri(conn, redirect_uri)
|
redirect_uri = redirect_uri(conn, redirect_uri)
|
||||||
url_params = %{code: auth.token}
|
url_params = %{code: auth.token}
|
||||||
url_params = Maps.put_if_present(url_params, :state, auth_attrs["state"])
|
url_params = Maps.put_if_present(url_params, :state, auth_attrs["state"])
|
||||||
url = UriHelper.append_uri_params(redirect_uri, url_params)
|
url = UriHelper.modify_uri_params(redirect_uri, url_params)
|
||||||
redirect(conn, external: url)
|
redirect(conn, external: url)
|
||||||
else
|
else
|
||||||
conn
|
conn
|
||||||
|
|
|
@ -90,6 +90,16 @@ def post_chat_message(
|
||||||
conn
|
conn
|
||||||
|> put_view(MessageReferenceView)
|
|> put_view(MessageReferenceView)
|
||||||
|> render("show.json", chat_message_reference: cm_ref)
|
|> render("show.json", chat_message_reference: cm_ref)
|
||||||
|
else
|
||||||
|
{:reject, message} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:unprocessable_entity)
|
||||||
|
|> json(%{error: message})
|
||||||
|
|
||||||
|
{:error, message} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:bad_request)
|
||||||
|
|> json(%{error: message})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -146,11 +156,8 @@ def index(%{assigns: %{user: %{id: user_id} = user}} = conn, _params) do
|
||||||
blocked_ap_ids = User.blocked_users_ap_ids(user)
|
blocked_ap_ids = User.blocked_users_ap_ids(user)
|
||||||
|
|
||||||
chats =
|
chats =
|
||||||
from(c in Chat,
|
Chat.for_user_query(user_id)
|
||||||
where: c.user_id == ^user_id,
|
|> where([c], c.recipient not in ^blocked_ap_ids)
|
||||||
where: c.recipient not in ^blocked_ap_ids,
|
|
||||||
order_by: [desc: c.updated_at]
|
|
||||||
)
|
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|
|
|
@ -10,14 +10,14 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleView do
|
||||||
alias Pleroma.Activity
|
alias Pleroma.Activity
|
||||||
alias Pleroma.HTML
|
alias Pleroma.HTML
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
|
alias Pleroma.Web.CommonAPI
|
||||||
alias Pleroma.Web.CommonAPI.Utils
|
alias Pleroma.Web.CommonAPI.Utils
|
||||||
alias Pleroma.Web.MastodonAPI.AccountView
|
alias Pleroma.Web.MastodonAPI.AccountView
|
||||||
alias Pleroma.Web.MastodonAPI.StatusView
|
|
||||||
|
|
||||||
def render("show.json", %{activity: %Activity{data: %{"type" => "Listen"}} = activity} = opts) do
|
def render("show.json", %{activity: %Activity{data: %{"type" => "Listen"}} = activity} = opts) do
|
||||||
object = Object.normalize(activity)
|
object = Object.normalize(activity)
|
||||||
|
|
||||||
user = StatusView.get_user(activity.data["actor"])
|
user = CommonAPI.get_user(activity.data["actor"])
|
||||||
created_at = Utils.to_masto_date(activity.data["published"])
|
created_at = Utils.to_masto_date(activity.data["published"])
|
||||||
|
|
||||||
%{
|
%{
|
||||||
|
|
|
@ -20,36 +20,61 @@ def parse(url) do
|
||||||
with {:ok, data} <- get_cached_or_parse(url),
|
with {:ok, data} <- get_cached_or_parse(url),
|
||||||
{:ok, _} <- set_ttl_based_on_image(data, url) do
|
{:ok, _} <- set_ttl_based_on_image(data, url) do
|
||||||
{:ok, data}
|
{:ok, data}
|
||||||
else
|
|
||||||
{:error, {:invalid_metadata, data}} = e ->
|
|
||||||
Logger.debug(fn -> "Incomplete or invalid metadata for #{url}: #{inspect(data)}" end)
|
|
||||||
e
|
|
||||||
|
|
||||||
error ->
|
|
||||||
Logger.error(fn -> "Rich media error for #{url}: #{inspect(error)}" end)
|
|
||||||
error
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_cached_or_parse(url) do
|
defp get_cached_or_parse(url) do
|
||||||
case Cachex.fetch!(:rich_media_cache, url, fn _ -> {:commit, parse_url(url)} end) do
|
case Cachex.fetch(:rich_media_cache, url, fn ->
|
||||||
{:ok, _data} = res ->
|
case parse_url(url) do
|
||||||
res
|
{:ok, _} = res ->
|
||||||
|
{:commit, res}
|
||||||
|
|
||||||
{:error, :body_too_large} = e ->
|
{:error, reason} = e ->
|
||||||
e
|
# Unfortunately we have to log errors here, instead of doing that
|
||||||
|
# along with ttl setting at the bottom. Otherwise we can get log spam
|
||||||
|
# if more than one process was waiting for the rich media card
|
||||||
|
# while it was generated. Ideally we would set ttl here as well,
|
||||||
|
# so we don't override it number_of_waiters_on_generation
|
||||||
|
# times, but one, obviously, can't set ttl for not-yet-created entry
|
||||||
|
# and Cachex doesn't support returning ttl from the fetch callback.
|
||||||
|
log_error(url, reason)
|
||||||
|
{:commit, e}
|
||||||
|
end
|
||||||
|
end) do
|
||||||
|
{action, res} when action in [:commit, :ok] ->
|
||||||
|
case res do
|
||||||
|
{:ok, _data} = res ->
|
||||||
|
res
|
||||||
|
|
||||||
{:error, {:content_type, _}} = e ->
|
{:error, reason} = e ->
|
||||||
e
|
if action == :commit, do: set_error_ttl(url, reason)
|
||||||
|
e
|
||||||
|
end
|
||||||
|
|
||||||
# The TTL is not set for the errors above, since they are unlikely to change
|
{:error, e} ->
|
||||||
# with time
|
{:error, {:cachex_error, e}}
|
||||||
{:error, _} = e ->
|
|
||||||
ttl = Pleroma.Config.get([:rich_media, :failure_backoff], 60_000)
|
|
||||||
Cachex.expire(:rich_media_cache, url, ttl)
|
|
||||||
e
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp set_error_ttl(_url, :body_too_large), do: :ok
|
||||||
|
defp set_error_ttl(_url, {:content_type, _}), do: :ok
|
||||||
|
|
||||||
|
# The TTL is not set for the errors above, since they are unlikely to change
|
||||||
|
# with time
|
||||||
|
|
||||||
|
defp set_error_ttl(url, _reason) do
|
||||||
|
ttl = Pleroma.Config.get([:rich_media, :failure_backoff], 60_000)
|
||||||
|
Cachex.expire(:rich_media_cache, url, ttl)
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
defp log_error(url, {:invalid_metadata, data}) do
|
||||||
|
Logger.debug(fn -> "Incomplete or invalid metadata for #{url}: #{inspect(data)}" end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp log_error(url, reason) do
|
||||||
|
Logger.warn(fn -> "Rich media error for #{url}: #{inspect(reason)}" end)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|
|
@ -178,9 +178,14 @@ defmodule Pleroma.Web.Router do
|
||||||
get("/users", AdminAPIController, :list_users)
|
get("/users", AdminAPIController, :list_users)
|
||||||
get("/users/:nickname", AdminAPIController, :user_show)
|
get("/users/:nickname", AdminAPIController, :user_show)
|
||||||
get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses)
|
get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses)
|
||||||
|
get("/users/:nickname/chats", AdminAPIController, :list_user_chats)
|
||||||
|
|
||||||
get("/instances/:instance/statuses", AdminAPIController, :list_instance_statuses)
|
get("/instances/:instance/statuses", AdminAPIController, :list_instance_statuses)
|
||||||
|
|
||||||
|
get("/instance_document/:name", InstanceDocumentController, :show)
|
||||||
|
patch("/instance_document/:name", InstanceDocumentController, :update)
|
||||||
|
delete("/instance_document/:name", InstanceDocumentController, :delete)
|
||||||
|
|
||||||
patch("/users/confirm_email", AdminAPIController, :confirm_email)
|
patch("/users/confirm_email", AdminAPIController, :confirm_email)
|
||||||
patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email)
|
patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email)
|
||||||
|
|
||||||
|
@ -214,6 +219,10 @@ defmodule Pleroma.Web.Router do
|
||||||
get("/media_proxy_caches", MediaProxyCacheController, :index)
|
get("/media_proxy_caches", MediaProxyCacheController, :index)
|
||||||
post("/media_proxy_caches/delete", MediaProxyCacheController, :delete)
|
post("/media_proxy_caches/delete", MediaProxyCacheController, :delete)
|
||||||
post("/media_proxy_caches/purge", MediaProxyCacheController, :purge)
|
post("/media_proxy_caches/purge", MediaProxyCacheController, :purge)
|
||||||
|
|
||||||
|
get("/chats/:id", ChatController, :show)
|
||||||
|
get("/chats/:id/messages", ChatController, :messages)
|
||||||
|
delete("/chats/:id/messages/:message_id", ChatController, :delete_message)
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do
|
scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do
|
||||||
|
@ -670,6 +679,8 @@ defmodule Pleroma.Web.Router do
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/proxy/", Pleroma.Web.MediaProxy do
|
scope "/proxy/", Pleroma.Web.MediaProxy do
|
||||||
|
get("/preview/:sig/:url", MediaProxyController, :preview)
|
||||||
|
get("/preview/:sig/:url/:filename", MediaProxyController, :preview)
|
||||||
get("/:sig/:url", MediaProxyController, :remote)
|
get("/:sig/:url", MediaProxyController, :remote)
|
||||||
get("/:sig/:url/:filename", MediaProxyController, :remote)
|
get("/:sig/:url/:filename", MediaProxyController, :remote)
|
||||||
end
|
end
|
||||||
|
|
4
mix.lock
4
mix.lock
|
@ -31,6 +31,7 @@
|
||||||
"ecto": {:hex, :ecto, "3.4.5", "2bcd262f57b2c888b0bd7f7a28c8a48aa11dc1a2c6a858e45dd8f8426d504265", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8c6d1d4d524559e9b7a062f0498e2c206122552d63eacff0a6567ffe7a8e8691"},
|
"ecto": {:hex, :ecto, "3.4.5", "2bcd262f57b2c888b0bd7f7a28c8a48aa11dc1a2c6a858e45dd8f8426d504265", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8c6d1d4d524559e9b7a062f0498e2c206122552d63eacff0a6567ffe7a8e8691"},
|
||||||
"ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"},
|
"ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"},
|
||||||
"ecto_sql": {:hex, :ecto_sql, "3.4.5", "30161f81b167d561a9a2df4329c10ae05ff36eca7ccc84628f2c8b9fa1e43323", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "31990c6a3579b36a3c0841d34a94c275e727de8b84f58509da5f1b2032c98ac2"},
|
"ecto_sql": {:hex, :ecto_sql, "3.4.5", "30161f81b167d561a9a2df4329c10ae05ff36eca7ccc84628f2c8b9fa1e43323", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "31990c6a3579b36a3c0841d34a94c275e727de8b84f58509da5f1b2032c98ac2"},
|
||||||
|
"eimp": {:hex, :eimp, "1.0.14", "fc297f0c7e2700457a95a60c7010a5f1dcb768a083b6d53f49cd94ab95a28f22", [:rebar3], [{:p1_utils, "1.0.18", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "501133f3112079b92d9e22da8b88bf4f0e13d4d67ae9c15c42c30bd25ceb83b6"},
|
||||||
"elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"},
|
"elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"},
|
||||||
"esshd": {:hex, :esshd, "0.1.1", "d4dd4c46698093a40a56afecce8a46e246eb35463c457c246dacba2e056f31b5", [:mix], [], "hexpm", "d73e341e3009d390aa36387dc8862860bf9f874c94d9fd92ade2926376f49981"},
|
"esshd": {:hex, :esshd, "0.1.1", "d4dd4c46698093a40a56afecce8a46e246eb35463c457c246dacba2e056f31b5", [:mix], [], "hexpm", "d73e341e3009d390aa36387dc8862860bf9f874c94d9fd92ade2926376f49981"},
|
||||||
"eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm", "b14f1dc204321429479c569cfbe8fb287541184ed040956c8862cb7a677b8406"},
|
"eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm", "b14f1dc204321429479c569cfbe8fb287541184ed040956c8862cb7a677b8406"},
|
||||||
|
@ -80,6 +81,7 @@
|
||||||
"nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]},
|
"nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]},
|
||||||
"oban": {:hex, :oban, "2.0.0", "e6ce70d94dd46815ec0882a1ffb7356df9a9d5b8a40a64ce5c2536617a447379", [:mix], [{:ecto_sql, ">= 3.4.3", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cf574813bd048b98a698aa587c21367d2e06842d4e1b1993dcd6a696e9e633bd"},
|
"oban": {:hex, :oban, "2.0.0", "e6ce70d94dd46815ec0882a1ffb7356df9a9d5b8a40a64ce5c2536617a447379", [:mix], [{:ecto_sql, ">= 3.4.3", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cf574813bd048b98a698aa587c21367d2e06842d4e1b1993dcd6a696e9e633bd"},
|
||||||
"open_api_spex": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/open_api_spex.git", "f296ac0924ba3cf79c7a588c4c252889df4c2edd", [ref: "f296ac0924ba3cf79c7a588c4c252889df4c2edd"]},
|
"open_api_spex": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/open_api_spex.git", "f296ac0924ba3cf79c7a588c4c252889df4c2edd", [ref: "f296ac0924ba3cf79c7a588c4c252889df4c2edd"]},
|
||||||
|
"p1_utils": {:hex, :p1_utils, "1.0.18", "3fe224de5b2e190d730a3c5da9d6e8540c96484cf4b4692921d1e28f0c32b01c", [:rebar3], [], "hexpm", "1fc8773a71a15553b179c986b22fbeead19b28fe486c332d4929700ffeb71f88"},
|
||||||
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
|
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
|
||||||
"pbkdf2_elixir": {:hex, :pbkdf2_elixir, "1.2.1", "9cbe354b58121075bd20eb83076900a3832324b7dd171a6895fab57b6bb2752c", [:mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}], "hexpm", "d3b40a4a4630f0b442f19eca891fcfeeee4c40871936fed2f68e1c4faa30481f"},
|
"pbkdf2_elixir": {:hex, :pbkdf2_elixir, "1.2.1", "9cbe354b58121075bd20eb83076900a3832324b7dd171a6895fab57b6bb2752c", [:mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}], "hexpm", "d3b40a4a4630f0b442f19eca891fcfeeee4c40871936fed2f68e1c4faa30481f"},
|
||||||
"phoenix": {:hex, :phoenix, "1.4.17", "1b1bd4cff7cfc87c94deaa7d60dd8c22e04368ab95499483c50640ef3bd838d8", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a8e5d7a3d76d452bb5fb86e8b7bd115f737e4f8efe202a463d4aeb4a5809611"},
|
"phoenix": {:hex, :phoenix, "1.4.17", "1b1bd4cff7cfc87c94deaa7d60dd8c22e04368ab95499483c50640ef3bd838d8", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a8e5d7a3d76d452bb5fb86e8b7bd115f737e4f8efe202a463d4aeb4a5809611"},
|
||||||
|
@ -118,5 +120,5 @@
|
||||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"},
|
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"},
|
||||||
"unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm", "6c7729a2d214806450d29766abc2afaa7a2cbecf415be64f36a6691afebb50e5"},
|
"unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm", "6c7729a2d214806450d29766abc2afaa7a2cbecf415be64f36a6691afebb50e5"},
|
||||||
"web_push_encryption": {:hex, :web_push_encryption, "0.3.0", "598b5135e696fd1404dc8d0d7c0fa2c027244a4e5d5e5a98ba267f14fdeaabc8", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "f10bdd1afe527ede694749fb77a2f22f146a51b054c7fa541c9fd920fba7c875"},
|
"web_push_encryption": {:hex, :web_push_encryption, "0.3.0", "598b5135e696fd1404dc8d0d7c0fa2c027244a4e5d5e5a98ba267f14fdeaabc8", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "f10bdd1afe527ede694749fb77a2f22f146a51b054c7fa541c9fd920fba7c875"},
|
||||||
"websocket_client": {:git, "https://github.com/jeremyong/websocket_client.git", "9a6f65d05ebf2725d62fb19262b21f1805a59fbf", []},
|
"websocket_client": {:git, "https://github.com/jeremyong/websocket_client.git", "9a6f65d05ebf2725d62fb19262b21f1805a59fbf", []}
|
||||||
}
|
}
|
||||||
|
|
580
priv/gettext/zh_Hans/LC_MESSAGES/errors.po
Normal file
580
priv/gettext/zh_Hans/LC_MESSAGES/errors.po
Normal file
|
@ -0,0 +1,580 @@
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2020-09-20 13:18+0000\n"
|
||||||
|
"PO-Revision-Date: 2020-09-20 14:48+0000\n"
|
||||||
|
"Last-Translator: Kana <gudzpoz@live.com>\n"
|
||||||
|
"Language-Team: Chinese (Simplified) <https://translate.pleroma.social/"
|
||||||
|
"projects/pleroma/pleroma/zh_Hans/>\n"
|
||||||
|
"Language: zh_Hans\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||||
|
"X-Generator: Weblate 4.0.4\n"
|
||||||
|
|
||||||
|
## This file is a PO Template file.
|
||||||
|
##
|
||||||
|
## `msgid`s here are often extracted from source code.
|
||||||
|
## Add new translations manually only if they're dynamic
|
||||||
|
## translations that can't be statically extracted.
|
||||||
|
##
|
||||||
|
## Run `mix gettext.extract` to bring this file up to
|
||||||
|
## date. Leave `msgstr`s empty as changing them here as no
|
||||||
|
## effect: edit them in PO (`.po`) files instead.
|
||||||
|
## From Ecto.Changeset.cast/4
|
||||||
|
msgid "can't be blank"
|
||||||
|
msgstr "不能为空"
|
||||||
|
|
||||||
|
## From Ecto.Changeset.unique_constraint/3
|
||||||
|
msgid "has already been taken"
|
||||||
|
msgstr "已被占用"
|
||||||
|
|
||||||
|
## From Ecto.Changeset.put_change/3
|
||||||
|
msgid "is invalid"
|
||||||
|
msgstr "不合法"
|
||||||
|
|
||||||
|
## From Ecto.Changeset.validate_format/3
|
||||||
|
msgid "has invalid format"
|
||||||
|
msgstr "的格式不合法"
|
||||||
|
|
||||||
|
## From Ecto.Changeset.validate_subset/3
|
||||||
|
msgid "has an invalid entry"
|
||||||
|
msgstr "中存在不合法的项目"
|
||||||
|
|
||||||
|
## From Ecto.Changeset.validate_exclusion/3
|
||||||
|
msgid "is reserved"
|
||||||
|
msgstr "是被保留的"
|
||||||
|
|
||||||
|
## From Ecto.Changeset.validate_confirmation/3
|
||||||
|
msgid "does not match confirmation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
## From Ecto.Changeset.no_assoc_constraint/3
|
||||||
|
msgid "is still associated with this entry"
|
||||||
|
msgstr "仍与该项目相关联"
|
||||||
|
|
||||||
|
msgid "are still associated with this entry"
|
||||||
|
msgstr "仍与该项目相关联"
|
||||||
|
|
||||||
|
## From Ecto.Changeset.validate_length/3
|
||||||
|
msgid "should be %{count} character(s)"
|
||||||
|
msgid_plural "should be %{count} character(s)"
|
||||||
|
msgstr[0] "应为 %{count} 个字符"
|
||||||
|
|
||||||
|
msgid "should have %{count} item(s)"
|
||||||
|
msgid_plural "should have %{count} item(s)"
|
||||||
|
msgstr[0] "应有 %{item} 项"
|
||||||
|
|
||||||
|
msgid "should be at least %{count} character(s)"
|
||||||
|
msgid_plural "should be at least %{count} character(s)"
|
||||||
|
msgstr[0] "应至少有 %{count} 个字符"
|
||||||
|
|
||||||
|
msgid "should have at least %{count} item(s)"
|
||||||
|
msgid_plural "should have at least %{count} item(s)"
|
||||||
|
msgstr[0] "应至少有 %{count} 项"
|
||||||
|
|
||||||
|
msgid "should be at most %{count} character(s)"
|
||||||
|
msgid_plural "should be at most %{count} character(s)"
|
||||||
|
msgstr[0] "应至多有 %{count} 个字符"
|
||||||
|
|
||||||
|
msgid "should have at most %{count} item(s)"
|
||||||
|
msgid_plural "should have at most %{count} item(s)"
|
||||||
|
msgstr[0] "应至多有 %{count} 项"
|
||||||
|
|
||||||
|
## From Ecto.Changeset.validate_number/3
|
||||||
|
msgid "must be less than %{number}"
|
||||||
|
msgstr "必须小于 %{number}"
|
||||||
|
|
||||||
|
msgid "must be greater than %{number}"
|
||||||
|
msgstr "必须大于 %{number}"
|
||||||
|
|
||||||
|
msgid "must be less than or equal to %{number}"
|
||||||
|
msgstr "必须小于等于 %{number}"
|
||||||
|
|
||||||
|
msgid "must be greater than or equal to %{number}"
|
||||||
|
msgstr "必须大于等于 %{number}"
|
||||||
|
|
||||||
|
msgid "must be equal to %{number}"
|
||||||
|
msgstr "必须等于 %{number}"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:505
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Account not found"
|
||||||
|
msgstr "未找到账号"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:339
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Already voted"
|
||||||
|
msgstr "已经进行了投票"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/oauth/oauth_controller.ex:359
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Bad request"
|
||||||
|
msgstr "不正确的请求"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:426
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Can't delete object"
|
||||||
|
msgstr "不能删除对象"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/controller_helper.ex:105
|
||||||
|
#: lib/pleroma/web/controller_helper.ex:111
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Can't display this activity"
|
||||||
|
msgstr "不能显示该活动"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:285
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Can't find user"
|
||||||
|
msgstr "找不到用户"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:61
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Can't get favorites"
|
||||||
|
msgstr "不能获取收藏"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:438
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Can't like object"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/utils.ex:563
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Cannot post an empty status without attachments"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/utils.ex:511
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Comment must be up to %{max_size} characters"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/config/config_db.ex:191
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Config with params %{params} not found"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:181
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:185
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Could not delete"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:231
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Could not favorite"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:453
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Could not pin"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:278
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Could not unfavorite"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:463
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Could not unpin"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:216
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Could not unrepeat"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:512
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:521
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Could not update state"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:207
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Error."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/twitter_api/twitter_api.ex:106
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Invalid CAPTCHA"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:116
|
||||||
|
#: lib/pleroma/web/oauth/oauth_controller.ex:568
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Invalid credentials"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/plugs/ensure_authenticated_plug.ex:38
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Invalid credentials."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:355
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Invalid indices"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:29
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Invalid parameters"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/utils.ex:414
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Invalid password."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:220
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Invalid request"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/twitter_api/twitter_api.ex:109
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Kocaptcha service unavailable"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:112
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Missing parameters"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/utils.ex:547
|
||||||
|
#, elixir-format
|
||||||
|
msgid "No such conversation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:388
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:414 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:456
|
||||||
|
#, elixir-format
|
||||||
|
msgid "No such permission_group"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/plugs/uploaded_media.ex:84
|
||||||
|
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:486 lib/pleroma/web/admin_api/controllers/fallback_controller.ex:11
|
||||||
|
#: lib/pleroma/web/feed/user_controller.ex:71 lib/pleroma/web/ostatus/ostatus_controller.ex:143
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Not found"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:331
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Poll's author can't vote"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:20
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:37 lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:49
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:50 lib/pleroma/web/mastodon_api/controllers/status_controller.ex:306
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:71
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Record not found"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:35
|
||||||
|
#: lib/pleroma/web/feed/user_controller.ex:77 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:36
|
||||||
|
#: lib/pleroma/web/ostatus/ostatus_controller.ex:149
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Something went wrong"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/activity_draft.ex:107
|
||||||
|
#, elixir-format
|
||||||
|
msgid "The message visibility must be direct"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/utils.ex:573
|
||||||
|
#, elixir-format
|
||||||
|
msgid "The status is over the character limit"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex:31
|
||||||
|
#, elixir-format
|
||||||
|
msgid "This resource requires authentication."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/plugs/rate_limiter/rate_limiter.ex:206
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Throttled"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:356
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Too many choices"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:443
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Unhandled activity type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:485
|
||||||
|
#, elixir-format
|
||||||
|
msgid "You can't revoke your own admin status."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/oauth/oauth_controller.ex:221
|
||||||
|
#: lib/pleroma/web/oauth/oauth_controller.ex:308
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Your account is currently disabled"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/oauth/oauth_controller.ex:183
|
||||||
|
#: lib/pleroma/web/oauth/oauth_controller.ex:331
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Your login is missing a confirmed e-mail address"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:390
|
||||||
|
#, elixir-format
|
||||||
|
msgid "can't read inbox of %{nickname} as %{as_nickname}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:473
|
||||||
|
#, elixir-format
|
||||||
|
msgid "can't update outbox of %{nickname} as %{as_nickname}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:471
|
||||||
|
#, elixir-format
|
||||||
|
msgid "conversation is already muted"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:314
|
||||||
|
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:492
|
||||||
|
#, elixir-format
|
||||||
|
msgid "error"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:32
|
||||||
|
#, elixir-format
|
||||||
|
msgid "mascots can only be images"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:62
|
||||||
|
#, elixir-format
|
||||||
|
msgid "not found"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/oauth/oauth_controller.ex:394
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Bad OAuth request."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/twitter_api/twitter_api.ex:115
|
||||||
|
#, elixir-format
|
||||||
|
msgid "CAPTCHA already used"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/twitter_api/twitter_api.ex:112
|
||||||
|
#, elixir-format
|
||||||
|
msgid "CAPTCHA expired"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/plugs/uploaded_media.ex:57
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Failed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/oauth/oauth_controller.ex:410
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Failed to authenticate: %{message}."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/oauth/oauth_controller.ex:441
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Failed to set up user account."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/plugs/oauth_scopes_plug.ex:38
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Insufficient permissions: %{permissions}."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/plugs/uploaded_media.ex:104
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Internal Error"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/oauth/fallback_controller.ex:22
|
||||||
|
#: lib/pleroma/web/oauth/fallback_controller.ex:29
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Invalid Username/Password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/twitter_api/twitter_api.ex:118
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Invalid answer data"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:33
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Nodeinfo schema version not handled"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/oauth/oauth_controller.ex:172
|
||||||
|
#, elixir-format
|
||||||
|
msgid "This action is outside the authorized scopes"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/oauth/fallback_controller.ex:14
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Unknown error, please check the details and try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/oauth/oauth_controller.ex:119
|
||||||
|
#: lib/pleroma/web/oauth/oauth_controller.ex:158
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Unlisted redirect_uri."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/oauth/oauth_controller.ex:390
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Unsupported OAuth provider: %{provider}."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/uploaders/uploader.ex:72
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Uploader callback timeout"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/uploader_controller.ex:23
|
||||||
|
#, elixir-format
|
||||||
|
msgid "bad request"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/twitter_api/twitter_api.ex:103
|
||||||
|
#, elixir-format
|
||||||
|
msgid "CAPTCHA Error"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:290
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Could not add reaction emoji"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:301
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Could not remove reaction emoji"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/twitter_api/twitter_api.ex:129
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Invalid CAPTCHA (Missing parameter: %{name})"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:92
|
||||||
|
#, elixir-format
|
||||||
|
msgid "List not found"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:123
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Missing parameter: %{name}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/oauth/oauth_controller.ex:210
|
||||||
|
#: lib/pleroma/web/oauth/oauth_controller.ex:321
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Password reset is required"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/tests/auth_test_controller.ex:9
|
||||||
|
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:6 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:6
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/config_controller.ex:6 lib/pleroma/web/admin_api/controllers/fallback_controller.ex:6
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/invite_controller.ex:6 lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex:6
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex:6 lib/pleroma/web/admin_api/controllers/relay_controller.ex:6
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/report_controller.ex:6 lib/pleroma/web/admin_api/controllers/status_controller.ex:6
|
||||||
|
#: lib/pleroma/web/controller_helper.ex:6 lib/pleroma/web/embed_controller.ex:6
|
||||||
|
#: lib/pleroma/web/fallback_redirect_controller.ex:6 lib/pleroma/web/feed/tag_controller.ex:6
|
||||||
|
#: lib/pleroma/web/feed/user_controller.ex:6 lib/pleroma/web/mailer/subscription_controller.ex:2
|
||||||
|
#: lib/pleroma/web/masto_fe_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/account_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/app_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/auth_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/filter_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/instance_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/list_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/marker_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex:14
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/media_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/notification_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/report_controller.ex:8
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/search_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/status_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:7
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:6
|
||||||
|
#: lib/pleroma/web/media_proxy/media_proxy_controller.ex:6 lib/pleroma/web/mongooseim/mongoose_im_controller.ex:6
|
||||||
|
#: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:6 lib/pleroma/web/oauth/fallback_controller.ex:6
|
||||||
|
#: lib/pleroma/web/oauth/mfa_controller.ex:10 lib/pleroma/web/oauth/oauth_controller.ex:6
|
||||||
|
#: lib/pleroma/web/ostatus/ostatus_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/account_controller.ex:6
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/chat_controller.ex:5 lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex:6
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:2 lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex:6
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/notification_controller.ex:6
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex:6
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex:7 lib/pleroma/web/static_fe/static_fe_controller.ex:6
|
||||||
|
#: lib/pleroma/web/twitter_api/controllers/password_controller.ex:10 lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex:6
|
||||||
|
#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:6 lib/pleroma/web/twitter_api/twitter_api_controller.ex:6
|
||||||
|
#: lib/pleroma/web/uploader_controller.ex:6 lib/pleroma/web/web_finger/web_finger_controller.ex:6
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Security violation: OAuth scopes check was neither handled nor explicitly skipped."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/plugs/ensure_authenticated_plug.ex:28
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Two-factor authentication enabled, you must use a access token."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:210
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Unexpected error occurred while adding file to pack."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:138
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Unexpected error occurred while creating pack."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:278
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Unexpected error occurred while removing file from pack."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:250
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Unexpected error occurred while updating file in pack."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:179
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Unexpected error occurred while updating pack metadata."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Web push subscription is disabled on this Pleroma instance"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:451
|
||||||
|
#, elixir-format
|
||||||
|
msgid "You can't revoke your own admin/moderator status."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:126
|
||||||
|
#, elixir-format
|
||||||
|
msgid "authorization required for timeline view"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:24
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Access denied"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:282
|
||||||
|
#, elixir-format
|
||||||
|
msgid "This API requires an authenticated user"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/plugs/user_is_admin_plug.ex:21
|
||||||
|
#, elixir-format
|
||||||
|
msgid "User is not an admin."
|
||||||
|
msgstr ""
|
|
@ -1 +1 @@
|
||||||
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1,user-scalable=no"><title>Pleroma</title><!--server-generated-meta--><link rel=icon type=image/png href=/favicon.png><link href=/static/css/app.77b1644622e3bae24b6b.css rel=stylesheet><link href=/static/fontello.1599568314856.css rel=stylesheet></head><body class=hidden><noscript>To use Pleroma, please enable JavaScript.</noscript><div id=app></div><script type=text/javascript src=/static/js/vendors~app.90c4af83c1ae68f4cd95.js></script><script type=text/javascript src=/static/js/app.55d173dc5e39519aa518.js></script></body></html>
|
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1,user-scalable=no"><title>Pleroma</title><!--server-generated-meta--><link rel=icon type=image/png href=/favicon.png><link href=/static/css/app.77b1644622e3bae24b6b.css rel=stylesheet><link href=/static/fontello.1600365488745.css rel=stylesheet></head><body class=hidden><noscript>To use Pleroma, please enable JavaScript.</noscript><div id=app></div><script type=text/javascript src=/static/js/vendors~app.90c4af83c1ae68f4cd95.js></script><script type=text/javascript src=/static/js/app.826c44232e0a76bbd9ba.js></script></body></html>
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -92,6 +92,8 @@
|
||||||
|
|
||||||
<glyph glyph-name="block" unicode="" d="M732 359q0 90-48 164l-421-420q76-50 166-50 62 0 118 25t96 65 65 97 24 119z m-557-167l421 421q-75 50-167 50-83 0-153-40t-110-111-41-153q0-91 50-167z m682 167q0-88-34-168t-91-137-137-92-166-34-167 34-137 92-91 137-34 168 34 167 91 137 137 91 167 34 166-34 137-91 91-137 34-167z" horiz-adv-x="857.1" />
|
<glyph glyph-name="block" unicode="" d="M732 359q0 90-48 164l-421-420q76-50 166-50 62 0 118 25t96 65 65 97 24 119z m-557-167l421 421q-75 50-167 50-83 0-153-40t-110-111-41-153q0-91 50-167z m682 167q0-88-34-168t-91-137-137-92-166-34-167 34-137 92-91 137-34 168 34 167 91 137 137 91 167 34 166-34 137-91 91-137 34-167z" horiz-adv-x="857.1" />
|
||||||
|
|
||||||
|
<glyph glyph-name="megaphone" unicode="" d="M929 500q29 0 50-21t21-51-21-50-50-21v-214q0-29-22-50t-50-22q-233 194-453 212-32-10-51-36t-17-57 22-51q-11-19-13-37t4-32 19-31 26-28 35-28q-17-32-63-46t-94-7-73 31q-4 13-17 49t-18 53-12 50-9 56 2 55 12 62h-68q-36 0-63 26t-26 63v107q0 37 26 63t63 26h268q243 0 500 215 29 0 50-22t22-50v-214z m-72-337v532q-220-168-428-191v-151q210-23 428-190z" horiz-adv-x="1000" />
|
||||||
|
|
||||||
<glyph glyph-name="spin3" unicode="" d="M494 857c-266 0-483-210-494-472-1-19 13-20 13-20l84 0c16 0 19 10 19 18 10 199 176 358 378 358 107 0 205-45 273-118l-58-57c-11-12-11-27 5-31l247-50c21-5 46 11 37 44l-58 227c-2 9-16 22-29 13l-65-60c-89 91-214 148-352 148z m409-508c-16 0-19-10-19-18-10-199-176-358-377-358-108 0-205 45-274 118l59 57c10 12 10 27-5 31l-248 50c-21 5-46-11-37-44l58-227c2-9 16-22 30-13l64 60c89-91 214-148 353-148 265 0 482 210 493 473 1 18-13 19-13 19l-84 0z" horiz-adv-x="1000" />
|
<glyph glyph-name="spin3" unicode="" d="M494 857c-266 0-483-210-494-472-1-19 13-20 13-20l84 0c16 0 19 10 19 18 10 199 176 358 378 358 107 0 205-45 273-118l-58-57c-11-12-11-27 5-31l247-50c21-5 46 11 37 44l-58 227c-2 9-16 22-29 13l-65-60c-89 91-214 148-352 148z m409-508c-16 0-19-10-19-18-10-199-176-358-377-358-108 0-205 45-274 118l59 57c10 12 10 27-5 31l-248 50c-21 5-46-11-37-44l58-227c2-9 16-22 30-13l64 60c89-91 214-148 353-148 265 0 482 210 493 473 1 18-13 19-13 19l-84 0z" horiz-adv-x="1000" />
|
||||||
|
|
||||||
<glyph glyph-name="spin4" unicode="" d="M498 857c-114 0-228-39-320-116l0 0c173 140 428 130 588-31 134-134 164-332 89-495-10-29-5-50 12-68 21-20 61-23 84 0 3 3 12 15 15 24 71 180 33 393-112 539-99 98-228 147-356 147z m-409-274c-14 0-29-5-39-16-3-3-13-15-15-24-71-180-34-393 112-539 185-185 479-195 676-31l0 0c-173-140-428-130-589 31-134 134-163 333-89 495 11 29 6 50-12 68-11 11-27 17-44 16z" horiz-adv-x="1001" />
|
<glyph glyph-name="spin4" unicode="" d="M498 857c-114 0-228-39-320-116l0 0c173 140 428 130 588-31 134-134 164-332 89-495-10-29-5-50 12-68 21-20 61-23 84 0 3 3 12 15 15 24 71 180 33 393-112 539-99 98-228 147-356 147z m-409-274c-14 0-29-5-39-16-3-3-13-15-15-24-71-180-34-393 112-539 185-185 479-195 676-31l0 0c-173-140-428-130-589 31-134 134-163 333-89 495 11 29 6 50-12 68-11 11-27 17-44 16z" horiz-adv-x="1001" />
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
Binary file not shown.
BIN
priv/static/static/font/fontello.1600365488745.woff
Normal file
BIN
priv/static/static/font/fontello.1600365488745.woff
Normal file
Binary file not shown.
BIN
priv/static/static/font/fontello.1600365488745.woff2
Normal file
BIN
priv/static/static/font/fontello.1600365488745.woff2
Normal file
Binary file not shown.
|
@ -1,11 +1,11 @@
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Icons";
|
font-family: "Icons";
|
||||||
src: url("./font/fontello.1599568314856.eot");
|
src: url("./font/fontello.1600365488745.eot");
|
||||||
src: url("./font/fontello.1599568314856.eot") format("embedded-opentype"),
|
src: url("./font/fontello.1600365488745.eot") format("embedded-opentype"),
|
||||||
url("./font/fontello.1599568314856.woff2") format("woff2"),
|
url("./font/fontello.1600365488745.woff2") format("woff2"),
|
||||||
url("./font/fontello.1599568314856.woff") format("woff"),
|
url("./font/fontello.1600365488745.woff") format("woff"),
|
||||||
url("./font/fontello.1599568314856.ttf") format("truetype"),
|
url("./font/fontello.1600365488745.ttf") format("truetype"),
|
||||||
url("./font/fontello.1599568314856.svg") format("svg");
|
url("./font/fontello.1600365488745.svg") format("svg");
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
@ -156,3 +156,5 @@ .icon-music::before { content: "\e828"; }
|
||||||
.icon-doc::before { content: "\e829"; }
|
.icon-doc::before { content: "\e829"; }
|
||||||
|
|
||||||
.icon-block::before { content: "\e82a"; }
|
.icon-block::before { content: "\e82a"; }
|
||||||
|
|
||||||
|
.icon-megaphone::before { content: "\e82b"; }
|
|
@ -405,6 +405,12 @@
|
||||||
"css": "block",
|
"css": "block",
|
||||||
"code": 59434,
|
"code": 59434,
|
||||||
"src": "fontawesome"
|
"src": "fontawesome"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "3e674995cacc2b09692c096ea7eb6165",
|
||||||
|
"css": "megaphone",
|
||||||
|
"code": 59435,
|
||||||
|
"src": "fontawesome"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
priv/static/static/js/2.e852a6b4b3bba752b838.js.map
Normal file
1
priv/static/static/js/2.e852a6b4b3bba752b838.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
priv/static/static/js/app.826c44232e0a76bbd9ba.js
Normal file
2
priv/static/static/js/app.826c44232e0a76bbd9ba.js
Normal file
File diff suppressed because one or more lines are too long
1
priv/static/static/js/app.826c44232e0a76bbd9ba.js.map
Normal file
1
priv/static/static/js/app.826c44232e0a76bbd9ba.js.map
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,4 +1,4 @@
|
||||||
var serviceWorkerOption = {"assets":["/static/fontello.1599568314856.css","/static/font/fontello.1599568314856.eot","/static/font/fontello.1599568314856.svg","/static/font/fontello.1599568314856.ttf","/static/font/fontello.1599568314856.woff","/static/font/fontello.1599568314856.woff2","/static/img/nsfw.74818f9.png","/static/css/app.77b1644622e3bae24b6b.css","/static/js/app.55d173dc5e39519aa518.js","/static/js/vendors~app.90c4af83c1ae68f4cd95.js","/static/css/2.0778a6a864a1307a6c41.css","/static/js/2.c92f4803ff24726cea58.js","/static/css/3.b2603a50868c68a1c192.css","/static/js/3.7d21accf4e5bd07e3ebf.js","/static/js/4.5719922a4e807145346d.js","/static/js/5.cf05c5ddbdbac890ae35.js","/static/js/6.ecfd3302a692de148391.js","/static/js/7.dd44c3d58fb9dced093d.js","/static/js/8.636322a87bb10a1754f8.js","/static/js/9.6010dbcce7b4d7c05a18.js","/static/js/10.46fbbdfaf0d4800f349b.js","/static/js/11.708cc2513c53879a92cc.js","/static/js/12.b3bf0bc313861d6ec36b.js","/static/js/13.adb8a942514d735722c4.js","/static/js/14.d015d9b2ea16407e389c.js","/static/js/15.19866e6a366ccf982284.js","/static/js/16.38a984effd54736f6a2c.js","/static/js/17.9c25507194320db2e85b.js","/static/js/18.94946caca48930c224c7.js","/static/js/19.233c81ac2c28d55e9f13.js","/static/js/20.818c38d27369c3a4d677.js","/static/js/21.ce4cda179d888ca6bc2a.js","/static/js/22.2ea93c6cc569ef0256ab.js","/static/js/23.a57a7845cc20fafd06d1.js","/static/js/24.35eb55a657b5485f8491.js","/static/js/25.5a9efe20e3ae1352e6d2.js","/static/js/26.cf13231d524e5ca3b3e6.js","/static/js/27.fca8d4f6e444bd14f376.js","/static/js/28.e0f9f164e0bfd890dc61.js","/static/js/29.0b69359f0fe5c0785746.js","/static/js/30.fce58be0b52ca3e32fa4.js"]};
|
var serviceWorkerOption = {"assets":["/static/fontello.1600365488745.css","/static/font/fontello.1600365488745.eot","/static/font/fontello.1600365488745.svg","/static/font/fontello.1600365488745.ttf","/static/font/fontello.1600365488745.woff","/static/font/fontello.1600365488745.woff2","/static/img/nsfw.74818f9.png","/static/css/app.77b1644622e3bae24b6b.css","/static/js/app.826c44232e0a76bbd9ba.js","/static/js/vendors~app.90c4af83c1ae68f4cd95.js","/static/css/2.0778a6a864a1307a6c41.css","/static/js/2.e852a6b4b3bba752b838.js","/static/css/3.b2603a50868c68a1c192.css","/static/js/3.7d21accf4e5bd07e3ebf.js","/static/js/4.5719922a4e807145346d.js","/static/js/5.cf05c5ddbdbac890ae35.js","/static/js/6.ecfd3302a692de148391.js","/static/js/7.dd44c3d58fb9dced093d.js","/static/js/8.636322a87bb10a1754f8.js","/static/js/9.6010dbcce7b4d7c05a18.js","/static/js/10.46fbbdfaf0d4800f349b.js","/static/js/11.708cc2513c53879a92cc.js","/static/js/12.b3bf0bc313861d6ec36b.js","/static/js/13.adb8a942514d735722c4.js","/static/js/14.d015d9b2ea16407e389c.js","/static/js/15.19866e6a366ccf982284.js","/static/js/16.38a984effd54736f6a2c.js","/static/js/17.9c25507194320db2e85b.js","/static/js/18.94946caca48930c224c7.js","/static/js/19.233c81ac2c28d55e9f13.js","/static/js/20.818c38d27369c3a4d677.js","/static/js/21.ce4cda179d888ca6bc2a.js","/static/js/22.2ea93c6cc569ef0256ab.js","/static/js/23.a57a7845cc20fafd06d1.js","/static/js/24.35eb55a657b5485f8491.js","/static/js/25.5a9efe20e3ae1352e6d2.js","/static/js/26.cf13231d524e5ca3b3e6.js","/static/js/27.fca8d4f6e444bd14f376.js","/static/js/28.e0f9f164e0bfd890dc61.js","/static/js/29.0b69359f0fe5c0785746.js","/static/js/30.fce58be0b52ca3e32fa4.js"]};
|
||||||
|
|
||||||
!function(t){var e={};function n(r){if(e[r])return e[r].exports;var o=e[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)n.d(r,o,function(e){return t[e]}.bind(null,o));return r},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="/",n(n.s=196)}([function(t,e){var n=Array.isArray;t.exports=n},function(t,e,n){t.exports=n(53)},function(t,e,n){var r=n(32),o="object"==typeof self&&self&&self.Object===Object&&self,i=r||o||Function("return this")();t.exports=i},function(t,e,n){var r=n(97),o=n(100);t.exports=function(t,e){var n=o(t,e);return r(n)?n:void 0}},function(t,e,n){var r=n(8),o=n(64),i=n(65),a="[object Null]",s="[object Undefined]",c=r?r.toStringTag:void 0;t.exports=function(t){return null==t?void 0===t?s:a:c&&c in Object(t)?o(t):i(t)}},function(t,e){t.exports=function(t){return null!=t&&"object"==typeof t}},function(t,e){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(t){"object"==typeof window&&(n=window)}t.exports=n},function(t,e,n){var r=n(31),o=n(20);t.exports=function(t){return null!=t&&o(t.length)&&!r(t)}},function(t,e,n){var r=n(2).Symbol;t.exports=r},function(t,e){t.exports=function(t){var e=typeof t;return null!=t&&("object"==e||"function"==e)}},function(t,e,n){var r=n(4),o=n(5),i="[object Symbol]";t.exports=function(t){return"symbol"==typeof t||o(t)&&r(t)==i}},function(t,e,n){var r=n(72),o=n(78),i=n(7);t.exports=function(t){return i(t)?r(t):o(t)}},function(t,e,n){var r=n(87),o=n(88),i=n(89),a=n(90),s=n(91);function c(t){var e=-1,n=null==t?0:t.length;for(this.clear();++e<n;){var r=t[e];this.set(r[0],r[1])}}c.prototype.clear=r,c.prototype.delete=o,c.prototype.get=i,c.prototype.has=a,c.prototype.set=s,t.exports=c},function(t,e,n){var r=n(24);t.exports=function(t,e){for(var n=t.length;n--;)if(r(t[n][0],e))return n;return-1}},function(t,e,n){var r=n(3)(Object,"create");t.exports=r},function(t,e,n){var r=n(109);t.exports=function(t,e){var n=t.__data__;return r(e)?n["string"==typeof e?"string":"hash"]:n.map}},function(t,e,n){var r=n(10),o=1/0;t.exports=function(t){if("string"==typeof t||r(t))return t;var e=t+"";return"0"==e&&1/t==-o?"-0":e}},function(t,e){t.exports=function(t){return t}},function(t,e,n){var r=n(42),o=n(164),i=n(37),a=n(0);t.exports=function(t,e){return(a(t)?r:o)(t,i(e,3))}},function(t,e){t.exports=function(t){return t.webpackPolyfill||(t.deprecate=function(){},t.paths=[],t.children||(t.children=[]),Object.defineProperty(t,"loaded",{enumerable:!0,get:function(){return t.l}}),Object.defineProperty(t,"id",{enumerable:!0,get:function(){return t.i}}),t.webpackPolyfill=1),t}},function(t,e){var n=9007199254740991;t.exports=function(t){return"number"==typeof t&&t>-1&&t%1==0&&t<=n}},function(t,e){t.exports=function(t,e){for(var n=-1,r=null==t?0:t.length,o=Array(r);++n<r;)o[n]=e(t[n],n,t);return o}},function(t,e,n){var r=n(74),o=n(5),i=Object.prototype,a=i.hasOwnProperty,s=i.propertyIsEnumerable,c=r(function(){return arguments}())?r:function(t){return o(t)&&a.call(t,"callee")&&!s.call(t,"callee")};t.exports=c},function(t,e){var n=9007199254740991,r=/^(?:0|[1-9]\d*)$/;t.exports=function(t,e){var o=typeof t;return!!(e=null==e?n:e)&&("number"==o||"symbol"!=o&&r.test(t))&&t>-1&&t%1==0&&t<e}},function(t,e){t.exports=function(t,e){return t===e||t!=t&&e!=e}},function(t,e,n){var r=n(3)(n(2),"Map");t.exports=r},function(t,e,n){var r=n(101),o=n(108),i=n(110),a=n(111),s=n(112);function c(t){var e=-1,n=null==t?0:t.length;for(this.clear();++e<n;){var r=t[e];this.set(r[0],r[1])}}c.prototype.clear=r,c.prototype.delete=o,c.prototype.get=i,c.prototype.has=a,c.prototype.set=s,t.exports=c},function(t,e,n){var r=n(0),o=n(10),i=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,a=/^\w*$/;t.exports=function(t,e){if(r(t))return!1;var n=typeof t;return!("number"!=n&&"symbol"!=n&&"boolean"!=n&&null!=t&&!o(t))||a.test(t)||!i.test(t)||null!=e&&t in Object(e)}},function(t,e){
|
!function(t){var e={};function n(r){if(e[r])return e[r].exports;var o=e[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)n.d(r,o,function(e){return t[e]}.bind(null,o));return r},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="/",n(n.s=196)}([function(t,e){var n=Array.isArray;t.exports=n},function(t,e,n){t.exports=n(53)},function(t,e,n){var r=n(32),o="object"==typeof self&&self&&self.Object===Object&&self,i=r||o||Function("return this")();t.exports=i},function(t,e,n){var r=n(97),o=n(100);t.exports=function(t,e){var n=o(t,e);return r(n)?n:void 0}},function(t,e,n){var r=n(8),o=n(64),i=n(65),a="[object Null]",s="[object Undefined]",c=r?r.toStringTag:void 0;t.exports=function(t){return null==t?void 0===t?s:a:c&&c in Object(t)?o(t):i(t)}},function(t,e){t.exports=function(t){return null!=t&&"object"==typeof t}},function(t,e){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(t){"object"==typeof window&&(n=window)}t.exports=n},function(t,e,n){var r=n(31),o=n(20);t.exports=function(t){return null!=t&&o(t.length)&&!r(t)}},function(t,e,n){var r=n(2).Symbol;t.exports=r},function(t,e){t.exports=function(t){var e=typeof t;return null!=t&&("object"==e||"function"==e)}},function(t,e,n){var r=n(4),o=n(5),i="[object Symbol]";t.exports=function(t){return"symbol"==typeof t||o(t)&&r(t)==i}},function(t,e,n){var r=n(72),o=n(78),i=n(7);t.exports=function(t){return i(t)?r(t):o(t)}},function(t,e,n){var r=n(87),o=n(88),i=n(89),a=n(90),s=n(91);function c(t){var e=-1,n=null==t?0:t.length;for(this.clear();++e<n;){var r=t[e];this.set(r[0],r[1])}}c.prototype.clear=r,c.prototype.delete=o,c.prototype.get=i,c.prototype.has=a,c.prototype.set=s,t.exports=c},function(t,e,n){var r=n(24);t.exports=function(t,e){for(var n=t.length;n--;)if(r(t[n][0],e))return n;return-1}},function(t,e,n){var r=n(3)(Object,"create");t.exports=r},function(t,e,n){var r=n(109);t.exports=function(t,e){var n=t.__data__;return r(e)?n["string"==typeof e?"string":"hash"]:n.map}},function(t,e,n){var r=n(10),o=1/0;t.exports=function(t){if("string"==typeof t||r(t))return t;var e=t+"";return"0"==e&&1/t==-o?"-0":e}},function(t,e){t.exports=function(t){return t}},function(t,e,n){var r=n(42),o=n(164),i=n(37),a=n(0);t.exports=function(t,e){return(a(t)?r:o)(t,i(e,3))}},function(t,e){t.exports=function(t){return t.webpackPolyfill||(t.deprecate=function(){},t.paths=[],t.children||(t.children=[]),Object.defineProperty(t,"loaded",{enumerable:!0,get:function(){return t.l}}),Object.defineProperty(t,"id",{enumerable:!0,get:function(){return t.i}}),t.webpackPolyfill=1),t}},function(t,e){var n=9007199254740991;t.exports=function(t){return"number"==typeof t&&t>-1&&t%1==0&&t<=n}},function(t,e){t.exports=function(t,e){for(var n=-1,r=null==t?0:t.length,o=Array(r);++n<r;)o[n]=e(t[n],n,t);return o}},function(t,e,n){var r=n(74),o=n(5),i=Object.prototype,a=i.hasOwnProperty,s=i.propertyIsEnumerable,c=r(function(){return arguments}())?r:function(t){return o(t)&&a.call(t,"callee")&&!s.call(t,"callee")};t.exports=c},function(t,e){var n=9007199254740991,r=/^(?:0|[1-9]\d*)$/;t.exports=function(t,e){var o=typeof t;return!!(e=null==e?n:e)&&("number"==o||"symbol"!=o&&r.test(t))&&t>-1&&t%1==0&&t<e}},function(t,e){t.exports=function(t,e){return t===e||t!=t&&e!=e}},function(t,e,n){var r=n(3)(n(2),"Map");t.exports=r},function(t,e,n){var r=n(101),o=n(108),i=n(110),a=n(111),s=n(112);function c(t){var e=-1,n=null==t?0:t.length;for(this.clear();++e<n;){var r=t[e];this.set(r[0],r[1])}}c.prototype.clear=r,c.prototype.delete=o,c.prototype.get=i,c.prototype.has=a,c.prototype.set=s,t.exports=c},function(t,e,n){var r=n(0),o=n(10),i=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,a=/^\w*$/;t.exports=function(t,e){if(r(t))return!1;var n=typeof t;return!("number"!=n&&"symbol"!=n&&"boolean"!=n&&null!=t&&!o(t))||a.test(t)||!i.test(t)||null!=e&&t in Object(e)}},function(t,e){
|
||||||
/*!
|
/*!
|
||||||
|
|
1
test/fixtures/custom_instance_panel.html
vendored
Normal file
1
test/fixtures/custom_instance_panel.html
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<h2>Custom instance panel</h2>
|
BIN
test/fixtures/image.gif
vendored
Executable file
BIN
test/fixtures/image.gif
vendored
Executable file
Binary file not shown.
After Width: | Height: | Size: 978 KiB |
BIN
test/fixtures/image.png
vendored
Executable file
BIN
test/fixtures/image.png
vendored
Executable file
Binary file not shown.
After Width: | Height: | Size: 102 KiB |
|
@ -1,20 +1,24 @@
|
||||||
{
|
{
|
||||||
"@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", {
|
"@context": [
|
||||||
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
"https://www.w3.org/ns/activitystreams",
|
||||||
"sensitive": "as:sensitive",
|
"https://w3id.org/security/v1",
|
||||||
"movedTo": "as:movedTo",
|
{
|
||||||
"Hashtag": "as:Hashtag",
|
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
||||||
"ostatus": "http://ostatus.org#",
|
"sensitive": "as:sensitive",
|
||||||
"atomUri": "ostatus:atomUri",
|
"movedTo": "as:movedTo",
|
||||||
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
|
"Hashtag": "as:Hashtag",
|
||||||
"conversation": "ostatus:conversation",
|
"ostatus": "http://ostatus.org#",
|
||||||
"toot": "http://joinmastodon.org/ns#",
|
"atomUri": "ostatus:atomUri",
|
||||||
"Emoji": "toot:Emoji",
|
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
|
||||||
"alsoKnownAs": {
|
"conversation": "ostatus:conversation",
|
||||||
"@id": "as:alsoKnownAs",
|
"toot": "http://joinmastodon.org/ns#",
|
||||||
"@type": "@id"
|
"Emoji": "toot:Emoji",
|
||||||
|
"alsoKnownAs": {
|
||||||
|
"@id": "as:alsoKnownAs",
|
||||||
|
"@type": "@id"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}],
|
],
|
||||||
"id": "http://mastodon.example.org/users/admin",
|
"id": "http://mastodon.example.org/users/admin",
|
||||||
"type": "Person",
|
"type": "Person",
|
||||||
"following": "http://mastodon.example.org/users/admin/following",
|
"following": "http://mastodon.example.org/users/admin/following",
|
||||||
|
@ -23,6 +27,7 @@
|
||||||
"outbox": "http://mastodon.example.org/users/admin/outbox",
|
"outbox": "http://mastodon.example.org/users/admin/outbox",
|
||||||
"preferredUsername": "admin",
|
"preferredUsername": "admin",
|
||||||
"name": null,
|
"name": null,
|
||||||
|
"discoverable": "true",
|
||||||
"summary": "\u003cp\u003e\u003c/p\u003e",
|
"summary": "\u003cp\u003e\u003c/p\u003e",
|
||||||
"url": "http://mastodon.example.org/@admin",
|
"url": "http://mastodon.example.org/@admin",
|
||||||
"manuallyApprovesFollowers": false,
|
"manuallyApprovesFollowers": false,
|
||||||
|
@ -34,7 +39,8 @@
|
||||||
"owner": "http://mastodon.example.org/users/admin",
|
"owner": "http://mastodon.example.org/users/admin",
|
||||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtc4Tir+3ADhSNF6VKrtW\nOU32T01w7V0yshmQei38YyiVwVvFu8XOP6ACchkdxbJ+C9mZud8qWaRJKVbFTMUG\nNX4+6Q+FobyuKrwN7CEwhDALZtaN2IPbaPd6uG1B7QhWorrY+yFa8f2TBM3BxnUy\nI4T+bMIZIEYG7KtljCBoQXuTQmGtuffO0UwJksidg2ffCF5Q+K//JfQagJ3UzrR+\nZXbKMJdAw4bCVJYs4Z5EhHYBwQWiXCyMGTd7BGlmMkY6Av7ZqHKC/owp3/0EWDNz\nNqF09Wcpr3y3e8nA10X40MJqp/wR+1xtxp+YGbq/Cj5hZGBG7etFOmIpVBrDOhry\nBwIDAQAB\n-----END PUBLIC KEY-----\n"
|
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtc4Tir+3ADhSNF6VKrtW\nOU32T01w7V0yshmQei38YyiVwVvFu8XOP6ACchkdxbJ+C9mZud8qWaRJKVbFTMUG\nNX4+6Q+FobyuKrwN7CEwhDALZtaN2IPbaPd6uG1B7QhWorrY+yFa8f2TBM3BxnUy\nI4T+bMIZIEYG7KtljCBoQXuTQmGtuffO0UwJksidg2ffCF5Q+K//JfQagJ3UzrR+\nZXbKMJdAw4bCVJYs4Z5EhHYBwQWiXCyMGTd7BGlmMkY6Av7ZqHKC/owp3/0EWDNz\nNqF09Wcpr3y3e8nA10X40MJqp/wR+1xtxp+YGbq/Cj5hZGBG7etFOmIpVBrDOhry\nBwIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||||
},
|
},
|
||||||
"attachment": [{
|
"attachment": [
|
||||||
|
{
|
||||||
"type": "PropertyValue",
|
"type": "PropertyValue",
|
||||||
"name": "foo",
|
"name": "foo",
|
||||||
"value": "bar"
|
"value": "bar"
|
||||||
|
@ -58,5 +64,7 @@
|
||||||
"mediaType": "image/png",
|
"mediaType": "image/png",
|
||||||
"url": "https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"
|
"url": "https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"
|
||||||
},
|
},
|
||||||
"alsoKnownAs": ["http://example.org/users/foo"]
|
"alsoKnownAs": [
|
||||||
}
|
"http://example.org/users/foo"
|
||||||
|
]
|
||||||
|
}
|
|
@ -8,6 +8,7 @@
|
||||||
"preferredUsername": "mike",
|
"preferredUsername": "mike",
|
||||||
"name": "Mike Macgirvin (Osada)",
|
"name": "Mike Macgirvin (Osada)",
|
||||||
"updated": "2018-08-29T03:09:11Z",
|
"updated": "2018-08-29T03:09:11Z",
|
||||||
|
"discoverable": "true",
|
||||||
"icon": {
|
"icon": {
|
||||||
"type": "Image",
|
"type": "Image",
|
||||||
"mediaType": "image/jpeg",
|
"mediaType": "image/jpeg",
|
||||||
|
@ -51,4 +52,4 @@
|
||||||
"created": "2018-10-17T07:16:28Z",
|
"created": "2018-10-17T07:16:28Z",
|
||||||
"signatureValue": "WbfFVIPImkd3yNu6brz0CvZaeV242rwAbH0vy8DM4vfnXCxLr5Uv/Wj9gwP+tbooTxGaahAKBeqlGkQp8RLEo37LATrKMRLA/0V6DeeV+C5ORWR9B4WxyWiD3s/9Wf+KesFMtktNLAcMZ5PfnOS/xNYerhnpkp/gWPxtkglmLIWJv+w18A5zZ01JCxsO4QljHbhYaEUPHUfQ97abrkLECeam+FThVwdO6BFCtbjoNXHfzjpSZL/oKyBpi5/fpnqMqOLOQPs5WgBBZJvjEYYkQcoPTyxYI5NGpNbzIjGHPQNuACnOelH16A7L+q4swLWDIaEFeXQ2/5bmqVKZDZZ6usNP4QyTVszwd8jqo27qcDTNibXDUTsTdKpNQvM/3UncBuzuzmUV3FczhtGshIU1/pRVZiQycpVqPlGLvXhP/yZCe+1siyqDd+3uMaS2vkHTObSl5r+VYof+c+TcjrZXHSWnQTg8/X3zkoBWosrQ93VZcwjzMxQoARYv6rphbOoTz7RPmGAXYUt3/PDWkqDlmQDwCpLNNkJo1EidyefZBdD9HXQpCBO0ZU0NHb0JmPvg/+zU0krxlv70bm3RHA/maBETVjroIWzt7EwQEg5pL2hVnvSBG+1wF3BtRVe77etkPOHxLnYYIcAMLlVKCcgDd89DPIziQyruvkx1busHI08="
|
"signatureValue": "WbfFVIPImkd3yNu6brz0CvZaeV242rwAbH0vy8DM4vfnXCxLr5Uv/Wj9gwP+tbooTxGaahAKBeqlGkQp8RLEo37LATrKMRLA/0V6DeeV+C5ORWR9B4WxyWiD3s/9Wf+KesFMtktNLAcMZ5PfnOS/xNYerhnpkp/gWPxtkglmLIWJv+w18A5zZ01JCxsO4QljHbhYaEUPHUfQ97abrkLECeam+FThVwdO6BFCtbjoNXHfzjpSZL/oKyBpi5/fpnqMqOLOQPs5WgBBZJvjEYYkQcoPTyxYI5NGpNbzIjGHPQNuACnOelH16A7L+q4swLWDIaEFeXQ2/5bmqVKZDZZ6usNP4QyTVszwd8jqo27qcDTNibXDUTsTdKpNQvM/3UncBuzuzmUV3FczhtGshIU1/pRVZiQycpVqPlGLvXhP/yZCe+1siyqDd+3uMaS2vkHTObSl5r+VYof+c+TcjrZXHSWnQTg8/X3zkoBWosrQ93VZcwjzMxQoARYv6rphbOoTz7RPmGAXYUt3/PDWkqDlmQDwCpLNNkJo1EidyefZBdD9HXQpCBO0ZU0NHb0JmPvg/+zU0krxlv70bm3RHA/maBETVjroIWzt7EwQEg5pL2hVnvSBG+1wF3BtRVe77etkPOHxLnYYIcAMLlVKCcgDd89DPIziQyruvkx1busHI08="
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -31,6 +31,7 @@ def user_factory do
|
||||||
nickname: sequence(:nickname, &"nick#{&1}"),
|
nickname: sequence(:nickname, &"nick#{&1}"),
|
||||||
password_hash: Pbkdf2.hash_pwd_salt("test"),
|
password_hash: Pbkdf2.hash_pwd_salt("test"),
|
||||||
bio: sequence(:bio, &"Tester Number #{&1}"),
|
bio: sequence(:bio, &"Tester Number #{&1}"),
|
||||||
|
discoverable: true,
|
||||||
last_digest_emailed_at: NaiveDateTime.utc_now(),
|
last_digest_emailed_at: NaiveDateTime.utc_now(),
|
||||||
last_refreshed_at: NaiveDateTime.utc_now(),
|
last_refreshed_at: NaiveDateTime.utc_now(),
|
||||||
notification_settings: %Pleroma.User.NotificationSetting{},
|
notification_settings: %Pleroma.User.NotificationSetting{},
|
||||||
|
|
|
@ -40,6 +40,19 @@ test "error if file with custom settings doesn't exist" do
|
||||||
on_exit(fn -> Application.put_env(:quack, :level, initial) end)
|
on_exit(fn -> Application.put_env(:quack, :level, initial) end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@tag capture_log: true
|
||||||
|
test "config migration refused when deprecated settings are found" do
|
||||||
|
clear_config([:media_proxy, :whitelist], ["domain_without_scheme.com"])
|
||||||
|
assert Repo.all(ConfigDB) == []
|
||||||
|
|
||||||
|
Mix.Tasks.Pleroma.Config.migrate_to_db("test/fixtures/config/temp.secret.exs")
|
||||||
|
|
||||||
|
assert_received {:mix_shell, :error, [message]}
|
||||||
|
|
||||||
|
assert message =~
|
||||||
|
"Migration is not allowed until all deprecation warnings have been resolved."
|
||||||
|
end
|
||||||
|
|
||||||
test "filtered settings are migrated to db" do
|
test "filtered settings are migrated to db" do
|
||||||
assert Repo.all(ConfigDB) == []
|
assert Repo.all(ConfigDB) == []
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,14 @@ test "excludes invisible users from results" do
|
||||||
assert found_user.id == user.id
|
assert found_user.id == user.id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "excludes users when discoverable is false" do
|
||||||
|
insert(:user, %{nickname: "john 3000", discoverable: false})
|
||||||
|
insert(:user, %{nickname: "john 3001"})
|
||||||
|
|
||||||
|
users = User.search("john")
|
||||||
|
assert Enum.count(users) == 1
|
||||||
|
end
|
||||||
|
|
||||||
test "excludes service actors from results" do
|
test "excludes service actors from results" do
|
||||||
insert(:user, actor_type: "Application", nickname: "user1")
|
insert(:user, actor_type: "Application", nickname: "user1")
|
||||||
service = insert(:user, actor_type: "Service", nickname: "user2")
|
service = insert(:user, actor_type: "Service", nickname: "user2")
|
||||||
|
|
|
@ -22,6 +22,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicyTest do
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setup do: clear_config([:media_proxy, :enabled], true)
|
||||||
|
|
||||||
test "it prefetches media proxy URIs" do
|
test "it prefetches media proxy URIs" do
|
||||||
with_mock HTTP, get: fn _, _, _ -> {:ok, []} end do
|
with_mock HTTP, get: fn _, _, _ -> {:ok, []} end do
|
||||||
MediaProxyWarmingPolicy.filter(@message)
|
MediaProxyWarmingPolicy.filter(@message)
|
||||||
|
|
|
@ -26,7 +26,7 @@ test "when given an `object_data` in meta, Federation will receive a the origina
|
||||||
{
|
{
|
||||||
Pleroma.Web.ActivityPub.MRF,
|
Pleroma.Web.ActivityPub.MRF,
|
||||||
[],
|
[],
|
||||||
[filter: fn o -> {:ok, o} end]
|
[pipeline_filter: fn o, m -> {:ok, o, m} end]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Pleroma.Web.ActivityPub.ActivityPub,
|
Pleroma.Web.ActivityPub.ActivityPub,
|
||||||
|
@ -51,7 +51,7 @@ test "when given an `object_data` in meta, Federation will receive a the origina
|
||||||
Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta)
|
Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta)
|
||||||
|
|
||||||
assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta))
|
assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta))
|
||||||
assert_called(Pleroma.Web.ActivityPub.MRF.filter(activity))
|
assert_called(Pleroma.Web.ActivityPub.MRF.pipeline_filter(activity, meta))
|
||||||
assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta))
|
assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta))
|
||||||
assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta))
|
assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta))
|
||||||
refute called(Pleroma.Web.Federator.publish(activity))
|
refute called(Pleroma.Web.Federator.publish(activity))
|
||||||
|
@ -68,7 +68,7 @@ test "it goes through validation, filtering, persisting, side effects and federa
|
||||||
{
|
{
|
||||||
Pleroma.Web.ActivityPub.MRF,
|
Pleroma.Web.ActivityPub.MRF,
|
||||||
[],
|
[],
|
||||||
[filter: fn o -> {:ok, o} end]
|
[pipeline_filter: fn o, m -> {:ok, o, m} end]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Pleroma.Web.ActivityPub.ActivityPub,
|
Pleroma.Web.ActivityPub.ActivityPub,
|
||||||
|
@ -93,7 +93,7 @@ test "it goes through validation, filtering, persisting, side effects and federa
|
||||||
Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta)
|
Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta)
|
||||||
|
|
||||||
assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta))
|
assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta))
|
||||||
assert_called(Pleroma.Web.ActivityPub.MRF.filter(activity))
|
assert_called(Pleroma.Web.ActivityPub.MRF.pipeline_filter(activity, meta))
|
||||||
assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta))
|
assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta))
|
||||||
assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta))
|
assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta))
|
||||||
assert_called(Pleroma.Web.Federator.publish(activity))
|
assert_called(Pleroma.Web.Federator.publish(activity))
|
||||||
|
@ -109,7 +109,7 @@ test "it goes through validation, filtering, persisting, side effects without fe
|
||||||
{
|
{
|
||||||
Pleroma.Web.ActivityPub.MRF,
|
Pleroma.Web.ActivityPub.MRF,
|
||||||
[],
|
[],
|
||||||
[filter: fn o -> {:ok, o} end]
|
[pipeline_filter: fn o, m -> {:ok, o, m} end]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Pleroma.Web.ActivityPub.ActivityPub,
|
Pleroma.Web.ActivityPub.ActivityPub,
|
||||||
|
@ -131,7 +131,7 @@ test "it goes through validation, filtering, persisting, side effects without fe
|
||||||
Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta)
|
Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta)
|
||||||
|
|
||||||
assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta))
|
assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta))
|
||||||
assert_called(Pleroma.Web.ActivityPub.MRF.filter(activity))
|
assert_called(Pleroma.Web.ActivityPub.MRF.pipeline_filter(activity, meta))
|
||||||
assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta))
|
assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta))
|
||||||
assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta))
|
assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta))
|
||||||
end
|
end
|
||||||
|
@ -148,7 +148,7 @@ test "it goes through validation, filtering, persisting, side effects without fe
|
||||||
{
|
{
|
||||||
Pleroma.Web.ActivityPub.MRF,
|
Pleroma.Web.ActivityPub.MRF,
|
||||||
[],
|
[],
|
||||||
[filter: fn o -> {:ok, o} end]
|
[pipeline_filter: fn o, m -> {:ok, o, m} end]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Pleroma.Web.ActivityPub.ActivityPub,
|
Pleroma.Web.ActivityPub.ActivityPub,
|
||||||
|
@ -170,7 +170,7 @@ test "it goes through validation, filtering, persisting, side effects without fe
|
||||||
Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta)
|
Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta)
|
||||||
|
|
||||||
assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta))
|
assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta))
|
||||||
assert_called(Pleroma.Web.ActivityPub.MRF.filter(activity))
|
assert_called(Pleroma.Web.ActivityPub.MRF.pipeline_filter(activity, meta))
|
||||||
assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta))
|
assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta))
|
||||||
assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta))
|
assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta))
|
||||||
end
|
end
|
||||||
|
|
|
@ -1510,6 +1510,56 @@ test "excludes reblogs by default", %{conn: conn, user: user} do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "GET /api/pleroma/admin/users/:nickname/chats" do
|
||||||
|
setup do
|
||||||
|
user = insert(:user)
|
||||||
|
recipients = insert_list(3, :user)
|
||||||
|
|
||||||
|
Enum.each(recipients, fn recipient ->
|
||||||
|
CommonAPI.post_chat_message(user, recipient, "yo")
|
||||||
|
end)
|
||||||
|
|
||||||
|
%{user: user}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders user's chats", %{conn: conn, user: user} do
|
||||||
|
conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/chats")
|
||||||
|
|
||||||
|
assert json_response(conn, 200) |> length() == 3
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "GET /api/pleroma/admin/users/:nickname/chats unauthorized" do
|
||||||
|
setup do
|
||||||
|
user = insert(:user)
|
||||||
|
recipient = insert(:user)
|
||||||
|
CommonAPI.post_chat_message(user, recipient, "yo")
|
||||||
|
%{conn: conn} = oauth_access(["read:chats"])
|
||||||
|
%{conn: conn, user: user}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns 403", %{conn: conn, user: user} do
|
||||||
|
conn
|
||||||
|
|> get("/api/pleroma/admin/users/#{user.nickname}/chats")
|
||||||
|
|> json_response(403)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "GET /api/pleroma/admin/users/:nickname/chats unauthenticated" do
|
||||||
|
setup do
|
||||||
|
user = insert(:user)
|
||||||
|
recipient = insert(:user)
|
||||||
|
CommonAPI.post_chat_message(user, recipient, "yo")
|
||||||
|
%{conn: build_conn(), user: user}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns 403", %{conn: conn, user: user} do
|
||||||
|
conn
|
||||||
|
|> get("/api/pleroma/admin/users/#{user.nickname}/chats")
|
||||||
|
|> json_response(403)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "GET /api/pleroma/admin/moderation_log" do
|
describe "GET /api/pleroma/admin/moderation_log" do
|
||||||
setup do
|
setup do
|
||||||
moderator = insert(:user, is_moderator: true)
|
moderator = insert(:user, is_moderator: true)
|
||||||
|
|
219
test/web/admin_api/controllers/chat_controller_test.exs
Normal file
219
test/web/admin_api/controllers/chat_controller_test.exs
Normal file
|
@ -0,0 +1,219 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.AdminAPI.ChatControllerTest do
|
||||||
|
use Pleroma.Web.ConnCase
|
||||||
|
|
||||||
|
import Pleroma.Factory
|
||||||
|
|
||||||
|
alias Pleroma.Chat
|
||||||
|
alias Pleroma.Chat.MessageReference
|
||||||
|
alias Pleroma.Config
|
||||||
|
alias Pleroma.ModerationLog
|
||||||
|
alias Pleroma.Object
|
||||||
|
alias Pleroma.Repo
|
||||||
|
alias Pleroma.Web.CommonAPI
|
||||||
|
|
||||||
|
defp admin_setup do
|
||||||
|
admin = insert(:user, is_admin: true)
|
||||||
|
token = insert(:oauth_admin_token, user: admin)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
build_conn()
|
||||||
|
|> assign(:user, admin)
|
||||||
|
|> assign(:token, token)
|
||||||
|
|
||||||
|
{:ok, %{admin: admin, token: token, conn: conn}}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "DELETE /api/pleroma/admin/chats/:id/messages/:message_id" do
|
||||||
|
setup do: admin_setup()
|
||||||
|
|
||||||
|
test "it deletes a message from the chat", %{conn: conn, admin: admin} do
|
||||||
|
user = insert(:user)
|
||||||
|
recipient = insert(:user)
|
||||||
|
|
||||||
|
{:ok, message} =
|
||||||
|
CommonAPI.post_chat_message(user, recipient, "Hello darkness my old friend")
|
||||||
|
|
||||||
|
object = Object.normalize(message, false)
|
||||||
|
|
||||||
|
chat = Chat.get(user.id, recipient.ap_id)
|
||||||
|
recipient_chat = Chat.get(recipient.id, user.ap_id)
|
||||||
|
|
||||||
|
cm_ref = MessageReference.for_chat_and_object(chat, object)
|
||||||
|
recipient_cm_ref = MessageReference.for_chat_and_object(recipient_chat, object)
|
||||||
|
|
||||||
|
result =
|
||||||
|
conn
|
||||||
|
|> put_req_header("content-type", "application/json")
|
||||||
|
|> delete("/api/pleroma/admin/chats/#{chat.id}/messages/#{cm_ref.id}")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
log_entry = Repo.one(ModerationLog)
|
||||||
|
|
||||||
|
assert ModerationLog.get_log_entry_message(log_entry) ==
|
||||||
|
"@#{admin.nickname} deleted chat message ##{cm_ref.id}"
|
||||||
|
|
||||||
|
assert result["id"] == cm_ref.id
|
||||||
|
refute MessageReference.get_by_id(cm_ref.id)
|
||||||
|
refute MessageReference.get_by_id(recipient_cm_ref.id)
|
||||||
|
assert %{data: %{"type" => "Tombstone"}} = Object.get_by_id(object.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "GET /api/pleroma/admin/chats/:id/messages" do
|
||||||
|
setup do: admin_setup()
|
||||||
|
|
||||||
|
test "it paginates", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
recipient = insert(:user)
|
||||||
|
|
||||||
|
Enum.each(1..30, fn _ ->
|
||||||
|
{:ok, _} = CommonAPI.post_chat_message(user, recipient, "hey")
|
||||||
|
end)
|
||||||
|
|
||||||
|
chat = Chat.get(user.id, recipient.ap_id)
|
||||||
|
|
||||||
|
result =
|
||||||
|
conn
|
||||||
|
|> get("/api/pleroma/admin/chats/#{chat.id}/messages")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert length(result) == 20
|
||||||
|
|
||||||
|
result =
|
||||||
|
conn
|
||||||
|
|> get("/api/pleroma/admin/chats/#{chat.id}/messages?max_id=#{List.last(result)["id"]}")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert length(result) == 10
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it returns the messages for a given chat", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
third_user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, _} = CommonAPI.post_chat_message(user, other_user, "hey")
|
||||||
|
{:ok, _} = CommonAPI.post_chat_message(user, third_user, "hey")
|
||||||
|
{:ok, _} = CommonAPI.post_chat_message(user, other_user, "how are you?")
|
||||||
|
{:ok, _} = CommonAPI.post_chat_message(other_user, user, "fine, how about you?")
|
||||||
|
|
||||||
|
chat = Chat.get(user.id, other_user.ap_id)
|
||||||
|
|
||||||
|
result =
|
||||||
|
conn
|
||||||
|
|> get("/api/pleroma/admin/chats/#{chat.id}/messages")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
result
|
||||||
|
|> Enum.each(fn message ->
|
||||||
|
assert message["chat_id"] == chat.id |> to_string()
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert length(result) == 3
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "GET /api/pleroma/admin/chats/:id" do
|
||||||
|
setup do: admin_setup()
|
||||||
|
|
||||||
|
test "it returns a chat", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id)
|
||||||
|
|
||||||
|
result =
|
||||||
|
conn
|
||||||
|
|> get("/api/pleroma/admin/chats/#{chat.id}")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert result["id"] == to_string(chat.id)
|
||||||
|
assert %{} = result["sender"]
|
||||||
|
assert %{} = result["receiver"]
|
||||||
|
refute result["account"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "unauthorized chat moderation" do
|
||||||
|
setup do
|
||||||
|
user = insert(:user)
|
||||||
|
recipient = insert(:user)
|
||||||
|
|
||||||
|
{:ok, message} = CommonAPI.post_chat_message(user, recipient, "Yo")
|
||||||
|
object = Object.normalize(message, false)
|
||||||
|
chat = Chat.get(user.id, recipient.ap_id)
|
||||||
|
cm_ref = MessageReference.for_chat_and_object(chat, object)
|
||||||
|
|
||||||
|
%{conn: conn} = oauth_access(["read:chats", "write:chats"])
|
||||||
|
%{conn: conn, chat: chat, cm_ref: cm_ref}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "DELETE /api/pleroma/admin/chats/:id/messages/:message_id", %{
|
||||||
|
conn: conn,
|
||||||
|
chat: chat,
|
||||||
|
cm_ref: cm_ref
|
||||||
|
} do
|
||||||
|
conn
|
||||||
|
|> put_req_header("content-type", "application/json")
|
||||||
|
|> delete("/api/pleroma/admin/chats/#{chat.id}/messages/#{cm_ref.id}")
|
||||||
|
|> json_response(403)
|
||||||
|
|
||||||
|
assert MessageReference.get_by_id(cm_ref.id) == cm_ref
|
||||||
|
end
|
||||||
|
|
||||||
|
test "GET /api/pleroma/admin/chats/:id/messages", %{conn: conn, chat: chat} do
|
||||||
|
conn
|
||||||
|
|> get("/api/pleroma/admin/chats/#{chat.id}/messages")
|
||||||
|
|> json_response(403)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "GET /api/pleroma/admin/chats/:id", %{conn: conn, chat: chat} do
|
||||||
|
conn
|
||||||
|
|> get("/api/pleroma/admin/chats/#{chat.id}")
|
||||||
|
|> json_response(403)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "unauthenticated chat moderation" do
|
||||||
|
setup do
|
||||||
|
user = insert(:user)
|
||||||
|
recipient = insert(:user)
|
||||||
|
|
||||||
|
{:ok, message} = CommonAPI.post_chat_message(user, recipient, "Yo")
|
||||||
|
object = Object.normalize(message, false)
|
||||||
|
chat = Chat.get(user.id, recipient.ap_id)
|
||||||
|
cm_ref = MessageReference.for_chat_and_object(chat, object)
|
||||||
|
|
||||||
|
%{conn: build_conn(), chat: chat, cm_ref: cm_ref}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "DELETE /api/pleroma/admin/chats/:id/messages/:message_id", %{
|
||||||
|
conn: conn,
|
||||||
|
chat: chat,
|
||||||
|
cm_ref: cm_ref
|
||||||
|
} do
|
||||||
|
conn
|
||||||
|
|> put_req_header("content-type", "application/json")
|
||||||
|
|> delete("/api/pleroma/admin/chats/#{chat.id}/messages/#{cm_ref.id}")
|
||||||
|
|> json_response(403)
|
||||||
|
|
||||||
|
assert MessageReference.get_by_id(cm_ref.id) == cm_ref
|
||||||
|
end
|
||||||
|
|
||||||
|
test "GET /api/pleroma/admin/chats/:id/messages", %{conn: conn, chat: chat} do
|
||||||
|
conn
|
||||||
|
|> get("/api/pleroma/admin/chats/#{chat.id}/messages")
|
||||||
|
|> json_response(403)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "GET /api/pleroma/admin/chats/:id", %{conn: conn, chat: chat} do
|
||||||
|
conn
|
||||||
|
|> get("/api/pleroma/admin/chats/#{chat.id}")
|
||||||
|
|> json_response(403)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue