diff --git a/CHANGELOG.md b/CHANGELOG.md
index f7a372e11..5a7e27fd3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,12 +5,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased
+### Added
+- Experimental websocket-based federation between Pleroma instances.
+
### Changed
- 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.
+- 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).
+### Added
+- Media preview proxy (requires media proxy be enabled; see `:media_preview_proxy` config for more details).
+
### Removed
- **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
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
-- 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
+- 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 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
@@ -41,6 +64,12 @@ switched to a new configuration mechanism, however it was not officially removed
### Added
- Rich media failure tracking (along with `:failure_backoff` option).
+
+ Admin API Changes
+
+- Add `PATCH /api/pleroma/admin/instance_document/:document_name` to modify the Terms of Service and Instance Panel HTML pages via Admin API
+
+
### Fixed
- 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
diff --git a/config/config.exs b/config/config.exs
index 61255a5c5..d3e42fe62 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -130,6 +130,7 @@
dispatch: [
{:_,
[
+ {"/api/fedsocket/v1", Pleroma.Web.FedSockets.IncomingHandler, []},
{"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
{"/websocket", Phoenix.Endpoint.CowboyWebSocket,
{Phoenix.Transports.WebSocket,
@@ -148,6 +149,16 @@
"SameSite=Lax"
]
+config :pleroma, :fed_sockets,
+ enabled: false,
+ connection_duration: :timer.hours(8),
+ rejection_duration: :timer.minutes(15),
+ fed_socket_fetches: [
+ default: 12_000,
+ interval: 3_000,
+ lazy: false
+ ]
+
# Configures Elixir's Logger
config :logger, :console,
level: :debug,
@@ -423,6 +434,8 @@
proxy_opts: [
redirect_on_failure: false,
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: [
follow_redirect: true,
pool: :media
@@ -437,6 +450,14 @@
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 :phoenix, :format_encoders, json: Jason
@@ -532,6 +553,7 @@
token_expiration: 5,
federator_incoming: 50,
federator_outgoing: 50,
+ ingestion_queue: 50,
web_push: 50,
mailer: 10,
transmogrifier: 20,
@@ -742,8 +764,8 @@
],
media: [
size: 50,
- max_waiting: 10,
- recv_timeout: 10_000
+ max_waiting: 20,
+ recv_timeout: 15_000
],
upload: [
size: 25,
@@ -788,6 +810,8 @@
config :ex_aws, http_client: Pleroma.HTTP.ExAws
+config :web_push_encryption, http_client: Pleroma.HTTP
+
config :pleroma, :instances_favicons, enabled: false
config :floki, :html_parser, Floki.HTMLParser.FastHtml
diff --git a/config/description.exs b/config/description.exs
index 2b30f8148..097e98633 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -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,
key: Pleroma.Emails.Mailer,
@@ -1874,6 +1887,7 @@
suggestions: [
redirect_on_failure: false,
max_body_length: 25 * 1_048_576,
+ max_read_duration: 30_000,
http: [
follow_redirect: true,
pool: :media
@@ -1894,6 +1908,11 @@
"Limits the content length to be approximately the " <>
"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,
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,
key: Pleroma.Web.MediaProxy.Invalidation.Http,
diff --git a/config/test.exs b/config/test.exs
index 0ee6f1b7f..93a0e2a61 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -19,6 +19,11 @@
level: :warn,
format: "\n[$level] $message\n"
+config :pleroma, :fed_sockets,
+ enabled: false,
+ connection_duration: 5,
+ rejection_duration: 5
+
config :pleroma, :auth, oauth_consumer_strategies: []
config :pleroma, Pleroma.Upload,
diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md
index c0ea074f0..7992db58f 100644
--- a/docs/API/admin_api.md
+++ b/docs/API/admin_api.md
@@ -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
+
Instance panel
+```
+
+## `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"
+}
+```
diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md
index 054b8fe43..42e5fe808 100644
--- a/docs/configuration/cheatsheet.md
+++ b/docs/configuration/cheatsheet.md
@@ -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
+## 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
### :frontend_configurations
@@ -314,6 +324,14 @@ This section describe PWA manifest instance-specific values. Currently this opti
* `enabled`: Enables purge cache
* `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
#### Pleroma.Web.MediaProxy.Invalidation.Script
diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex
index 904c5a74b..18f99318d 100644
--- a/lib/mix/tasks/pleroma/config.ex
+++ b/lib/mix/tasks/pleroma/config.ex
@@ -32,7 +32,8 @@ def run(["migrate_from_db" | options]) do
@spec migrate_to_db(Path.t() | nil) :: any()
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 =
if file_path do
file_path
@@ -46,7 +47,8 @@ def migrate_to_db(file_path \\ nil) do
do_migrate_to_db(config_file)
else
- migration_error()
+ :error -> deprecation_error()
+ _ -> migration_error()
end
end
@@ -120,6 +122,10 @@ defp migration_error do
)
end
+ defp deprecation_error do
+ shell_error("Migration is not allowed until all deprecation warnings have been resolved.")
+ end
+
if Code.ensure_loaded?(Config.Reader) do
defp config_header, do: "import Config\r\n\r\n"
defp read_file(config_file), do: Config.Reader.read_imports!(config_file)
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index c39e24919..00ec79a2a 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -99,7 +99,7 @@ def start(_type, _args) do
{Oban, Config.get(Oban)}
] ++
task_children(@env) ++
- streamer_child(@env) ++
+ dont_run_in_test(@env) ++
chat_child(@env, chat_enabled?()) ++
[
Pleroma.Web.Endpoint,
@@ -188,16 +188,17 @@ def build_cachex(type, opts),
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,
[
name: Pleroma.Web.Streamer.registry(),
keys: :duplicate,
partitions: System.schedulers_online()
- ]}
+ ]},
+ Pleroma.Web.FedSockets.Supervisor
]
end
diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex
index 24a86371e..84f8806a0 100644
--- a/lib/pleroma/chat.ex
+++ b/lib/pleroma/chat.ex
@@ -6,7 +6,9 @@ defmodule Pleroma.Chat do
use Ecto.Schema
import Ecto.Changeset
+ import Ecto.Query
+ alias Pleroma.Chat
alias Pleroma.Repo
alias Pleroma.User
@@ -69,4 +71,12 @@ def bump_or_create(user_id, recipient) do
conflict_target: [:user_id, :recipient]
)
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
diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex
index 412d55a77..98c4dc9c8 100644
--- a/lib/pleroma/config/deprecation_warnings.ex
+++ b/lib/pleroma/config/deprecation_warnings.ex
@@ -26,6 +26,10 @@ def check_hellthread_threshold do
!!!DEPRECATION WARNING!!!
You are using the old configuration mechanism for the hellthread filter. Please check config.md.
""")
+
+ :error
+ else
+ :ok
end
end
@@ -47,17 +51,26 @@ def mrf_user_allowlist do
config :pleroma, :mrf_user_allowlist, #{inspect(rewritten, pretty: true)}
""")
+
+ :error
+ else
+ :ok
end
end
def warn do
- check_hellthread_threshold()
- mrf_user_allowlist()
- check_old_mrf_config()
- check_media_proxy_whitelist_config()
- check_welcome_message_config()
- check_gun_pool_options()
- check_activity_expiration_config()
+ with :ok <- check_hellthread_threshold(),
+ :ok <- mrf_user_allowlist(),
+ :ok <- check_old_mrf_config(),
+ :ok <- check_media_proxy_whitelist_config(),
+ :ok <- check_welcome_message_config(),
+ :ok <- check_gun_pool_options(),
+ :ok <- check_activity_expiration_config() do
+ :ok
+ else
+ _ ->
+ :error
+ end
end
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_message` is now `config :pleroma, :welcome, :direct_message, :message`
""")
+
+ :error
+ else
+ :ok
end
end
@@ -101,8 +118,11 @@ def move_namespace_and_warn(config_map, warning_preface) do
end
end)
- if warning != "" do
+ if warning == "" do
+ :ok
+ else
Logger.warn(warning_preface <> warning)
+ :error
end
end
@@ -115,6 +135,10 @@ def check_media_proxy_whitelist_config do
!!!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.
""")
+
+ :error
+ else
+ :ok
end
end
@@ -157,6 +181,9 @@ def check_gun_pool_options do
Logger.warn(Enum.join([warning_preface | pool_warnings]))
Config.put(:pools, updated_config)
+ :error
+ else
+ :ok
end
end
diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex
new file mode 100644
index 000000000..b6f35a24b
--- /dev/null
+++ b/lib/pleroma/helpers/media_helper.ex
@@ -0,0 +1,150 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# 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
diff --git a/lib/pleroma/helpers/qt_fast_start.ex b/lib/pleroma/helpers/qt_fast_start.ex
new file mode 100644
index 000000000..bb93224b5
--- /dev/null
+++ b/lib/pleroma/helpers/qt_fast_start.ex
@@ -0,0 +1,131 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# 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(
+ <>,
+ pos,
+ got_moov,
+ got_mdat,
+ acc
+ ) do
+ full_size = (size - 8) * 8
+ <> = rest
+
+ acc = [
+ {fourcc, pos, pos + size, size,
+ <>}
+ | 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
+ [ftyp, moov_head, fix_moov(moov_data, offset, []), rest]
+ end
+
+ defp fix_moov(
+ <>,
+ offset,
+ acc
+ ) do
+ full_size = (size - 8) * 8
+ <> = rest
+
+ data =
+ cond do
+ fourcc in ["trak", "mdia", "minf", "stbl"] ->
+ # Theses contains sto or co64 part
+ [<>, fix_moov(data, offset, [])]
+
+ fourcc in ["stco", "co64"] ->
+ # fix the damn thing
+ <> = data
+
+ entry_size =
+ case fourcc do
+ "stco" -> 32
+ "co64" -> 64
+ end
+
+ [
+ <>,
+ rewrite_entries(entry_size, offset, rest, [])
+ ]
+
+ true ->
+ [<>, 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,
+ <>,
+ acc
+ ) do
+ rewrite_entries(unquote(size), offset, rest, [
+ acc | <>
+ ])
+ end
+ end
+
+ defp rewrite_entries(_, _, <<>>, acc), do: acc
+end
diff --git a/lib/pleroma/helpers/uri_helper.ex b/lib/pleroma/helpers/uri_helper.ex
index 6d205a636..f1301f055 100644
--- a/lib/pleroma/helpers/uri_helper.ex
+++ b/lib/pleroma/helpers/uri_helper.ex
@@ -3,18 +3,22 @@
# SPDX-License-Identifier: AGPL-3.0-only
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)
- appended_params = for {k, v} <- appended_params, into: %{}, do: {to_string(k), v}
- existing_params = URI.query_decoder(uri.query || "") |> Enum.into(%{})
- updated_params_keys = Enum.uniq(Map.keys(existing_params) ++ Map.keys(appended_params))
+
+ existing_params = URI.query_decoder(uri.query || "") |> Map.new()
+ 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 =
- for k <- updated_params_keys, do: {k, appended_params[k] || existing_params[k]}
+ existing_params
+ |> Map.merge(overridden_params)
+ |> Map.drop(deleted_params)
uri
|> Map.put(:query, URI.encode_query(updated_params))
|> URI.to_string()
+ |> String.replace_suffix("?", "")
end
def maybe_add_base("/" <> uri, base), do: Path.join([base, uri])
diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex
index 6948651c7..f0f601469 100644
--- a/lib/pleroma/instances/instance.ex
+++ b/lib/pleroma/instances/instance.ex
@@ -156,9 +156,7 @@ def get_or_update_favicon(%URI{host: host} = instance_uri) do
defp scrape_favicon(%URI{} = instance_uri) do
try do
with {:ok, %Tesla.Env{body: html}} <-
- Pleroma.HTTP.get(to_string(instance_uri), [{"accept", "text/html"}],
- adapter: [pool: :media]
- ),
+ Pleroma.HTTP.get(to_string(instance_uri), [{"accept", "text/html"}], pool: :media),
{_, [favicon_rel | _]} when is_binary(favicon_rel) <-
{:parse,
html |> Floki.parse_document!() |> Floki.attribute("link[rel=icon]", "href")},
diff --git a/lib/pleroma/moderation_log.ex b/lib/pleroma/moderation_log.ex
index 31c9afe2a..47036a6f6 100644
--- a/lib/pleroma/moderation_log.ex
+++ b/lib/pleroma/moderation_log.ex
@@ -320,6 +320,19 @@ def insert_log(%{
|> insert_log_entry_with_message()
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}
defp insert_log_entry_with_message(entry) do
entry.data["message"]
@@ -627,6 +640,17 @@ def get_log_entry_message(%ModerationLog{
"@#{actor_nickname} updated users: #{users_to_nicknames_string(subjects)}"
end
+ @spec get_log_entry_message(ModerationLog) :: String.t()
+ def get_log_entry_message(%ModerationLog{
+ data: %{
+ "actor" => %{"nickname" => actor_nickname},
+ "action" => "chat_message_delete",
+ "subject_id" => subject_id
+ }
+ }) do
+ "@#{actor_nickname} deleted chat message ##{subject_id}"
+ end
+
defp nicknames_to_string(nicknames) do
nicknames
|> Enum.map(&"@#{&1}")
diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex
index 24dc7cb95..169298b34 100644
--- a/lib/pleroma/object/fetcher.ex
+++ b/lib/pleroma/object/fetcher.ex
@@ -12,6 +12,7 @@ defmodule Pleroma.Object.Fetcher do
alias Pleroma.Web.ActivityPub.ObjectValidator
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.Federator
+ alias Pleroma.Web.FedSockets
require Logger
require Pleroma.Constants
@@ -182,27 +183,20 @@ defp maybe_date_fetch(headers, date) do
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")
- 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")},
- {:ok, %{body: body, status: code}} when code in 200..299 <- HTTP.get(id, headers),
- {:ok, data} <- Jason.decode(body),
+ {:ok, body} <- get_object(id, opts),
+ {:ok, data} <- safe_json_decode(body),
:ok <- Containment.contain_origin_from_id(id, data) do
{:ok, data}
else
- {:ok, %{status: code}} when code in [404, 410] ->
- {:error, "Object has been deleted"}
-
{: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
- def fetch_and_contain_remote_object_from_id(%{"id" => id}),
- do: fetch_and_contain_remote_object_from_id(id)
+ def fetch_and_contain_remote_object_from_id(_id, _opts),
+ 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
diff --git a/lib/pleroma/reverse_proxy/reverse_proxy.ex b/lib/pleroma/reverse_proxy/reverse_proxy.ex
index 0de4e2309..8ae1157df 100644
--- a/lib/pleroma/reverse_proxy/reverse_proxy.ex
+++ b/lib/pleroma/reverse_proxy/reverse_proxy.ex
@@ -17,6 +17,9 @@ defmodule Pleroma.ReverseProxy do
@failed_request_ttl :timer.seconds(60)
@methods ~w(GET HEAD)
+ def max_read_duration_default, do: @max_read_duration
+ def default_cache_control_header, do: @default_cache_control_header
+
@moduledoc """
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 check_read_duration(nil = _duration, max), do: check_read_duration(@max_read_duration, max)
+
defp check_read_duration(duration, max)
when is_integer(duration) and is_integer(max) and max > 0 do
if duration > max do
diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex
index 3aa6909d2..e388993b7 100644
--- a/lib/pleroma/signature.ex
+++ b/lib/pleroma/signature.ex
@@ -39,7 +39,7 @@ def key_id_to_actor_id(key_id) do
def fetch_public_key(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
{: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}
else
e ->
@@ -50,8 +50,8 @@ def fetch_public_key(conn) do
def refetch_public_key(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
{:ok, actor_id} <- key_id_to_actor_id(kid),
- {:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id),
- {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
+ {: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, force_http: true) do
{:ok, public_key}
else
e ->
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 1542986d3..83eb4d5ff 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -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 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)
- 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
{_, {: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 get_public_key_for_ap_id(ap_id) do
- with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
+ def get_public_key_for_ap_id(ap_id, opts \\ []) do
+ with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id, opts),
{:ok, public_key} <- public_key(user) do
{:ok, public_key}
else
diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex
index 7babd47ea..b8c648672 100644
--- a/lib/pleroma/user/search.ex
+++ b/lib/pleroma/user/search.ex
@@ -52,6 +52,7 @@ defp search_query(query_string, for_user, following) do
|> base_query(following)
|> filter_blocked_user(for_user)
|> filter_invisible_users()
+ |> filter_discoverable_users()
|> filter_internal_users()
|> filter_blocked_domains(for_user)
|> fts_search(query_string)
@@ -122,6 +123,10 @@ defp filter_invisible_users(query) do
from(q in query, where: q.invisible == false)
end
+ defp filter_discoverable_users(query) do
+ from(q in query, where: q.discoverable == true)
+ end
+
defp filter_internal_users(query) do
from(q in query, where: q.actor_type != "Application")
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 92fc1e422..06e8e1a7c 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -1270,10 +1270,12 @@ defp object_to_user_data(data) do
def fetch_follow_information_for_user(user) do
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, 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,
%{
@@ -1347,8 +1349,8 @@ def user_data_from_user_object(data) do
end
end
- def fetch_and_prepare_user_from_ap_id(ap_id) do
- with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id),
+ 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, opts),
{:ok, data} <- user_data_from_user_object(data) do
{:ok, maybe_update_follow_information(data)}
else
@@ -1390,13 +1392,13 @@ def maybe_handle_clashing_nickname(data) do
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)
if user && !User.ap_enabled?(user) do
Transmogrifier.upgrade_user_from_ap_id(ap_id)
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
user
|> User.remote_user_changeset(data)
diff --git a/lib/pleroma/web/activity_pub/mrf.ex b/lib/pleroma/web/activity_pub/mrf.ex
index 206d6af52..5e5361082 100644
--- a/lib/pleroma/web/activity_pub/mrf.ex
+++ b/lib/pleroma/web/activity_pub/mrf.ex
@@ -5,16 +5,34 @@
defmodule Pleroma.Web.ActivityPub.MRF do
@callback filter(Map.t()) :: {:ok | :reject, Map.t()}
- def filter(policies, %{} = object) do
+ def filter(policies, %{} = message) do
policies
- |> Enum.reduce({:ok, object}, fn
- policy, {:ok, object} -> policy.filter(object)
+ |> Enum.reduce({:ok, message}, fn
+ policy, {:ok, message} -> policy.filter(message)
_, error -> error
end)
end
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
Pleroma.Config.get([:mrf, :policies], []) |> get_policies()
end
diff --git a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
index 15e09dcf0..db66cfa3e 100644
--- a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
@@ -20,9 +20,17 @@ defp string_matches?(string, pattern) do
String.match?(string, pattern)
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 ->
- string_matches?(content, pattern) or string_matches?(summary, pattern)
+ string_matches?(payload, pattern)
end) do
{:reject, "[KeywordPolicy] Matches with rejected keyword"}
else
@@ -30,12 +38,12 @@ defp check_reject(%{"object" => %{"content" => content, "summary" => summary}} =
end
end
- defp check_ftl_removal(
- %{"to" => to, "object" => %{"content" => content, "summary" => summary}} = message
- ) do
+ defp check_ftl_removal(%{"to" => to, "object" => %{} = object} = message) do
+ payload = object_payload(object)
+
if Pleroma.Constants.as_public() in to and
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
to = List.delete(to, Pleroma.Constants.as_public())
cc = [Pleroma.Constants.as_public() | message["cc"] || []]
@@ -51,35 +59,24 @@ defp check_ftl_removal(
end
end
- defp check_replace(%{"object" => %{"content" => content, "summary" => summary}} = message) do
- content =
- if is_binary(content) do
- content
- else
- ""
- end
+ defp check_replace(%{"object" => %{} = object} = message) do
+ object =
+ ["content", "name", "summary"]
+ |> Enum.filter(fn field -> Map.has_key?(object, field) && object[field] end)
+ |> Enum.reduce(object, fn field, object ->
+ data =
+ Enum.reduce(
+ Pleroma.Config.get([:mrf_keyword, :replace]),
+ object[field],
+ fn {pat, repl}, acc -> String.replace(acc, pat, repl) end
+ )
- summary =
- if is_binary(summary) do
- summary
- else
- ""
- end
+ Map.put(object, field, data)
+ end)
- {content, summary} =
- 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
- )
+ message = Map.put(message, "object", object)
- {:ok,
- message
- |> put_in(["object", "content"], content)
- |> put_in(["object", "summary"], summary)}
+ {:ok, message}
end
@impl true
diff --git a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex
index 98d595469..0fb05d3c4 100644
--- a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex
@@ -12,17 +12,21 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do
require Logger
- @options [
+ @adapter_options [
pool: :media,
recv_timeout: 10_000
]
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
- |> MediaProxy.url()
- |> HTTP.get([], @options)
+ Logger.debug("Prefetching #{inspect(url)} as #{inspect(prefetch_url)}")
+
+ HTTP.get(prefetch_url, [], @adapter_options)
+ end
end
def perform(:preload, %{"object" => %{"attachment" => attachments}} = _message) do
diff --git a/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex b/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex
index c9f20571f..048052da6 100644
--- a/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex
@@ -28,8 +28,7 @@ def filter(%{"actor" => actor} = message) do
}"
)
- subchain
- |> MRF.filter(message)
+ MRF.filter(subchain, message)
else
_e -> {:ok, message}
end
diff --git a/lib/pleroma/web/activity_pub/pipeline.ex b/lib/pleroma/web/activity_pub/pipeline.ex
index 36e325c37..2db86f116 100644
--- a/lib/pleroma/web/activity_pub/pipeline.ex
+++ b/lib/pleroma/web/activity_pub/pipeline.ex
@@ -26,13 +26,17 @@ def common_pipeline(object, meta) do
{:error, e} ->
{:error, e}
+
+ {:reject, e} ->
+ {:reject, e}
end
end
def do_common_pipeline(object, meta) do
with {_, {:ok, validated_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}} <-
{:persist_object, ActivityPub.persist(mrfd_object, meta)},
{_, {:ok, activity, meta}} <-
@@ -40,7 +44,7 @@ def do_common_pipeline(object, meta) do
{_, {:ok, _}} <- {:federation, maybe_federate(activity, meta)} do
{:ok, activity, meta}
else
- {:mrf_object, {:reject, _}} -> {:ok, nil, meta}
+ {:mrf_object, {:reject, message, _}} -> {:reject, message}
e -> {:error, e}
end
end
diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex
index d88f7f3ee..9c3956683 100644
--- a/lib/pleroma/web/activity_pub/publisher.ex
+++ b/lib/pleroma/web/activity_pub/publisher.ex
@@ -13,6 +13,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier
+ alias Pleroma.Web.FedSockets
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
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())
date = Pleroma.Signature.signed_date()
signature =
Pleroma.Signature.sign(actor, %{
- "(request-target)": "post #{uri.path}",
+ "(request-target)": "post #{path}",
host: signature_host(uri),
"content-length": byte_size(json),
digest: digest,
@@ -89,15 +110,6 @@ def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = pa
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
if port == URI.default_port(scheme) do
host
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index fcca014f0..aa6a69463 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -1000,7 +1000,7 @@ def perform(:user_upgrade, user) do
def upgrade_user_from_ap_id(ap_id) do
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
TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
{:ok, user}
diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
index f5e4d49f9..d5713c3dd 100644
--- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
@@ -23,8 +23,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Router
- require Logger
-
@users_page_size 50
plug(
@@ -68,6 +66,12 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
when action in [:list_user_statuses, :list_instance_statuses]
)
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read:chats"], admin: true}
+ when action in [:list_user_chats]
+ )
+
plug(
OAuthScopesPlug,
%{scopes: ["read"], admin: true}
@@ -256,6 +260,20 @@ def list_user_statuses(%{assigns: %{user: admin}} = conn, %{"nickname" => nickna
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
user = User.get_cached_by_nickname(nickname)
diff --git a/lib/pleroma/web/admin_api/controllers/chat_controller.ex b/lib/pleroma/web/admin_api/controllers/chat_controller.ex
new file mode 100644
index 000000000..967600d69
--- /dev/null
+++ b/lib/pleroma/web/admin_api/controllers/chat_controller.ex
@@ -0,0 +1,85 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# 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
diff --git a/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex b/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex
new file mode 100644
index 000000000..504d9b517
--- /dev/null
+++ b/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex
@@ -0,0 +1,41 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# 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
diff --git a/lib/pleroma/web/admin_api/views/chat_view.ex b/lib/pleroma/web/admin_api/views/chat_view.ex
new file mode 100644
index 000000000..847df1423
--- /dev/null
+++ b/lib/pleroma/web/admin_api/views/chat_view.ex
@@ -0,0 +1,30 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# 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
diff --git a/lib/pleroma/web/admin_api/views/status_view.ex b/lib/pleroma/web/admin_api/views/status_view.ex
index 500800be2..6042a22b6 100644
--- a/lib/pleroma/web/admin_api/views/status_view.ex
+++ b/lib/pleroma/web/admin_api/views/status_view.ex
@@ -8,6 +8,7 @@ defmodule Pleroma.Web.AdminAPI.StatusView do
require Pleroma.Constants
alias Pleroma.Web.AdminAPI
+ alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI
defdelegate merge_account_views(user), to: AdminAPI.AccountView
@@ -17,7 +18,7 @@ def render("index.json", opts) do
end
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)
|> Map.merge(%{account: merge_account_views(user)})
diff --git a/lib/pleroma/web/api_spec.ex b/lib/pleroma/web/api_spec.ex
index 79fd5f871..93a5273e3 100644
--- a/lib/pleroma/web/api_spec.ex
+++ b/lib/pleroma/web/api_spec.ex
@@ -13,10 +13,15 @@ defmodule Pleroma.Web.ApiSpec do
@impl OpenApi
def spec do
%OpenApi{
- servers: [
- # Populate the Server info from a phoenix endpoint
- OpenApiSpex.Server.from_endpoint(Endpoint)
- ],
+ servers:
+ if Phoenix.Endpoint.server?(:pleroma, Endpoint) do
+ [
+ # Populate the Server info from a phoenix endpoint
+ OpenApiSpex.Server.from_endpoint(Endpoint)
+ ]
+ else
+ []
+ end,
info: %OpenApiSpex.Info{
title: "Pleroma",
description: Application.spec(:pleroma, :description) |> to_string(),
diff --git a/lib/pleroma/web/api_spec/helpers.ex b/lib/pleroma/web/api_spec/helpers.ex
index 2a7f1a706..34de2ed57 100644
--- a/lib/pleroma/web/api_spec/helpers.ex
+++ b/lib/pleroma/web/api_spec/helpers.ex
@@ -72,7 +72,11 @@ def empty_object_response do
end
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
def no_content_response do
diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex
index de715a077..a0933ba52 100644
--- a/lib/pleroma/web/api_spec/operations/account_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/account_operation.ex
@@ -378,6 +378,10 @@ def identity_proofs_operation do
tags: ["accounts"],
summary: "Identity proofs",
operationId: "AccountController.identity_proofs",
+ # Validators complains about unused path params otherwise
+ parameters: [
+ %Reference{"$ref": "#/components/parameters/accountIdOrNickname"}
+ ],
description: "Not implemented",
responses: %{
200 => empty_array_response()
@@ -475,7 +479,6 @@ defp create_response do
identifier: %Schema{type: :string},
message: %Schema{type: :string}
},
- required: [],
# Note: example of successful registration with failed login response:
# example: %{
# "identifier" => "missing_confirmed_email",
@@ -536,7 +539,7 @@ defp update_credentials_request do
nullable: true,
oneOf: [
%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
diff --git a/lib/pleroma/web/api_spec/operations/admin/chat_operation.ex b/lib/pleroma/web/api_spec/operations/admin/chat_operation.ex
new file mode 100644
index 000000000..d3e5dfc1c
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/admin/chat_operation.ex
@@ -0,0 +1,96 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# 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
diff --git a/lib/pleroma/web/api_spec/operations/admin/instance_document_operation.ex b/lib/pleroma/web/api_spec/operations/admin/instance_document_operation.ex
new file mode 100644
index 000000000..a120ff4e8
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/admin/instance_document_operation.ex
@@ -0,0 +1,115 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# 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: "Instance panel
"
+ })
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex
index b1a0d26ab..56554d5b4 100644
--- a/lib/pleroma/web/api_spec/operations/chat_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex
@@ -184,7 +184,8 @@ def post_chat_message_operation do
"application/json",
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: [
%{
diff --git a/lib/pleroma/web/api_spec/operations/custom_emoji_operation.ex b/lib/pleroma/web/api_spec/operations/custom_emoji_operation.ex
index 2f812ac77..5ff263ceb 100644
--- a/lib/pleroma/web/api_spec/operations/custom_emoji_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/custom_emoji_operation.ex
@@ -69,7 +69,7 @@ defp custom_emoji do
type: :object,
properties: %{
category: %Schema{type: :string},
- tags: %Schema{type: :array}
+ tags: %Schema{type: :array, items: %Schema{type: :string}}
}
}
],
diff --git a/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex b/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex
index 1a49fece0..745d41f88 100644
--- a/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex
@@ -23,7 +23,7 @@ def index_operation do
parameters: [
Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
Operation.parameter(:emoji, :path, :string, "Filter by a single unicode emoji",
- required: false
+ required: nil
)
],
security: [%{"oAuth" => ["read:statuses"]}],
diff --git a/lib/pleroma/web/api_spec/operations/list_operation.ex b/lib/pleroma/web/api_spec/operations/list_operation.ex
index 15039052e..f6e73968a 100644
--- a/lib/pleroma/web/api_spec/operations/list_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/list_operation.ex
@@ -187,8 +187,7 @@ defp add_remove_accounts_request(required) when is_boolean(required) do
type: :object,
properties: %{
account_ids: %Schema{type: :array, description: "Array of account IDs", items: FlakeID}
- },
- required: required && [:account_ids]
+ }
},
required: required
)
diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex
index 6589a16f3..b3b6ceb68 100644
--- a/lib/pleroma/web/api_spec/operations/status_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/status_operation.ex
@@ -55,7 +55,7 @@ def create_operation do
"application/json",
%Schema{oneOf: [Status, ScheduledStatus]}
),
- 422 => Operation.response("Bad Request", "application/json", ApiError)
+ 422 => Operation.response("Bad Request / MRF Rejection", "application/json", ApiError)
}
}
end
diff --git a/lib/pleroma/web/api_spec/schemas/chat_message.ex b/lib/pleroma/web/api_spec/schemas/chat_message.ex
index bbf2a4427..9d2799618 100644
--- a/lib/pleroma/web/api_spec/schemas/chat_message.ex
+++ b/lib/pleroma/web/api_spec/schemas/chat_message.ex
@@ -4,6 +4,7 @@
defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessage do
alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.Emoji
require OpenApiSpex
@@ -18,7 +19,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessage do
chat_id: %Schema{type: :string},
content: %Schema{type: :string, nullable: true},
created_at: %Schema{type: :string, format: :"date-time"},
- emojis: %Schema{type: :array},
+ emojis: %Schema{type: :array, items: Emoji},
attachment: %Schema{type: :object, nullable: true},
card: %Schema{
type: :object,
diff --git a/lib/pleroma/web/api_spec/schemas/scheduled_status.ex b/lib/pleroma/web/api_spec/schemas/scheduled_status.ex
index 0520d0848..addefa9d3 100644
--- a/lib/pleroma/web/api_spec/schemas/scheduled_status.ex
+++ b/lib/pleroma/web/api_spec/schemas/scheduled_status.ex
@@ -27,9 +27,9 @@ defmodule Pleroma.Web.ApiSpec.Schemas.ScheduledStatus do
media_ids: %Schema{type: :array, nullable: true, items: %Schema{type: :string}},
sensitive: %Schema{type: :boolean, 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},
- poll: %Schema{type: Poll, nullable: true},
+ poll: %Schema{allOf: [Poll], nullable: true},
in_reply_to_id: %Schema{type: :string, nullable: true}
}
}
diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
index b49a95791..fca9246a5 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -48,6 +48,9 @@ def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ [])
local: true
)} do
{:ok, activity}
+ else
+ {:common_pipeline, {:reject, _} = e} -> e
+ e -> e
end
end
@@ -560,4 +563,21 @@ def hide_reblogs(%User{} = user, %User{} = target) do
def show_reblogs(%User{} = user, %User{} = target) do
UserRelationship.delete_reblog_mute(user, target)
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
diff --git a/lib/pleroma/web/fed_sockets/fed_registry.ex b/lib/pleroma/web/fed_sockets/fed_registry.ex
new file mode 100644
index 000000000..e00ea69c0
--- /dev/null
+++ b/lib/pleroma/web/fed_sockets/fed_registry.ex
@@ -0,0 +1,185 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# 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
diff --git a/lib/pleroma/web/fed_sockets/fed_socket.ex b/lib/pleroma/web/fed_sockets/fed_socket.ex
new file mode 100644
index 000000000..98d64e65a
--- /dev/null
+++ b/lib/pleroma/web/fed_sockets/fed_socket.ex
@@ -0,0 +1,137 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# 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
diff --git a/lib/pleroma/web/fed_sockets/fed_sockets.ex b/lib/pleroma/web/fed_sockets/fed_sockets.ex
new file mode 100644
index 000000000..1fd5899c8
--- /dev/null
+++ b/lib/pleroma/web/fed_sockets/fed_sockets.ex
@@ -0,0 +1,185 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# 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
diff --git a/lib/pleroma/web/fed_sockets/fetch_registry.ex b/lib/pleroma/web/fed_sockets/fetch_registry.ex
new file mode 100644
index 000000000..7897f0fc6
--- /dev/null
+++ b/lib/pleroma/web/fed_sockets/fetch_registry.ex
@@ -0,0 +1,151 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# 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
diff --git a/lib/pleroma/web/fed_sockets/incoming_handler.ex b/lib/pleroma/web/fed_sockets/incoming_handler.ex
new file mode 100644
index 000000000..49d0d9d84
--- /dev/null
+++ b/lib/pleroma/web/fed_sockets/incoming_handler.ex
@@ -0,0 +1,88 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# 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
diff --git a/lib/pleroma/web/fed_sockets/ingester_worker.ex b/lib/pleroma/web/fed_sockets/ingester_worker.ex
new file mode 100644
index 000000000..325f2a4ab
--- /dev/null
+++ b/lib/pleroma/web/fed_sockets/ingester_worker.ex
@@ -0,0 +1,33 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# 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
diff --git a/lib/pleroma/web/fed_sockets/outgoing_handler.ex b/lib/pleroma/web/fed_sockets/outgoing_handler.ex
new file mode 100644
index 000000000..6ddef17fe
--- /dev/null
+++ b/lib/pleroma/web/fed_sockets/outgoing_handler.ex
@@ -0,0 +1,146 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# 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
diff --git a/lib/pleroma/web/fed_sockets/socket_info.ex b/lib/pleroma/web/fed_sockets/socket_info.ex
new file mode 100644
index 000000000..d6fdffe1a
--- /dev/null
+++ b/lib/pleroma/web/fed_sockets/socket_info.ex
@@ -0,0 +1,52 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# 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
diff --git a/lib/pleroma/web/fed_sockets/supervisor.ex b/lib/pleroma/web/fed_sockets/supervisor.ex
new file mode 100644
index 000000000..a5f4bebfb
--- /dev/null
+++ b/lib/pleroma/web/fed_sockets/supervisor.ex
@@ -0,0 +1,59 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# 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
diff --git a/lib/pleroma/web/instance_document.ex b/lib/pleroma/web/instance_document.ex
new file mode 100644
index 000000000..df5caebf0
--- /dev/null
+++ b/lib/pleroma/web/instance_document.ex
@@ -0,0 +1,62 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# 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
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index d2a30a548..121ba1693 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -181,8 +181,10 @@ defp do_render("show.json", %{user: user} = opts) do
user = User.sanitize_html(user, User.html_filter_policy(opts[:for]))
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_static = User.banner_url(user) |> MediaProxy.preview_url(static: true)
following_count =
if !user.hide_follows_count or !user.hide_follows or opts[:for] == user do
@@ -247,10 +249,10 @@ defp do_render("show.json", %{user: user} = opts) do
statuses_count: user.note_count,
note: user.bio,
url: user.uri || user.ap_id,
- avatar: image,
- avatar_static: image,
+ avatar: avatar,
+ avatar_static: avatar_static,
header: header,
- header_static: header,
+ header_static: header_static,
emojis: emojis,
fields: user.fields,
bot: bot,
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index ca42917fc..435bcde15 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -55,23 +55,6 @@ defp get_replied_to_activities(activities) do
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),
do: context_id
@@ -119,7 +102,7 @@ def render("index.json", opts) do
# Note: unresolved users are filtered out
actors =
(activities ++ parent_activities)
- |> Enum.map(&get_user(&1.data["actor"], false))
+ |> Enum.map(&CommonAPI.get_user(&1.data["actor"], false))
|> Enum.filter(& &1)
UserRelationship.view_relationships_option(reading_user, actors, subset: :source_mutes)
@@ -138,7 +121,7 @@ def render(
"show.json",
%{activity: %{data: %{"type" => "Announce", "object" => _object}} = activity} = opts
) do
- user = get_user(activity.data["actor"])
+ user = CommonAPI.get_user(activity.data["actor"])
created_at = Utils.to_masto_date(activity.data["published"])
activity_object = Object.normalize(activity)
@@ -211,7 +194,7 @@ def render(
def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
object = Object.normalize(activity)
- user = get_user(activity.data["actor"])
+ user = CommonAPI.get_user(activity.data["actor"])
user_follower_address = user.follower_address
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_user = reply_to && get_user(reply_to.data["actor"])
+ reply_to_user = reply_to && CommonAPI.get_user(reply_to.data["actor"])
content =
object
@@ -432,6 +415,7 @@ def render("attachment.json", %{attachment: attachment}) do
[attachment_url | _] = attachment["url"]
media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
href = attachment_url["href"] |> MediaProxy.url()
+ href_preview = attachment_url["href"] |> MediaProxy.preview_url()
type =
cond do
@@ -447,7 +431,7 @@ def render("attachment.json", %{attachment: attachment}) do
id: to_string(attachment["id"] || hash_id),
url: href,
remote_url: href,
- preview_url: href,
+ preview_url: href_preview,
text_url: href,
type: type,
description: attachment["name"],
diff --git a/lib/pleroma/web/media_proxy/invalidation.ex b/lib/pleroma/web/media_proxy/invalidation.ex
index 5808861e6..4f4340478 100644
--- a/lib/pleroma/web/media_proxy/invalidation.ex
+++ b/lib/pleroma/web/media_proxy/invalidation.ex
@@ -33,6 +33,8 @@ defp do_purge(urls) do
def prepare_urls(urls) do
urls
|> 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
diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex
index e18dd8224..8656b8cad 100644
--- a/lib/pleroma/web/media_proxy/media_proxy.ex
+++ b/lib/pleroma/web/media_proxy/media_proxy.ex
@@ -4,6 +4,7 @@
defmodule Pleroma.Web.MediaProxy do
alias Pleroma.Config
+ alias Pleroma.Helpers.UriHelper
alias Pleroma.Upload
alias Pleroma.Web
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
- if disabled?() or not url_proxiable?(url) do
- url
- else
+ if enabled?() and url_proxiable?(url) do
encode_url(url)
+ else
+ url
end
end
@spec url_proxiable?(String.t()) :: boolean()
def url_proxiable?(url) do
- if local?(url) or whitelisted?(url) do
- false
+ not local?(url) and not whitelisted?(url)
+ end
+
+ def preview_url(url, preview_params \\ []) do
+ if preview_enabled?() do
+ encode_preview_url(url, preview_params)
else
- true
+ url(url)
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)
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
- def encode_url(url) do
+ defp base64_sig64(url) do
base64 = Base.url_encode64(url, @base64_opts)
sig64 =
base64
- |> signed_url
+ |> signed_url()
|> Base.url_encode64(@base64_opts)
+ {base64, sig64}
+ end
+
+ def encode_url(url) do
+ {base64, sig64} = base64_sig64(url)
+
build_url(sig64, base64, filename(url))
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
with {:ok, sig} <- Base.url_decode64(sig, @base64_opts),
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)
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()),
- "proxy",
+ base_url(),
+ path,
sig_base64,
url_base64,
filename
@@ -124,4 +149,38 @@ def build_url(sig_base64, url_base64, filename \\ nil) do
|> Enum.filter(& &1)
|> Path.join()
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
diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex
index 9a64b0ef3..90651ed9b 100644
--- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex
+++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex
@@ -5,44 +5,201 @@
defmodule Pleroma.Web.MediaProxy.MediaProxyController do
use Pleroma.Web, :controller
+ alias Pleroma.Config
+ alias Pleroma.Helpers.MediaHelper
+ alias Pleroma.Helpers.UriHelper
alias Pleroma.ReverseProxy
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} = params) do
- with config <- Pleroma.Config.get([:media_proxy], []),
- true <- Keyword.get(config, :enabled, false),
+ def remote(conn, %{"sig" => sig64, "url" => url64}) do
+ with {_, true} <- {:enabled, MediaProxy.enabled?()},
{:ok, url} <- MediaProxy.decode_url(sig64, url64),
{_, false} <- {:in_banned_urls, MediaProxy.in_banned_urls(url)},
- :ok <- filename_matches(params, conn.request_path, url) do
- ReverseProxy.call(conn, url, Keyword.get(config, :proxy_opts, @default_proxy_opts))
+ :ok <- MediaProxy.verify_request_path_and_url(conn, url) do
+ ReverseProxy.call(conn, url, media_proxy_opts())
else
- error when error in [false, {:in_banned_urls, true}] ->
- send_resp(conn, 404, Plug.Conn.Status.reason_phrase(404))
+ {:enabled, false} ->
+ 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} ->
- send_resp(conn, 403, Plug.Conn.Status.reason_phrase(403))
+ send_resp(conn, 403, Conn.Status.reason_phrase(403))
{:wrong_filename, filename} ->
redirect(conn, external: MediaProxy.build_url(sig64, url64, filename))
end
end
- def filename_matches(%{"filename" => _} = _, path, url) do
- filename = MediaProxy.filename(url)
-
- if filename && does_not_match(path, filename) do
- {:wrong_filename, filename}
+ def preview(%Conn{} = conn, %{"sig" => sig64, "url" => url64}) do
+ with {_, true} <- {:enabled, MediaProxy.preview_enabled?()},
+ {:ok, url} <- MediaProxy.decode_url(sig64, url64),
+ :ok <- MediaProxy.verify_request_path_and_url(conn, url) do
+ handle_preview(conn, url)
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
- def filename_matches(_, _, _), do: :ok
+ defp handle_preview(conn, url) do
+ media_proxy_url = MediaProxy.url(url)
- defp does_not_match(path, filename) do
- basename = Path.basename(path)
- basename != filename and URI.decode(basename) != filename and URI.encode(basename) != filename
+ with {:ok, %{status: status} = head_response} when status in 200..299 <-
+ Pleroma.HTTP.request("head", media_proxy_url, [], [], pool: :media) do
+ 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
diff --git a/lib/pleroma/web/metadata/restrict_indexing.ex b/lib/pleroma/web/metadata/restrict_indexing.ex
index f15607896..a1dcb6e15 100644
--- a/lib/pleroma/web/metadata/restrict_indexing.ex
+++ b/lib/pleroma/web/metadata/restrict_indexing.ex
@@ -10,7 +10,9 @@ defmodule Pleroma.Web.Metadata.Providers.RestrictIndexing do
"""
@impl true
- def build_tags(%{user: %{local: false}}) do
+ def build_tags(%{user: %{local: true, discoverable: true}}), do: []
+
+ def build_tags(_) do
[
{:meta,
[
@@ -19,7 +21,4 @@ def build_tags(%{user: %{local: false}}) do
], []}
]
end
-
- @impl true
- def build_tags(%{user: %{local: true}}), do: []
end
diff --git a/lib/pleroma/web/metadata/utils.ex b/lib/pleroma/web/metadata/utils.ex
index 2f0dfb474..8a206e019 100644
--- a/lib/pleroma/web/metadata/utils.ex
+++ b/lib/pleroma/web/metadata/utils.ex
@@ -38,7 +38,7 @@ def scrub_html(content) when is_binary(content) do
def scrub_html(content), do: content
def attachment_url(url) do
- MediaProxy.url(url)
+ MediaProxy.preview_url(url)
end
def user_name_string(user) do
diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex
index 26e68be42..a4152e840 100644
--- a/lib/pleroma/web/oauth/oauth_controller.ex
+++ b/lib/pleroma/web/oauth/oauth_controller.ex
@@ -119,7 +119,7 @@ defp handle_existing_authorization(
redirect_uri = redirect_uri(conn, redirect_uri)
url_params = %{access_token: token.token}
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)
else
conn
@@ -161,7 +161,7 @@ def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
redirect_uri = redirect_uri(conn, redirect_uri)
url_params = %{code: auth.token}
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)
else
conn
diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
index e8a1746d4..867cff829 100644
--- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
@@ -90,6 +90,16 @@ def post_chat_message(
conn
|> put_view(MessageReferenceView)
|> 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
@@ -146,11 +156,8 @@ def index(%{assigns: %{user: %{id: user_id} = user}} = conn, _params) do
blocked_ap_ids = User.blocked_users_ap_ids(user)
chats =
- from(c in Chat,
- where: c.user_id == ^user_id,
- where: c.recipient not in ^blocked_ap_ids,
- order_by: [desc: c.updated_at]
- )
+ Chat.for_user_query(user_id)
+ |> where([c], c.recipient not in ^blocked_ap_ids)
|> Repo.all()
conn
diff --git a/lib/pleroma/web/pleroma_api/views/scrobble_view.ex b/lib/pleroma/web/pleroma_api/views/scrobble_view.ex
index bbff93abe..95bd4c368 100644
--- a/lib/pleroma/web/pleroma_api/views/scrobble_view.ex
+++ b/lib/pleroma/web/pleroma_api/views/scrobble_view.ex
@@ -10,14 +10,14 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleView do
alias Pleroma.Activity
alias Pleroma.HTML
alias Pleroma.Object
+ alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.AccountView
- alias Pleroma.Web.MastodonAPI.StatusView
def render("show.json", %{activity: %Activity{data: %{"type" => "Listen"}} = activity} = opts) do
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"])
%{
diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex
index 33f6f1fa1..c70d2fdba 100644
--- a/lib/pleroma/web/rich_media/parser.ex
+++ b/lib/pleroma/web/rich_media/parser.ex
@@ -20,36 +20,61 @@ def parse(url) do
with {:ok, data} <- get_cached_or_parse(url),
{:ok, _} <- set_ttl_based_on_image(data, url) do
{: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
defp get_cached_or_parse(url) do
- case Cachex.fetch!(:rich_media_cache, url, fn _ -> {:commit, parse_url(url)} end) do
- {:ok, _data} = res ->
- res
+ case Cachex.fetch(:rich_media_cache, url, fn ->
+ case parse_url(url) do
+ {:ok, _} = res ->
+ {:commit, res}
- {:error, :body_too_large} = e ->
- e
+ {:error, reason} = 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 ->
- e
+ {:error, reason} = 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
- # with time
- {:error, _} = e ->
- ttl = Pleroma.Config.get([:rich_media, :failure_backoff], 60_000)
- Cachex.expire(:rich_media_cache, url, ttl)
- e
+ {:error, e} ->
+ {:error, {:cachex_error, e}}
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
@doc """
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index c6433cc53..67fbbde92 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -178,9 +178,14 @@ defmodule Pleroma.Web.Router do
get("/users", AdminAPIController, :list_users)
get("/users/:nickname", AdminAPIController, :user_show)
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("/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/resend_confirmation_email", AdminAPIController, :resend_confirmation_email)
@@ -214,6 +219,10 @@ defmodule Pleroma.Web.Router do
get("/media_proxy_caches", MediaProxyCacheController, :index)
post("/media_proxy_caches/delete", MediaProxyCacheController, :delete)
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
scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do
@@ -670,6 +679,8 @@ defmodule Pleroma.Web.Router do
end
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/:filename", MediaProxyController, :remote)
end
diff --git a/mix.lock b/mix.lock
index a28c47017..adb3f024a 100644
--- a/mix.lock
+++ b/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_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"},
+ "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"},
"esshd": {:hex, :esshd, "0.1.1", "d4dd4c46698093a40a56afecce8a46e246eb35463c457c246dacba2e056f31b5", [:mix], [], "hexpm", "d73e341e3009d390aa36387dc8862860bf9f874c94d9fd92ade2926376f49981"},
"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"]},
"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"]},
+ "p1_utils": {:hex, :p1_utils, "1.0.18", "3fe224de5b2e190d730a3c5da9d6e8540c96484cf4b4692921d1e28f0c32b01c", [:rebar3], [], "hexpm", "1fc8773a71a15553b179c986b22fbeead19b28fe486c332d4929700ffeb71f88"},
"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"},
"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"},
"unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm", "6c7729a2d214806450d29766abc2afaa7a2cbecf415be64f36a6691afebb50e5"},
"web_push_encryption": {:hex, :web_push_encryption, "0.3.0", "598b5135e696fd1404dc8d0d7c0fa2c027244a4e5d5e5a98ba267f14fdeaabc8", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "f10bdd1afe527ede694749fb77a2f22f146a51b054c7fa541c9fd920fba7c875"},
- "websocket_client": {:git, "https://github.com/jeremyong/websocket_client.git", "9a6f65d05ebf2725d62fb19262b21f1805a59fbf", []},
+ "websocket_client": {:git, "https://github.com/jeremyong/websocket_client.git", "9a6f65d05ebf2725d62fb19262b21f1805a59fbf", []}
}
diff --git a/priv/gettext/zh_Hans/LC_MESSAGES/errors.po b/priv/gettext/zh_Hans/LC_MESSAGES/errors.po
new file mode 100644
index 000000000..4f029d558
--- /dev/null
+++ b/priv/gettext/zh_Hans/LC_MESSAGES/errors.po
@@ -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 \n"
+"Language-Team: Chinese (Simplified) \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 ""
diff --git a/priv/static/index.html b/priv/static/index.html
index 6fa237768..f5690a8d6 100644
--- a/priv/static/index.html
+++ b/priv/static/index.html
@@ -1 +1 @@
-Pleroma
\ No newline at end of file
+Pleroma
\ No newline at end of file
diff --git a/priv/static/static/font/fontello.1599568314856.woff b/priv/static/static/font/fontello.1599568314856.woff
deleted file mode 100644
index 64f566383..000000000
Binary files a/priv/static/static/font/fontello.1599568314856.woff and /dev/null differ
diff --git a/priv/static/static/font/fontello.1599568314856.woff2 b/priv/static/static/font/fontello.1599568314856.woff2
deleted file mode 100644
index 972e70831..000000000
Binary files a/priv/static/static/font/fontello.1599568314856.woff2 and /dev/null differ
diff --git a/priv/static/static/font/fontello.1599568314856.eot b/priv/static/static/font/fontello.1600365488745.eot
similarity index 89%
rename from priv/static/static/font/fontello.1599568314856.eot
rename to priv/static/static/font/fontello.1600365488745.eot
index 1a6931a0e..255f50372 100644
Binary files a/priv/static/static/font/fontello.1599568314856.eot and b/priv/static/static/font/fontello.1600365488745.eot differ
diff --git a/priv/static/static/font/fontello.1599568314856.svg b/priv/static/static/font/fontello.1600365488745.svg
similarity index 98%
rename from priv/static/static/font/fontello.1599568314856.svg
rename to priv/static/static/font/fontello.1600365488745.svg
index 71b5d70af..9eddf62ea 100644
--- a/priv/static/static/font/fontello.1599568314856.svg
+++ b/priv/static/static/font/fontello.1600365488745.svg
@@ -92,6 +92,8 @@
+
+
diff --git a/priv/static/static/font/fontello.1599568314856.ttf b/priv/static/static/font/fontello.1600365488745.ttf
similarity index 89%
rename from priv/static/static/font/fontello.1599568314856.ttf
rename to priv/static/static/font/fontello.1600365488745.ttf
index 795464475..6bda99d50 100644
Binary files a/priv/static/static/font/fontello.1599568314856.ttf and b/priv/static/static/font/fontello.1600365488745.ttf differ
diff --git a/priv/static/static/font/fontello.1600365488745.woff b/priv/static/static/font/fontello.1600365488745.woff
new file mode 100644
index 000000000..11c866ae0
Binary files /dev/null and b/priv/static/static/font/fontello.1600365488745.woff differ
diff --git a/priv/static/static/font/fontello.1600365488745.woff2 b/priv/static/static/font/fontello.1600365488745.woff2
new file mode 100644
index 000000000..06151d28c
Binary files /dev/null and b/priv/static/static/font/fontello.1600365488745.woff2 differ
diff --git a/priv/static/static/fontello.1599568314856.css b/priv/static/static/fontello.1600365488745.css
similarity index 89%
rename from priv/static/static/fontello.1599568314856.css
rename to priv/static/static/fontello.1600365488745.css
index e636286c0..781ff7620 100644
Binary files a/priv/static/static/fontello.1599568314856.css and b/priv/static/static/fontello.1600365488745.css differ
diff --git a/priv/static/static/fontello.json b/priv/static/static/fontello.json
index 706800cdb..b0136fd90 100644
--- a/priv/static/static/fontello.json
+++ b/priv/static/static/fontello.json
@@ -405,6 +405,12 @@
"css": "block",
"code": 59434,
"src": "fontawesome"
+ },
+ {
+ "uid": "3e674995cacc2b09692c096ea7eb6165",
+ "css": "megaphone",
+ "code": 59435,
+ "src": "fontawesome"
}
]
}
\ No newline at end of file
diff --git a/priv/static/static/js/2.c92f4803ff24726cea58.js.map b/priv/static/static/js/2.c92f4803ff24726cea58.js.map
deleted file mode 100644
index e3cc6a3fb..000000000
Binary files a/priv/static/static/js/2.c92f4803ff24726cea58.js.map and /dev/null differ
diff --git a/priv/static/static/js/2.c92f4803ff24726cea58.js b/priv/static/static/js/2.e852a6b4b3bba752b838.js
similarity index 66%
rename from priv/static/static/js/2.c92f4803ff24726cea58.js
rename to priv/static/static/js/2.e852a6b4b3bba752b838.js
index 55aa1f44e..42e446575 100644
Binary files a/priv/static/static/js/2.c92f4803ff24726cea58.js and b/priv/static/static/js/2.e852a6b4b3bba752b838.js differ
diff --git a/priv/static/static/js/2.e852a6b4b3bba752b838.js.map b/priv/static/static/js/2.e852a6b4b3bba752b838.js.map
new file mode 100644
index 000000000..d698f09e1
Binary files /dev/null and b/priv/static/static/js/2.e852a6b4b3bba752b838.js.map differ
diff --git a/priv/static/static/js/app.55d173dc5e39519aa518.js b/priv/static/static/js/app.55d173dc5e39519aa518.js
deleted file mode 100644
index d04ae3499..000000000
Binary files a/priv/static/static/js/app.55d173dc5e39519aa518.js and /dev/null differ
diff --git a/priv/static/static/js/app.55d173dc5e39519aa518.js.map b/priv/static/static/js/app.55d173dc5e39519aa518.js.map
deleted file mode 100644
index 600e97afa..000000000
Binary files a/priv/static/static/js/app.55d173dc5e39519aa518.js.map and /dev/null differ
diff --git a/priv/static/static/js/app.826c44232e0a76bbd9ba.js b/priv/static/static/js/app.826c44232e0a76bbd9ba.js
new file mode 100644
index 000000000..16762165e
Binary files /dev/null and b/priv/static/static/js/app.826c44232e0a76bbd9ba.js differ
diff --git a/priv/static/static/js/app.826c44232e0a76bbd9ba.js.map b/priv/static/static/js/app.826c44232e0a76bbd9ba.js.map
new file mode 100644
index 000000000..b188c3379
Binary files /dev/null and b/priv/static/static/js/app.826c44232e0a76bbd9ba.js.map differ
diff --git a/priv/static/sw-pleroma.js b/priv/static/sw-pleroma.js
index 54c6ed8f0..fa4969025 100644
Binary files a/priv/static/sw-pleroma.js and b/priv/static/sw-pleroma.js differ
diff --git a/test/fixtures/custom_instance_panel.html b/test/fixtures/custom_instance_panel.html
new file mode 100644
index 000000000..6371a1e43
--- /dev/null
+++ b/test/fixtures/custom_instance_panel.html
@@ -0,0 +1 @@
+Custom instance panel
\ No newline at end of file
diff --git a/test/fixtures/image.gif b/test/fixtures/image.gif
new file mode 100755
index 000000000..9df64778b
Binary files /dev/null and b/test/fixtures/image.gif differ
diff --git a/test/fixtures/image.png b/test/fixtures/image.png
new file mode 100755
index 000000000..e999e8800
Binary files /dev/null and b/test/fixtures/image.png differ
diff --git a/test/fixtures/tesla_mock/admin@mastdon.example.org.json b/test/fixtures/tesla_mock/admin@mastdon.example.org.json
index a911b979a..f961ccb36 100644
--- a/test/fixtures/tesla_mock/admin@mastdon.example.org.json
+++ b/test/fixtures/tesla_mock/admin@mastdon.example.org.json
@@ -1,20 +1,24 @@
{
- "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", {
- "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
- "sensitive": "as:sensitive",
- "movedTo": "as:movedTo",
- "Hashtag": "as:Hashtag",
- "ostatus": "http://ostatus.org#",
- "atomUri": "ostatus:atomUri",
- "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
- "conversation": "ostatus:conversation",
- "toot": "http://joinmastodon.org/ns#",
- "Emoji": "toot:Emoji",
- "alsoKnownAs": {
- "@id": "as:alsoKnownAs",
- "@type": "@id"
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ {
+ "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+ "sensitive": "as:sensitive",
+ "movedTo": "as:movedTo",
+ "Hashtag": "as:Hashtag",
+ "ostatus": "http://ostatus.org#",
+ "atomUri": "ostatus:atomUri",
+ "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
+ "conversation": "ostatus:conversation",
+ "toot": "http://joinmastodon.org/ns#",
+ "Emoji": "toot:Emoji",
+ "alsoKnownAs": {
+ "@id": "as:alsoKnownAs",
+ "@type": "@id"
+ }
}
- }],
+ ],
"id": "http://mastodon.example.org/users/admin",
"type": "Person",
"following": "http://mastodon.example.org/users/admin/following",
@@ -23,6 +27,7 @@
"outbox": "http://mastodon.example.org/users/admin/outbox",
"preferredUsername": "admin",
"name": null,
+ "discoverable": "true",
"summary": "\u003cp\u003e\u003c/p\u003e",
"url": "http://mastodon.example.org/@admin",
"manuallyApprovesFollowers": false,
@@ -34,7 +39,8 @@
"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"
},
- "attachment": [{
+ "attachment": [
+ {
"type": "PropertyValue",
"name": "foo",
"value": "bar"
@@ -58,5 +64,7 @@
"mediaType": "image/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"
+ ]
+}
\ No newline at end of file
diff --git a/test/fixtures/tesla_mock/https___osada.macgirvin.com_channel_mike.json b/test/fixtures/tesla_mock/https___osada.macgirvin.com_channel_mike.json
index c42f3a53c..ca76d6e17 100644
--- a/test/fixtures/tesla_mock/https___osada.macgirvin.com_channel_mike.json
+++ b/test/fixtures/tesla_mock/https___osada.macgirvin.com_channel_mike.json
@@ -8,6 +8,7 @@
"preferredUsername": "mike",
"name": "Mike Macgirvin (Osada)",
"updated": "2018-08-29T03:09:11Z",
+ "discoverable": "true",
"icon": {
"type": "Image",
"mediaType": "image/jpeg",
@@ -51,4 +52,4 @@
"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="
}
-}
+}
\ No newline at end of file
diff --git a/test/support/factory.ex b/test/support/factory.ex
index 2fdfabbc5..fb82be0c4 100644
--- a/test/support/factory.ex
+++ b/test/support/factory.ex
@@ -31,6 +31,7 @@ def user_factory do
nickname: sequence(:nickname, &"nick#{&1}"),
password_hash: Pbkdf2.hash_pwd_salt("test"),
bio: sequence(:bio, &"Tester Number #{&1}"),
+ discoverable: true,
last_digest_emailed_at: NaiveDateTime.utc_now(),
last_refreshed_at: NaiveDateTime.utc_now(),
notification_settings: %Pleroma.User.NotificationSetting{},
diff --git a/test/tasks/config_test.exs b/test/tasks/config_test.exs
index fb12e7fb3..f36648829 100644
--- a/test/tasks/config_test.exs
+++ b/test/tasks/config_test.exs
@@ -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)
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
assert Repo.all(ConfigDB) == []
diff --git a/test/user_search_test.exs b/test/user_search_test.exs
index 01976bf58..8529ce6db 100644
--- a/test/user_search_test.exs
+++ b/test/user_search_test.exs
@@ -25,6 +25,14 @@ test "excludes invisible users from results" do
assert found_user.id == user.id
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
insert(:user, actor_type: "Application", nickname: "user1")
service = insert(:user, actor_type: "Service", nickname: "user2")
diff --git a/test/web/activity_pub/mrf/mediaproxy_warming_policy_test.exs b/test/web/activity_pub/mrf/mediaproxy_warming_policy_test.exs
index 313d59a66..1710c4d2a 100644
--- a/test/web/activity_pub/mrf/mediaproxy_warming_policy_test.exs
+++ b/test/web/activity_pub/mrf/mediaproxy_warming_policy_test.exs
@@ -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
with_mock HTTP, get: fn _, _, _ -> {:ok, []} end do
MediaProxyWarmingPolicy.filter(@message)
diff --git a/test/web/activity_pub/pipeline_test.exs b/test/web/activity_pub/pipeline_test.exs
index f2a231eaf..210a06563 100644
--- a/test/web/activity_pub/pipeline_test.exs
+++ b/test/web/activity_pub/pipeline_test.exs
@@ -26,7 +26,7 @@ test "when given an `object_data` in meta, Federation will receive a the origina
{
Pleroma.Web.ActivityPub.MRF,
[],
- [filter: fn o -> {:ok, o} end]
+ [pipeline_filter: fn o, m -> {:ok, o, m} end]
},
{
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)
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.SideEffects.handle(activity, meta))
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,
[],
- [filter: fn o -> {:ok, o} end]
+ [pipeline_filter: fn o, m -> {:ok, o, m} end]
},
{
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)
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.SideEffects.handle(activity, meta))
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,
[],
- [filter: fn o -> {:ok, o} end]
+ [pipeline_filter: fn o, m -> {:ok, o, m} end]
},
{
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)
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.SideEffects.handle(activity, meta))
end
@@ -148,7 +148,7 @@ test "it goes through validation, filtering, persisting, side effects without fe
{
Pleroma.Web.ActivityPub.MRF,
[],
- [filter: fn o -> {:ok, o} end]
+ [pipeline_filter: fn o, m -> {:ok, o, m} end]
},
{
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)
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.SideEffects.handle(activity, meta))
end
diff --git a/test/web/admin_api/controllers/admin_api_controller_test.exs b/test/web/admin_api/controllers/admin_api_controller_test.exs
index 3bc88c6a9..e4d3512de 100644
--- a/test/web/admin_api/controllers/admin_api_controller_test.exs
+++ b/test/web/admin_api/controllers/admin_api_controller_test.exs
@@ -1510,6 +1510,56 @@ test "excludes reblogs by default", %{conn: conn, user: user} do
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
setup do
moderator = insert(:user, is_moderator: true)
diff --git a/test/web/admin_api/controllers/chat_controller_test.exs b/test/web/admin_api/controllers/chat_controller_test.exs
new file mode 100644
index 000000000..bd4c9c9d1
--- /dev/null
+++ b/test/web/admin_api/controllers/chat_controller_test.exs
@@ -0,0 +1,219 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# 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
diff --git a/test/web/admin_api/controllers/instance_document_controller_test.exs b/test/web/admin_api/controllers/instance_document_controller_test.exs
new file mode 100644
index 000000000..5f7b042f6
--- /dev/null
+++ b/test/web/admin_api/controllers/instance_document_controller_test.exs
@@ -0,0 +1,106 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.AdminAPI.InstanceDocumentControllerTest do
+ use Pleroma.Web.ConnCase, async: true
+ import Pleroma.Factory
+ alias Pleroma.Config
+
+ @dir "test/tmp/instance_static"
+ @default_instance_panel ~s(Welcome to Pleroma!
)
+
+ setup do
+ File.mkdir_p!(@dir)
+ on_exit(fn -> File.rm_rf(@dir) end)
+ end
+
+ setup do: clear_config([:instance, :static_dir], @dir)
+
+ 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 "GET /api/pleroma/admin/instance_document/:name" do
+ test "return the instance document url", %{conn: conn} do
+ conn = get(conn, "/api/pleroma/admin/instance_document/instance-panel")
+
+ assert content = html_response(conn, 200)
+ assert String.contains?(content, @default_instance_panel)
+ end
+
+ test "it returns 403 if requested by a non-admin" do
+ non_admin_user = insert(:user)
+ token = insert(:oauth_token, user: non_admin_user)
+
+ conn =
+ build_conn()
+ |> assign(:user, non_admin_user)
+ |> assign(:token, token)
+ |> get("/api/pleroma/admin/instance_document/instance-panel")
+
+ assert json_response(conn, :forbidden)
+ end
+
+ test "it returns 404 if the instance document with the given name doesn't exist", %{
+ conn: conn
+ } do
+ conn = get(conn, "/api/pleroma/admin/instance_document/1234")
+
+ assert json_response_and_validate_schema(conn, 404)
+ end
+ end
+
+ describe "PATCH /api/pleroma/admin/instance_document/:name" do
+ test "uploads the instance document", %{conn: conn} do
+ image = %Plug.Upload{
+ content_type: "text/html",
+ path: Path.absname("test/fixtures/custom_instance_panel.html"),
+ filename: "custom_instance_panel.html"
+ }
+
+ conn =
+ conn
+ |> put_req_header("content-type", "multipart/form-data")
+ |> patch("/api/pleroma/admin/instance_document/instance-panel", %{
+ "file" => image
+ })
+
+ assert %{"url" => url} = json_response_and_validate_schema(conn, 200)
+ index = get(build_conn(), url)
+ assert html_response(index, 200) == "Custom instance panel
"
+ end
+ end
+
+ describe "DELETE /api/pleroma/admin/instance_document/:name" do
+ test "deletes the instance document", %{conn: conn} do
+ File.mkdir!(@dir <> "/instance/")
+ File.write!(@dir <> "/instance/panel.html", "Custom instance panel")
+
+ conn_resp =
+ conn
+ |> get("/api/pleroma/admin/instance_document/instance-panel")
+
+ assert html_response(conn_resp, 200) == "Custom instance panel"
+
+ conn
+ |> delete("/api/pleroma/admin/instance_document/instance-panel")
+ |> json_response_and_validate_schema(200)
+
+ conn_resp =
+ conn
+ |> get("/api/pleroma/admin/instance_document/instance-panel")
+
+ assert content = html_response(conn_resp, 200)
+ assert String.contains?(content, @default_instance_panel)
+ end
+ end
+end
diff --git a/test/web/admin_api/search_test.exs b/test/web/admin_api/search_test.exs
index b974cedd5..d88867c52 100644
--- a/test/web/admin_api/search_test.exs
+++ b/test/web/admin_api/search_test.exs
@@ -177,5 +177,14 @@ test "it returns unapproved user" do
assert total == 3
assert count == 1
end
+
+ test "it returns non-discoverable users" do
+ insert(:user)
+ insert(:user, discoverable: false)
+
+ {:ok, _results, total} = Search.user()
+
+ assert total == 2
+ end
end
end
diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs
index bdc569e64..bf4353a57 100644
--- a/test/web/common_api/common_api_test.exs
+++ b/test/web/common_api/common_api_test.exs
@@ -217,6 +217,17 @@ test "it reject messages over the local limit" do
assert message == :content_too_long
end
+
+ test "it reject messages via MRF" do
+ clear_config([:mrf_keyword, :reject], ["GNO"])
+ clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy])
+
+ author = insert(:user)
+ recipient = insert(:user)
+
+ assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} ==
+ CommonAPI.post_chat_message(author, recipient, "GNO/Linux")
+ end
end
describe "unblocking" do
@@ -1209,4 +1220,24 @@ test "respects visibility=private" do
assert Visibility.get_visibility(activity) == "private"
end
end
+
+ describe "get_user/1" do
+ test "gets user by ap_id" do
+ user = insert(:user)
+ assert CommonAPI.get_user(user.ap_id) == user
+ end
+
+ test "gets user by guessed nickname" do
+ user = insert(:user, ap_id: "", nickname: "mario@mushroom.kingdom")
+ assert CommonAPI.get_user("https://mushroom.kingdom/users/mario") == user
+ end
+
+ test "fallback" do
+ assert %User{
+ name: "",
+ ap_id: "",
+ nickname: "erroruser@example.com"
+ } = CommonAPI.get_user("")
+ end
+ end
end
diff --git a/test/web/fed_sockets/fed_registry_test.exs b/test/web/fed_sockets/fed_registry_test.exs
new file mode 100644
index 000000000..19ac874d6
--- /dev/null
+++ b/test/web/fed_sockets/fed_registry_test.exs
@@ -0,0 +1,124 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.FedSockets.FedRegistryTest do
+ use ExUnit.Case
+
+ alias Pleroma.Web.FedSockets
+ alias Pleroma.Web.FedSockets.FedRegistry
+ alias Pleroma.Web.FedSockets.SocketInfo
+
+ @good_domain "http://good.domain"
+ @good_domain_origin "good.domain:80"
+
+ setup do
+ start_supervised({Pleroma.Web.FedSockets.Supervisor, []})
+ build_test_socket(@good_domain)
+ Process.sleep(10)
+
+ :ok
+ end
+
+ describe "add_fed_socket/1 without conflicting sockets" do
+ test "can be added" do
+ Process.sleep(10)
+ assert {:ok, %SocketInfo{origin: origin}} = FedRegistry.get_fed_socket(@good_domain_origin)
+ assert origin == "good.domain:80"
+ end
+
+ test "multiple origins can be added" do
+ build_test_socket("http://anothergood.domain")
+ Process.sleep(10)
+
+ assert {:ok, %SocketInfo{origin: origin_1}} =
+ FedRegistry.get_fed_socket(@good_domain_origin)
+
+ assert {:ok, %SocketInfo{origin: origin_2}} =
+ FedRegistry.get_fed_socket("anothergood.domain:80")
+
+ assert origin_1 == "good.domain:80"
+ assert origin_2 == "anothergood.domain:80"
+ assert FedRegistry.list_all() |> Enum.count() == 2
+ end
+ end
+
+ describe "add_fed_socket/1 when duplicate sockets conflict" do
+ setup do
+ build_test_socket(@good_domain)
+ build_test_socket(@good_domain)
+ Process.sleep(10)
+ :ok
+ end
+
+ test "will be ignored" do
+ assert {:ok, %SocketInfo{origin: origin, pid: pid_one}} =
+ FedRegistry.get_fed_socket(@good_domain_origin)
+
+ assert origin == "good.domain:80"
+
+ assert FedRegistry.list_all() |> Enum.count() == 1
+ end
+
+ test "the newer process will be closed" do
+ pid_two = build_test_socket(@good_domain)
+
+ assert {:ok, %SocketInfo{origin: origin, pid: pid_one}} =
+ FedRegistry.get_fed_socket(@good_domain_origin)
+
+ assert origin == "good.domain:80"
+ Process.sleep(10)
+
+ refute Process.alive?(pid_two)
+
+ assert FedRegistry.list_all() |> Enum.count() == 1
+ end
+ end
+
+ describe "get_fed_socket/1" do
+ test "returns missing for unknown hosts" do
+ assert {:error, :missing} = FedRegistry.get_fed_socket("not_a_dmoain")
+ end
+
+ test "returns rejected for hosts previously rejected" do
+ "rejected.domain:80"
+ |> FedSockets.uri_for_origin()
+ |> FedRegistry.set_host_rejected()
+
+ assert {:error, :rejected} = FedRegistry.get_fed_socket("rejected.domain:80")
+ end
+
+ test "can retrieve a previously added SocketInfo" do
+ build_test_socket(@good_domain)
+ Process.sleep(10)
+ assert {:ok, %SocketInfo{origin: origin}} = FedRegistry.get_fed_socket(@good_domain_origin)
+ assert origin == "good.domain:80"
+ end
+
+ test "removes references to SocketInfos when the process crashes" do
+ assert {:ok, %SocketInfo{origin: origin, pid: pid}} =
+ FedRegistry.get_fed_socket(@good_domain_origin)
+
+ assert origin == "good.domain:80"
+
+ Process.exit(pid, :testing)
+ Process.sleep(100)
+ assert {:error, :missing} = FedRegistry.get_fed_socket(@good_domain_origin)
+ end
+ end
+
+ def build_test_socket(uri) do
+ Kernel.spawn(fn -> fed_socket_almost(uri) end)
+ end
+
+ def fed_socket_almost(origin) do
+ FedRegistry.add_fed_socket(origin)
+
+ receive do
+ :close ->
+ :ok
+ after
+ 5_000 -> :timeout
+ end
+ end
+end
diff --git a/test/web/fed_sockets/fetch_registry_test.exs b/test/web/fed_sockets/fetch_registry_test.exs
new file mode 100644
index 000000000..7bd2d995a
--- /dev/null
+++ b/test/web/fed_sockets/fetch_registry_test.exs
@@ -0,0 +1,67 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.FedSockets.FetchRegistryTest do
+ use ExUnit.Case
+
+ alias Pleroma.Web.FedSockets.FetchRegistry
+ alias Pleroma.Web.FedSockets.FetchRegistry.FetchRegistryData
+
+ @json_message "hello"
+ @json_reply "hello back"
+
+ setup do
+ start_supervised(
+ {Pleroma.Web.FedSockets.Supervisor,
+ [
+ ping_interval: 8,
+ connection_duration: 15,
+ rejection_duration: 5,
+ fed_socket_fetches: [default: 10, interval: 10]
+ ]}
+ )
+
+ :ok
+ end
+
+ test "fetches can be stored" do
+ uuid = FetchRegistry.register_fetch(@json_message)
+
+ assert {:error, :waiting} = FetchRegistry.check_fetch(uuid)
+ end
+
+ test "fetches can return" do
+ uuid = FetchRegistry.register_fetch(@json_message)
+ task = Task.async(fn -> FetchRegistry.register_fetch_received(uuid, @json_reply) end)
+
+ assert {:error, :waiting} = FetchRegistry.check_fetch(uuid)
+ Task.await(task)
+
+ assert {:ok, %FetchRegistryData{received_json: received_json}} =
+ FetchRegistry.check_fetch(uuid)
+
+ assert received_json == @json_reply
+ end
+
+ test "fetches are deleted once popped from stack" do
+ uuid = FetchRegistry.register_fetch(@json_message)
+ task = Task.async(fn -> FetchRegistry.register_fetch_received(uuid, @json_reply) end)
+ Task.await(task)
+
+ assert {:ok, %FetchRegistryData{received_json: received_json}} =
+ FetchRegistry.check_fetch(uuid)
+
+ assert received_json == @json_reply
+ assert {:ok, @json_reply} = FetchRegistry.pop_fetch(uuid)
+
+ assert {:error, :missing} = FetchRegistry.check_fetch(uuid)
+ end
+
+ test "fetches can time out" do
+ uuid = FetchRegistry.register_fetch(@json_message)
+ assert {:error, :waiting} = FetchRegistry.check_fetch(uuid)
+ Process.sleep(500)
+ assert {:error, :missing} = FetchRegistry.check_fetch(uuid)
+ end
+end
diff --git a/test/web/fed_sockets/socket_info_test.exs b/test/web/fed_sockets/socket_info_test.exs
new file mode 100644
index 000000000..db3d6edcd
--- /dev/null
+++ b/test/web/fed_sockets/socket_info_test.exs
@@ -0,0 +1,118 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.FedSockets.SocketInfoTest do
+ use ExUnit.Case
+
+ alias Pleroma.Web.FedSockets
+ alias Pleroma.Web.FedSockets.SocketInfo
+
+ describe "uri_for_origin" do
+ test "provides the fed_socket URL given the origin information" do
+ endpoint = "example.com:4000"
+ assert FedSockets.uri_for_origin(endpoint) =~ "ws://"
+ assert FedSockets.uri_for_origin(endpoint) =~ endpoint
+ end
+ end
+
+ describe "origin" do
+ test "will provide the origin field given a url" do
+ endpoint = "example.com:4000"
+ assert SocketInfo.origin("ws://#{endpoint}") == endpoint
+ assert SocketInfo.origin("http://#{endpoint}") == endpoint
+ assert SocketInfo.origin("https://#{endpoint}") == endpoint
+ end
+
+ test "will proide the origin field given a uri" do
+ endpoint = "example.com:4000"
+ uri = URI.parse("http://#{endpoint}")
+
+ assert SocketInfo.origin(uri) == endpoint
+ end
+ end
+
+ describe "touch" do
+ test "will update the TTL" do
+ endpoint = "example.com:4000"
+ socket = SocketInfo.build("ws://#{endpoint}")
+ Process.sleep(2)
+ touched_socket = SocketInfo.touch(socket)
+
+ assert socket.connected_until < touched_socket.connected_until
+ end
+ end
+
+ describe "expired?" do
+ setup do
+ start_supervised(
+ {Pleroma.Web.FedSockets.Supervisor,
+ [
+ ping_interval: 8,
+ connection_duration: 5,
+ rejection_duration: 5,
+ fed_socket_rejections: [lazy: true]
+ ]}
+ )
+
+ :ok
+ end
+
+ test "tests if the TTL is exceeded" do
+ endpoint = "example.com:4000"
+ socket = SocketInfo.build("ws://#{endpoint}")
+ refute SocketInfo.expired?(socket)
+ Process.sleep(10)
+
+ assert SocketInfo.expired?(socket)
+ end
+ end
+
+ describe "creating outgoing connection records" do
+ test "can be passed a string" do
+ assert %{conn_pid: :pid, origin: _origin} = SocketInfo.build("example.com:4000", :pid)
+ end
+
+ test "can be passed a URI" do
+ uri = URI.parse("http://example.com:4000")
+ assert %{conn_pid: :pid, origin: origin} = SocketInfo.build(uri, :pid)
+ assert origin =~ "example.com:4000"
+ end
+
+ test "will include the port number" do
+ assert %{conn_pid: :pid, origin: origin} = SocketInfo.build("http://example.com:4000", :pid)
+
+ assert origin =~ ":4000"
+ end
+
+ test "will provide the port if missing" do
+ assert %{conn_pid: :pid, origin: "example.com:80"} =
+ SocketInfo.build("http://example.com", :pid)
+
+ assert %{conn_pid: :pid, origin: "example.com:443"} =
+ SocketInfo.build("https://example.com", :pid)
+ end
+ end
+
+ describe "creating incoming connection records" do
+ test "can be passed a string" do
+ assert %{pid: _, origin: _origin} = SocketInfo.build("example.com:4000")
+ end
+
+ test "can be passed a URI" do
+ uri = URI.parse("example.com:4000")
+ assert %{pid: _, origin: _origin} = SocketInfo.build(uri)
+ end
+
+ test "will include the port number" do
+ assert %{pid: _, origin: origin} = SocketInfo.build("http://example.com:4000")
+
+ assert origin =~ ":4000"
+ end
+
+ test "will provide the port if missing" do
+ assert %{pid: _, origin: "example.com:80"} = SocketInfo.build("http://example.com")
+ assert %{pid: _, origin: "example.com:443"} = SocketInfo.build("https://example.com")
+ end
+ end
+end
diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs
index 1101d0952..b5ef1a49f 100644
--- a/test/web/mastodon_api/views/account_view_test.exs
+++ b/test/web/mastodon_api/views/account_view_test.exs
@@ -5,6 +5,7 @@
defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
use Pleroma.DataCase
+ alias Pleroma.Config
alias Pleroma.User
alias Pleroma.UserRelationship
alias Pleroma.Web.CommonAPI
@@ -68,7 +69,7 @@ test "Represent a user account" do
sensitive: false,
pleroma: %{
actor_type: "Person",
- discoverable: false
+ discoverable: true
},
fields: []
},
@@ -166,7 +167,7 @@ test "Represent a Service(bot) account" do
sensitive: false,
pleroma: %{
actor_type: "Service",
- discoverable: false
+ discoverable: true
},
fields: []
},
@@ -540,8 +541,9 @@ test "shows non-zero when historical unapproved requests are present" do
end
end
- test "uses mediaproxy urls when it's enabled" do
+ test "uses mediaproxy urls when it's enabled (regardless of media preview proxy state)" do
clear_config([:media_proxy, :enabled], true)
+ clear_config([:media_preview_proxy, :enabled])
user =
insert(:user,
@@ -550,20 +552,24 @@ test "uses mediaproxy urls when it's enabled" do
emoji: %{"joker_smile" => "https://evil.website/society.png"}
)
- AccountView.render("show.json", %{user: user, skip_visibility_check: true})
- |> Enum.all?(fn
- {key, url} when key in [:avatar, :avatar_static, :header, :header_static] ->
- String.starts_with?(url, Pleroma.Web.base_url())
+ with media_preview_enabled <- [false, true] do
+ Config.put([:media_preview_proxy, :enabled], media_preview_enabled)
- {:emojis, emojis} ->
- Enum.all?(emojis, fn %{url: url, static_url: static_url} ->
- String.starts_with?(url, Pleroma.Web.base_url()) &&
- String.starts_with?(static_url, Pleroma.Web.base_url())
- end)
+ AccountView.render("show.json", %{user: user, skip_visibility_check: true})
+ |> Enum.all?(fn
+ {key, url} when key in [:avatar, :avatar_static, :header, :header_static] ->
+ String.starts_with?(url, Pleroma.Web.base_url())
- _ ->
- true
- end)
- |> assert()
+ {:emojis, emojis} ->
+ Enum.all?(emojis, fn %{url: url, static_url: static_url} ->
+ String.starts_with?(url, Pleroma.Web.base_url()) &&
+ String.starts_with?(static_url, Pleroma.Web.base_url())
+ end)
+
+ _ ->
+ true
+ end)
+ |> assert()
+ end
end
end
diff --git a/test/web/media_proxy/media_proxy_controller_test.exs b/test/web/media_proxy/media_proxy_controller_test.exs
index d4db44c63..33e6873f7 100644
--- a/test/web/media_proxy/media_proxy_controller_test.exs
+++ b/test/web/media_proxy/media_proxy_controller_test.exs
@@ -8,34 +8,34 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
import Mock
alias Pleroma.Web.MediaProxy
- alias Pleroma.Web.MediaProxy.MediaProxyController
alias Plug.Conn
setup do
on_exit(fn -> Cachex.clear(:banned_urls_cache) end)
end
- test "it returns 404 when MediaProxy disabled", %{conn: conn} do
- clear_config([:media_proxy, :enabled], false)
-
- assert %Conn{
- status: 404,
- resp_body: "Not Found"
- } = get(conn, "/proxy/hhgfh/eeeee")
-
- assert %Conn{
- status: 404,
- resp_body: "Not Found"
- } = get(conn, "/proxy/hhgfh/eeee/fff")
- end
-
- describe "" do
+ describe "Media Proxy" do
setup do
clear_config([:media_proxy, :enabled], true)
clear_config([Pleroma.Web.Endpoint, :secret_key_base], "00000000000")
+
[url: MediaProxy.encode_url("https://google.fn/test.png")]
end
+ test "it returns 404 when disabled", %{conn: conn} do
+ clear_config([:media_proxy, :enabled], false)
+
+ assert %Conn{
+ status: 404,
+ resp_body: "Not Found"
+ } = get(conn, "/proxy/hhgfh/eeeee")
+
+ assert %Conn{
+ status: 404,
+ resp_body: "Not Found"
+ } = get(conn, "/proxy/hhgfh/eeee/fff")
+ end
+
test "it returns 403 for invalid signature", %{conn: conn, url: url} do
Pleroma.Config.put([Pleroma.Web.Endpoint, :secret_key_base], "000")
%{path: path} = URI.parse(url)
@@ -56,7 +56,7 @@ test "it returns 403 for invalid signature", %{conn: conn, url: url} do
} = get(conn, "/proxy/hhgfh/eeee/fff")
end
- test "redirects on valid url when filename is invalidated", %{conn: conn, url: url} do
+ test "redirects to valid url when filename is invalidated", %{conn: conn, url: url} do
invalid_url = String.replace(url, "test.png", "test-file.png")
response = get(conn, invalid_url)
assert response.status == 302
@@ -80,42 +80,248 @@ test "it returns 404 when url is in banned_urls cache", %{conn: conn, url: url}
end
end
- describe "filename_matches/3" do
- test "preserves the encoded or decoded path" do
- assert MediaProxyController.filename_matches(
- %{"filename" => "/Hello world.jpg"},
- "/Hello world.jpg",
- "http://pleroma.social/Hello world.jpg"
- ) == :ok
+ describe "Media Preview Proxy" do
+ setup do
+ clear_config([:media_proxy, :enabled], true)
+ clear_config([:media_preview_proxy, :enabled], true)
+ clear_config([Pleroma.Web.Endpoint, :secret_key_base], "00000000000")
- assert MediaProxyController.filename_matches(
- %{"filename" => "/Hello%20world.jpg"},
- "/Hello%20world.jpg",
- "http://pleroma.social/Hello%20world.jpg"
- ) == :ok
+ original_url = "https://google.fn/test.png"
- assert MediaProxyController.filename_matches(
- %{"filename" => "/my%2Flong%2Furl%2F2019%2F07%2FS.jpg"},
- "/my%2Flong%2Furl%2F2019%2F07%2FS.jpg",
- "http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg"
- ) == :ok
-
- assert MediaProxyController.filename_matches(
- %{"filename" => "/my%2Flong%2Furl%2F2019%2F07%2FS.jp"},
- "/my%2Flong%2Furl%2F2019%2F07%2FS.jp",
- "http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg"
- ) == {:wrong_filename, "my%2Flong%2Furl%2F2019%2F07%2FS.jpg"}
+ [
+ url: MediaProxy.encode_preview_url(original_url),
+ media_proxy_url: MediaProxy.encode_url(original_url)
+ ]
end
- test "encoded url are tried to match for proxy as `conn.request_path` encodes the url" do
- # conn.request_path will return encoded url
- request_path = "/ANALYSE-DAI-_-LE-STABLECOIN-100-D%C3%89CENTRALIS%C3%89-BQ.jpg"
+ test "returns 404 when media proxy is disabled", %{conn: conn} do
+ clear_config([:media_proxy, :enabled], false)
- assert MediaProxyController.filename_matches(
- true,
- request_path,
- "https://mydomain.com/uploads/2019/07/ANALYSE-DAI-_-LE-STABLECOIN-100-DÉCENTRALISÉ-BQ.jpg"
- ) == :ok
+ assert %Conn{
+ status: 404,
+ resp_body: "Not Found"
+ } = get(conn, "/proxy/preview/hhgfh/eeeee")
+
+ assert %Conn{
+ status: 404,
+ resp_body: "Not Found"
+ } = get(conn, "/proxy/preview/hhgfh/fff")
+ end
+
+ test "returns 404 when disabled", %{conn: conn} do
+ clear_config([:media_preview_proxy, :enabled], false)
+
+ assert %Conn{
+ status: 404,
+ resp_body: "Not Found"
+ } = get(conn, "/proxy/preview/hhgfh/eeeee")
+
+ assert %Conn{
+ status: 404,
+ resp_body: "Not Found"
+ } = get(conn, "/proxy/preview/hhgfh/fff")
+ end
+
+ test "it returns 403 for invalid signature", %{conn: conn, url: url} do
+ Pleroma.Config.put([Pleroma.Web.Endpoint, :secret_key_base], "000")
+ %{path: path} = URI.parse(url)
+
+ assert %Conn{
+ status: 403,
+ resp_body: "Forbidden"
+ } = get(conn, path)
+
+ assert %Conn{
+ status: 403,
+ resp_body: "Forbidden"
+ } = get(conn, "/proxy/preview/hhgfh/eeee")
+
+ assert %Conn{
+ status: 403,
+ resp_body: "Forbidden"
+ } = get(conn, "/proxy/preview/hhgfh/eeee/fff")
+ end
+
+ test "redirects to valid url when filename is invalidated", %{conn: conn, url: url} do
+ invalid_url = String.replace(url, "test.png", "test-file.png")
+ response = get(conn, invalid_url)
+ assert response.status == 302
+ assert redirected_to(response) == url
+ end
+
+ test "responds with 424 Failed Dependency if HEAD request to media proxy fails", %{
+ conn: conn,
+ url: url,
+ media_proxy_url: media_proxy_url
+ } do
+ Tesla.Mock.mock(fn
+ %{method: "head", url: ^media_proxy_url} ->
+ %Tesla.Env{status: 500, body: ""}
+ end)
+
+ response = get(conn, url)
+ assert response.status == 424
+ assert response.resp_body == "Can't fetch HTTP headers (HTTP 500)."
+ end
+
+ test "redirects to media proxy URI on unsupported content type", %{
+ conn: conn,
+ url: url,
+ media_proxy_url: media_proxy_url
+ } do
+ Tesla.Mock.mock(fn
+ %{method: "head", url: ^media_proxy_url} ->
+ %Tesla.Env{status: 200, body: "", headers: [{"content-type", "application/pdf"}]}
+ end)
+
+ response = get(conn, url)
+ assert response.status == 302
+ assert redirected_to(response) == media_proxy_url
+ end
+
+ test "with `static=true` and GIF image preview requested, responds with JPEG image", %{
+ conn: conn,
+ url: url,
+ media_proxy_url: media_proxy_url
+ } do
+ # Setting a high :min_content_length to ensure this scenario is not affected by its logic
+ clear_config([:media_preview_proxy, :min_content_length], 1_000_000_000)
+
+ Tesla.Mock.mock(fn
+ %{method: "head", url: ^media_proxy_url} ->
+ %Tesla.Env{
+ status: 200,
+ body: "",
+ headers: [{"content-type", "image/gif"}, {"content-length", "1001718"}]
+ }
+
+ %{method: :get, url: ^media_proxy_url} ->
+ %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.gif")}
+ end)
+
+ response = get(conn, url <> "?static=true")
+
+ assert response.status == 200
+ assert Conn.get_resp_header(response, "content-type") == ["image/jpeg"]
+ assert response.resp_body != ""
+ end
+
+ test "with GIF image preview requested and no `static` param, redirects to media proxy URI",
+ %{
+ conn: conn,
+ url: url,
+ media_proxy_url: media_proxy_url
+ } do
+ Tesla.Mock.mock(fn
+ %{method: "head", url: ^media_proxy_url} ->
+ %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/gif"}]}
+ end)
+
+ response = get(conn, url)
+
+ assert response.status == 302
+ assert redirected_to(response) == media_proxy_url
+ end
+
+ test "with `static` param and non-GIF image preview requested, " <>
+ "redirects to media preview proxy URI without `static` param",
+ %{
+ conn: conn,
+ url: url,
+ media_proxy_url: media_proxy_url
+ } do
+ Tesla.Mock.mock(fn
+ %{method: "head", url: ^media_proxy_url} ->
+ %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/jpeg"}]}
+ end)
+
+ response = get(conn, url <> "?static=true")
+
+ assert response.status == 302
+ assert redirected_to(response) == url
+ end
+
+ test "with :min_content_length setting not matched by Content-Length header, " <>
+ "redirects to media proxy URI",
+ %{
+ conn: conn,
+ url: url,
+ media_proxy_url: media_proxy_url
+ } do
+ clear_config([:media_preview_proxy, :min_content_length], 100_000)
+
+ Tesla.Mock.mock(fn
+ %{method: "head", url: ^media_proxy_url} ->
+ %Tesla.Env{
+ status: 200,
+ body: "",
+ headers: [{"content-type", "image/gif"}, {"content-length", "5000"}]
+ }
+ end)
+
+ response = get(conn, url)
+
+ assert response.status == 302
+ assert redirected_to(response) == media_proxy_url
+ end
+
+ test "thumbnails PNG images into PNG", %{
+ conn: conn,
+ url: url,
+ media_proxy_url: media_proxy_url
+ } do
+ Tesla.Mock.mock(fn
+ %{method: "head", url: ^media_proxy_url} ->
+ %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/png"}]}
+
+ %{method: :get, url: ^media_proxy_url} ->
+ %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.png")}
+ end)
+
+ response = get(conn, url)
+
+ assert response.status == 200
+ assert Conn.get_resp_header(response, "content-type") == ["image/png"]
+ assert response.resp_body != ""
+ end
+
+ test "thumbnails JPEG images into JPEG", %{
+ conn: conn,
+ url: url,
+ media_proxy_url: media_proxy_url
+ } do
+ Tesla.Mock.mock(fn
+ %{method: "head", url: ^media_proxy_url} ->
+ %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/jpeg"}]}
+
+ %{method: :get, url: ^media_proxy_url} ->
+ %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")}
+ end)
+
+ response = get(conn, url)
+
+ assert response.status == 200
+ assert Conn.get_resp_header(response, "content-type") == ["image/jpeg"]
+ assert response.resp_body != ""
+ end
+
+ test "redirects to media proxy URI in case of thumbnailing error", %{
+ conn: conn,
+ url: url,
+ media_proxy_url: media_proxy_url
+ } do
+ Tesla.Mock.mock(fn
+ %{method: "head", url: ^media_proxy_url} ->
+ %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/jpeg"}]}
+
+ %{method: :get, url: ^media_proxy_url} ->
+ %Tesla.Env{status: 200, body: "error"}
+ end)
+
+ response = get(conn, url)
+
+ assert response.status == 302
+ assert redirected_to(response) == media_proxy_url
end
end
end
diff --git a/test/web/media_proxy/media_proxy_test.exs b/test/web/media_proxy/media_proxy_test.exs
index 72885cfdd..0e6df826c 100644
--- a/test/web/media_proxy/media_proxy_test.exs
+++ b/test/web/media_proxy/media_proxy_test.exs
@@ -6,9 +6,16 @@ defmodule Pleroma.Web.MediaProxyTest do
use ExUnit.Case
use Pleroma.Tests.Helpers
+ alias Pleroma.Config
alias Pleroma.Web.Endpoint
alias Pleroma.Web.MediaProxy
+ defp decode_result(encoded) do
+ [_, "proxy", sig, base64 | _] = URI.parse(encoded).path |> String.split("/")
+ {:ok, decoded} = MediaProxy.decode_url(sig, base64)
+ decoded
+ end
+
describe "when enabled" do
setup do: clear_config([:media_proxy, :enabled], true)
@@ -35,7 +42,7 @@ test "encodes and decodes URL" do
assert String.starts_with?(
encoded,
- Pleroma.Config.get([:media_proxy, :base_url], Pleroma.Web.base_url())
+ Config.get([:media_proxy, :base_url], Pleroma.Web.base_url())
)
assert String.ends_with?(encoded, "/logo.png")
@@ -75,6 +82,64 @@ test "validates signature" do
assert MediaProxy.decode_url(sig, base64) == {:error, :invalid_signature}
end
+ def test_verify_request_path_and_url(request_path, url, expected_result) do
+ assert MediaProxy.verify_request_path_and_url(request_path, url) == expected_result
+
+ assert MediaProxy.verify_request_path_and_url(
+ %Plug.Conn{
+ params: %{"filename" => Path.basename(request_path)},
+ request_path: request_path
+ },
+ url
+ ) == expected_result
+ end
+
+ test "if first arg of `verify_request_path_and_url/2` is a Plug.Conn without \"filename\" " <>
+ "parameter, `verify_request_path_and_url/2` returns :ok " do
+ assert MediaProxy.verify_request_path_and_url(
+ %Plug.Conn{params: %{}, request_path: "/some/path"},
+ "https://instance.com/file.jpg"
+ ) == :ok
+
+ assert MediaProxy.verify_request_path_and_url(
+ %Plug.Conn{params: %{}, request_path: "/path/to/file.jpg"},
+ "https://instance.com/file.jpg"
+ ) == :ok
+ end
+
+ test "`verify_request_path_and_url/2` preserves the encoded or decoded path" do
+ test_verify_request_path_and_url(
+ "/Hello world.jpg",
+ "http://pleroma.social/Hello world.jpg",
+ :ok
+ )
+
+ test_verify_request_path_and_url(
+ "/Hello%20world.jpg",
+ "http://pleroma.social/Hello%20world.jpg",
+ :ok
+ )
+
+ test_verify_request_path_and_url(
+ "/my%2Flong%2Furl%2F2019%2F07%2FS.jpg",
+ "http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg",
+ :ok
+ )
+
+ test_verify_request_path_and_url(
+ # Note: `conn.request_path` returns encoded url
+ "/ANALYSE-DAI-_-LE-STABLECOIN-100-D%C3%89CENTRALIS%C3%89-BQ.jpg",
+ "https://mydomain.com/uploads/2019/07/ANALYSE-DAI-_-LE-STABLECOIN-100-DÉCENTRALISÉ-BQ.jpg",
+ :ok
+ )
+
+ test_verify_request_path_and_url(
+ "/my%2Flong%2Furl%2F2019%2F07%2FS",
+ "http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg",
+ {:wrong_filename, "my%2Flong%2Furl%2F2019%2F07%2FS.jpg"}
+ )
+ end
+
test "uses the configured base_url" do
base_url = "https://cache.pleroma.social"
clear_config([:media_proxy, :base_url], base_url)
@@ -124,12 +189,6 @@ test "does not encode remote urls" do
end
end
- defp decode_result(encoded) do
- [_, "proxy", sig, base64 | _] = URI.parse(encoded).path |> String.split("/")
- {:ok, decoded} = MediaProxy.decode_url(sig, base64)
- decoded
- end
-
describe "whitelist" do
setup do: clear_config([:media_proxy, :enabled], true)
diff --git a/test/web/metadata/metadata_test.exs b/test/web/metadata/metadata_test.exs
index 9d3121b7b..ca6cbe67f 100644
--- a/test/web/metadata/metadata_test.exs
+++ b/test/web/metadata/metadata_test.exs
@@ -16,7 +16,14 @@ test "for remote user" do
end
test "for local user" do
- user = insert(:user)
+ user = insert(:user, discoverable: false)
+
+ assert Pleroma.Web.Metadata.build_tags(%{user: user}) =~
+ ""
+ end
+
+ test "for local user set to discoverable" do
+ user = insert(:user, discoverable: true)
refute Pleroma.Web.Metadata.build_tags(%{user: user}) =~
""
@@ -24,11 +31,19 @@ test "for local user" do
end
describe "no metadata for private instances" do
- test "for local user" do
+ test "for local user set to discoverable" do
clear_config([:instance, :public], false)
- user = insert(:user, bio: "This is my secret fedi account bio")
+ user = insert(:user, bio: "This is my secret fedi account bio", discoverable: true)
assert "" = Pleroma.Web.Metadata.build_tags(%{user: user})
end
+
+ test "search exclusion metadata is included" do
+ clear_config([:instance, :public], false)
+ user = insert(:user, bio: "This is my secret fedi account bio", discoverable: false)
+
+ assert ~s() ==
+ Pleroma.Web.Metadata.build_tags(%{user: user})
+ end
end
end
diff --git a/test/web/metadata/restrict_indexing_test.exs b/test/web/metadata/restrict_indexing_test.exs
index aad0bac42..6b3a65372 100644
--- a/test/web/metadata/restrict_indexing_test.exs
+++ b/test/web/metadata/restrict_indexing_test.exs
@@ -14,8 +14,14 @@ test "for remote user" do
test "for local user" do
assert Pleroma.Web.Metadata.Providers.RestrictIndexing.build_tags(%{
- user: %Pleroma.User{local: true}
+ user: %Pleroma.User{local: true, discoverable: true}
}) == []
end
+
+ test "for local user when discoverable is false" do
+ assert Pleroma.Web.Metadata.Providers.RestrictIndexing.build_tags(%{
+ user: %Pleroma.User{local: true, discoverable: false}
+ }) == [{:meta, [name: "robots", content: "noindex, noarchive"], []}]
+ end
end
end
diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs
index 7be5fe09c..44a78a738 100644
--- a/test/web/pleroma_api/controllers/chat_controller_test.exs
+++ b/test/web/pleroma_api/controllers/chat_controller_test.exs
@@ -100,7 +100,7 @@ test "it fails if there is no content", %{conn: conn, user: user} do
|> post("/api/v1/pleroma/chats/#{chat.id}/messages")
|> json_response_and_validate_schema(400)
- assert result
+ assert %{"error" => "no_content"} == result
end
test "it works with an attachment", %{conn: conn, user: user} do
@@ -126,6 +126,23 @@ test "it works with an attachment", %{conn: conn, user: user} do
assert result["attachment"]
end
+
+ test "gets MRF reason when rejected", %{conn: conn, user: user} do
+ clear_config([:mrf_keyword, :reject], ["GNO"])
+ clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy])
+
+ other_user = insert(:user)
+
+ {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id)
+
+ result =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/pleroma/chats/#{chat.id}/messages", %{"content" => "GNO/Linux"})
+ |> json_response_and_validate_schema(422)
+
+ assert %{"error" => "[KeywordPolicy] Matches with rejected keyword"} == result
+ end
end
describe "DELETE /api/v1/pleroma/chats/:id/messages/:message_id" do
diff --git a/test/web/push/impl_test.exs b/test/web/push/impl_test.exs
index aeb5c1fbd..c7c17e156 100644
--- a/test/web/push/impl_test.exs
+++ b/test/web/push/impl_test.exs
@@ -12,7 +12,9 @@ defmodule Pleroma.Web.Push.ImplTest do
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Push.Impl
alias Pleroma.Web.Push.Subscription
+ alias Pleroma.Web.WebPushHttpClientMock
+ import Mock
import Pleroma.Factory
setup do
@@ -78,6 +80,22 @@ test "successful message sending" do
assert Impl.push_message(@message, @sub, @api_key, %Subscription{}) == :ok
end
+ test_with_mock "uses WebPushHttpClientMock as an HTTP client", WebPushHttpClientMock,
+ post: fn _, _, _ -> {:ok, %{status_code: 200}} end do
+ Impl.push_message(@message, @sub, @api_key, %Subscription{})
+ assert_called(WebPushHttpClientMock.post("https://example.com/example/1234", :_, :_))
+ end
+
+ test_with_mock "uses Pleroma.HTTP as an HTTP client", Pleroma.HTTP,
+ post: fn _, _, _ -> {:ok, %{status_code: 200}} end do
+ client = Application.get_env(:web_push_encryption, :http_client)
+ on_exit(fn -> Application.put_env(:web_push_encryption, :http_client, client) end)
+ Application.put_env(:web_push_encryption, :http_client, Pleroma.HTTP)
+
+ Impl.push_message(@message, @sub, @api_key, %Subscription{})
+ assert_called(Pleroma.HTTP.post("https://example.com/example/1234", :_, :_))
+ end
+
@tag capture_log: true
test "fail message sending" do
assert Impl.push_message(